import Dispatchimport Foundation #if canImport(FoundationNetworking)import FoundationNetworking#endif #if os(macOS)import LocalAuthenticationimport Security#endif // swiftlint:disable type_body_length file_lengthpublic enum ClaudeOAuthCredentialsStore { private static let credentialsPath = ".claude/.credentials.json" static let claudeKeychainService = "Claude Code-credentials" private static let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) public static let environmentTokenKey = "CODEXBAR_CLAUDE_OAUTH_TOKEN" public static let environmentScopesKey = "CODEXBAR_CLAUDE_OAUTH_SCOPES" // Claude CLI's OAuth client ID - this is a public identifier (not a secret). // It's the same client ID used by Claude Code CLI for OAuth PKCE flow. // Can be overridden via environment variable if Anthropic ever changes it. public static let defaultOAuthClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" public static let environmentClientIDKey = "CODEXBAR_CLAUDE_OAUTH_CLIENT_ID" private static let tokenRefreshEndpoint = "https://platform.claude.com/v1/oauth/token" private static var oauthClientID: String { ProcessInfo.processInfo.environment[self.environmentClientIDKey]? .trimmingCharacters(in: .whitespacesAndNewlines) ?? self.defaultOAuthClientID } static let log = CodexBarLog.logger(LogCategories.claudeUsage) private static let fileFingerprintKey = "ClaudeOAuthCredentialsFileFingerprintV2" private static let claudeKeychainPromptLock = NSLock() private static let claudeKeychainFingerprintKey = "ClaudeOAuthClaudeKeychainFingerprintV2" private static let claudeKeychainFingerprintLegacyKey = "ClaudeOAuthClaudeKeychainFingerprintV1" private static let claudeKeychainChangeCheckLock = NSLock() private nonisolated(unsafe) static var lastClaudeKeychainChangeCheckAt: Date? private static let claudeKeychainChangeCheckMinimumInterval: TimeInterval = 60 private static let reauthenticateHint = "Run `claude` to re-authenticate." struct ClaudeKeychainFingerprint: Codable, Equatable { let modifiedAt: Int? let createdAt: Int? let persistentRefHash: String? } struct CredentialsFileFingerprint: Codable, Equatable { let modifiedAtMs: Int? let size: Int } struct CacheEntry: Codable { let data: Data let storedAt: Date let owner: ClaudeOAuthCredentialOwner? init(data: Data, storedAt: Date, owner: ClaudeOAuthCredentialOwner? = nil) { self.data = data self.storedAt = storedAt self.owner = owner } } private nonisolated(unsafe) static var credentialsURLOverride: URL? #if DEBUG @TaskLocal private static var taskCredentialsURLOverride: URL? #endif @TaskLocal static var allowBackgroundPromptBootstrap: Bool = false // In-memory cache (nonisolated for synchronous access) private static let memoryCacheLock = NSLock() private nonisolated(unsafe) static var cachedCredentialRecord: ClaudeOAuthCredentialRecord? private nonisolated(unsafe) static var cacheTimestamp: Date? private static let memoryCacheValidityDuration: TimeInterval = 1800 private static func readMemoryCache() -> (record: ClaudeOAuthCredentialRecord?, timestamp: Date?) { #if DEBUG if let store = self.taskMemoryCacheStoreOverride { return (store.record, store.timestamp) } #endif self.memoryCacheLock.lock() defer { self.memoryCacheLock.unlock() } return (self.cachedCredentialRecord, self.cacheTimestamp) } private static func writeMemoryCache(record: ClaudeOAuthCredentialRecord?, timestamp: Date?) { #if DEBUG if let store = self.taskMemoryCacheStoreOverride { store.record = record store.timestamp = timestamp return } #endif self.memoryCacheLock.lock() self.cachedCredentialRecord = record self.cacheTimestamp = timestamp self.memoryCacheLock.unlock() } private struct CollaboratorContext { let allowBackgroundPromptBootstrap: Bool #if DEBUG let credentialsURLOverride: URL? let testingOverrides: TestingOverridesSnapshot #endif func run<T>(_ operation: () throws -> T) rethrows -> T { try ClaudeOAuthCredentialsStore.$allowBackgroundPromptBootstrap .withValue(self.allowBackgroundPromptBootstrap) { #if DEBUG try ClaudeOAuthCredentialsStore.withTestingOverridesSnapshotForTask(self.testingOverrides) { try ClaudeOAuthCredentialsStore .withCredentialsURLOverrideForTesting(self.credentialsURLOverride) { try operation() } } #else try operation() #endif } } func run<T>(_ operation: () async throws -> T) async rethrows -> T { try await ClaudeOAuthCredentialsStore.$allowBackgroundPromptBootstrap .withValue(self.allowBackgroundPromptBootstrap) { #if DEBUG try await ClaudeOAuthCredentialsStore.withTestingOverridesSnapshotForTask(self.testingOverrides) { try await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting( self.credentialsURLOverride) { try await operation() } } #else try await operation() #endif } } } private static func currentCollaboratorContext() -> CollaboratorContext { #if DEBUG CollaboratorContext( allowBackgroundPromptBootstrap: self.allowBackgroundPromptBootstrap, credentialsURLOverride: self.taskCredentialsURLOverride, testingOverrides: self.currentTestingOverridesSnapshotForTask) #else CollaboratorContext( allowBackgroundPromptBootstrap: self.allowBackgroundPromptBootstrap) #endif } private struct Repository { let context: CollaboratorContext func load(environment: [String: String], allowKeychainPrompt: Bool, respectKeychainPromptCooldown: Bool) throws -> ClaudeOAuthCredentials { try self.loadRecord( environment: environment, allowKeychainPrompt: allowKeychainPrompt, respectKeychainPromptCooldown: respectKeychainPromptCooldown, allowClaudeKeychainRepairWithoutPrompt: true).credentials } func loadRecord( environment: [String: String], allowKeychainPrompt: Bool, respectKeychainPromptCooldown: Bool, allowClaudeKeychainRepairWithoutPrompt: Bool) throws -> ClaudeOAuthCredentialRecord { try self.context.run { let shouldRespectKeychainPromptCooldownForSilentProbes = respectKeychainPromptCooldown || !allowKeychainPrompt if let credentials = ClaudeOAuthCredentialsStore.loadFromEnvironment(environment) { return ClaudeOAuthCredentialRecord( credentials: credentials, owner: .environment, source: .environment) } _ = self.invalidateCacheIfCredentialsFileChanged() let recovery = Recovery(context: self.context) let memory = ClaudeOAuthCredentialsStore.readMemoryCache() if let cachedRecord = memory.record, let timestamp = memory.timestamp, Date().timeIntervalSince(timestamp) < ClaudeOAuthCredentialsStore.memoryCacheValidityDuration, !cachedRecord.credentials.isExpired { let owner = self.resolvedCacheOwner(cachedRecord.owner) let record = ClaudeOAuthCredentialRecord( credentials: cachedRecord.credentials, owner: owner, source: .memoryCache) if recovery.shouldAttemptFreshnessSyncFromClaudeKeychain(cached: record), let synced = recovery.syncWithClaudeKeychainIfChanged( cached: record, respectKeychainPromptCooldown: shouldRespectKeychainPromptCooldownForSilentProbes) { return synced } return record } var lastError: Error? var expiredRecord: ClaudeOAuthCredentialRecord? var cacheTemporarilyUnavailable = false switch KeychainCacheStore.load(key: ClaudeOAuthCredentialsStore.cacheKey, as: CacheEntry.self) { case let .found(entry): if let creds = try? ClaudeOAuthCredentials.parse(data: entry.data) { let owner = self.resolvedCacheOwner(entry.owner ?? .claudeCLI) let record = ClaudeOAuthCredentialRecord( credentials: creds, owner: owner, source: .cacheKeychain) if creds.isExpired { expiredRecord = record } else { if recovery.shouldAttemptFreshnessSyncFromClaudeKeychain(cached: record), let synced = recovery.syncWithClaudeKeychainIfChanged( cached: record, respectKeychainPromptCooldown: shouldRespectKeychainPromptCooldownForSilentProbes) { return synced } ClaudeOAuthCredentialsStore.writeMemoryCache( record: ClaudeOAuthCredentialRecord( credentials: creds, owner: owner, source: .memoryCache), timestamp: Date()) return record } } else { KeychainCacheStore.clear(key: ClaudeOAuthCredentialsStore.cacheKey) } case .invalid: KeychainCacheStore.clear(key: ClaudeOAuthCredentialsStore.cacheKey) case .temporarilyUnavailable: cacheTemporarilyUnavailable = true case .missing: break } do { let fileData = try ClaudeOAuthCredentialsStore.loadFromFile() let creds = try ClaudeOAuthCredentials.parse(data: fileData) let record = ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .credentialsFile) if creds.isExpired { expiredRecord = record } else { ClaudeOAuthCredentialsStore.writeMemoryCache( record: ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .memoryCache), timestamp: Date()) if !cacheTemporarilyUnavailable { ClaudeOAuthCredentialsStore.saveToCacheKeychain(fileData, owner: .claudeCLI) } return record } } catch let error as ClaudeOAuthCredentialsError { if case .notFound = error { } else { lastError = error } } catch { lastError = error } if allowClaudeKeychainRepairWithoutPrompt, !allowKeychainPrompt { if let repaired = recovery.repairFromClaudeKeychainWithoutPromptIfAllowed( now: Date(), respectKeychainPromptCooldown: shouldRespectKeychainPromptCooldownForSilentProbes, allowCacheKeychainWrite: !cacheTemporarilyUnavailable) { return repaired } } if let prompted = self.loadFromClaudeKeychainWithPromptIfAllowed( allowKeychainPrompt: allowKeychainPrompt, respectKeychainPromptCooldown: respectKeychainPromptCooldown, allowCacheKeychainWrite: !cacheTemporarilyUnavailable, lastError: &lastError) { return prompted } if let expiredRecord { return expiredRecord } if let lastError { throw lastError } throw ClaudeOAuthCredentialsError.notFound } } private func loadFromClaudeKeychainWithPromptIfAllowed( allowKeychainPrompt: Bool, respectKeychainPromptCooldown: Bool, allowCacheKeychainWrite: Bool, lastError: inout Error?) -> ClaudeOAuthCredentialRecord? { let shouldApplyPromptCooldown = ClaudeOAuthCredentialsStore.isPromptPolicyApplicable && respectKeychainPromptCooldown let promptAllowed = allowKeychainPrompt && (!shouldApplyPromptCooldown || ClaudeOAuthKeychainAccessGate.shouldAllowPrompt()) guard promptAllowed else { return nil } do { ClaudeOAuthCredentialsStore.claudeKeychainPromptLock.lock() defer { ClaudeOAuthCredentialsStore.claudeKeychainPromptLock.unlock() } let memory = ClaudeOAuthCredentialsStore.readMemoryCache() if let cachedRecord = memory.record, let timestamp = memory.timestamp, Date().timeIntervalSince(timestamp) < ClaudeOAuthCredentialsStore.memoryCacheValidityDuration, !cachedRecord.credentials.isExpired { let owner = self.resolvedCacheOwner(cachedRecord.owner) return ClaudeOAuthCredentialRecord( credentials: cachedRecord.credentials, owner: owner, source: .memoryCache) } if case let .found(entry) = KeychainCacheStore.load( key: ClaudeOAuthCredentialsStore.cacheKey, as: CacheEntry.self), let creds = try? ClaudeOAuthCredentials.parse(data: entry.data), !creds.isExpired { let owner = self.resolvedCacheOwner(entry.owner ?? .claudeCLI) return ClaudeOAuthCredentialRecord( credentials: creds, owner: owner, source: .cacheKeychain) } let promptMode = ClaudeOAuthKeychainPromptPreference.current() guard ClaudeOAuthCredentialsStore.shouldAllowClaudeCodeKeychainAccess(mode: promptMode) else { return nil } if ClaudeOAuthCredentialsStore.shouldPreferSecurityCLIKeychainRead(), let keychainData = ClaudeOAuthCredentialsStore.loadFromClaudeKeychainViaSecurityCLIIfEnabled( interaction: ProviderInteractionContext.current) { let creds = try ClaudeOAuthCredentials.parse(data: keychainData) let record = ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .claudeKeychain) ClaudeOAuthCredentialsStore.writeMemoryCache( record: ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .memoryCache), timestamp: Date()) if allowCacheKeychainWrite { ClaudeOAuthCredentialsStore.saveToCacheKeychain(keychainData, owner: .claudeCLI) } return record } let shouldPreferSecurityCLIKeychainRead = ClaudeOAuthCredentialsStore.shouldPreferSecurityCLIKeychainRead() var fallbackPromptMode = promptMode if shouldPreferSecurityCLIKeychainRead { fallbackPromptMode = ClaudeOAuthKeychainPromptPreference.securityFrameworkFallbackMode() let fallbackDecision = ClaudeOAuthCredentialsStore.securityFrameworkFallbackPromptDecision( promptMode: fallbackPromptMode, allowKeychainPrompt: allowKeychainPrompt, respectKeychainPromptCooldown: respectKeychainPromptCooldown) ClaudeOAuthCredentialsStore.log.debug( "Claude keychain Security.framework fallback prompt policy evaluated", metadata: [ "reader": "securityFrameworkFallback", "fallbackPromptMode": fallbackPromptMode.rawValue, "fallbackPromptAllowed": "\(fallbackDecision.allowed)", "fallbackBlockedReason": fallbackDecision.blockedReason ?? "none", ]) guard fallbackDecision.allowed else { return nil } } if ClaudeOAuthCredentialsStore.shouldShowClaudeKeychainPreAlert() { KeychainPromptHandler.notify( KeychainPromptContext( kind: .claudeOAuth, service: ClaudeOAuthCredentialsStore.claudeKeychainService, account: nil)) } let keychainData: Data = if shouldPreferSecurityCLIKeychainRead { try ClaudeOAuthCredentialsStore.loadFromClaudeKeychainUsingSecurityFramework( promptMode: fallbackPromptMode, allowKeychainPrompt: true) } else { try ClaudeOAuthCredentialsStore.loadFromClaudeKeychain() } let creds = try ClaudeOAuthCredentials.parse(data: keychainData) let record = ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .claudeKeychain) ClaudeOAuthCredentialsStore.writeMemoryCache( record: ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .memoryCache), timestamp: Date()) if allowCacheKeychainWrite { ClaudeOAuthCredentialsStore.saveToCacheKeychain(keychainData, owner: .claudeCLI) } return record } catch let error as ClaudeOAuthCredentialsError { if case .notFound = error { } else { lastError = error } } catch { lastError = error } return nil } private func resolvedCacheOwner(_ owner: ClaudeOAuthCredentialOwner) -> ClaudeOAuthCredentialOwner { guard owner == .codexbar else { return owner } guard self.hasClaudeCLIStorageWithoutPrompt() else { return owner } // Claude Code rotates refresh tokens; when its storage exists, it owns the refresh lifecycle. return .claudeCLI } private func hasClaudeCLIStorageWithoutPrompt() -> Bool { if ClaudeOAuthCredentialsStore.currentFileFingerprint() != nil { return true } return ClaudeOAuthCredentialsStore.hasClaudeKeychainItemWithoutPrompt() } @discardableResult func invalidateCacheIfCredentialsFileChanged() -> Bool { self.context.run { let current = ClaudeOAuthCredentialsStore.currentFileFingerprint() let stored = ClaudeOAuthCredentialsStore.loadFileFingerprint() guard current != stored else { return false } ClaudeOAuthCredentialsStore.log.info("Claude OAuth credentials file changed; invalidating cache") ClaudeOAuthCredentialsStore.writeMemoryCache(record: nil, timestamp: nil) var shouldClearKeychainCache = false var shouldSaveFileFingerprint = true if let current { if let modifiedAtMs = current.modifiedAtMs { let modifiedAt = Date(timeIntervalSince1970: TimeInterval(Double(modifiedAtMs) / 1000.0)) switch KeychainCacheStore.load( key: ClaudeOAuthCredentialsStore.cacheKey, as: CacheEntry.self) { case let .found(entry): if entry.storedAt < modifiedAt { shouldClearKeychainCache = true } case .missing, .invalid: shouldClearKeychainCache = true case .temporarilyUnavailable: shouldClearKeychainCache = false shouldSaveFileFingerprint = false } } else { shouldClearKeychainCache = true } } else { shouldClearKeychainCache = true } if shouldClearKeychainCache { ClaudeOAuthCredentialsStore.clearCacheKeychain() } if shouldSaveFileFingerprint { ClaudeOAuthCredentialsStore.saveFileFingerprint(current) } return true } } func invalidateCache() { self.context.run { ClaudeOAuthCredentialsStore.writeMemoryCache(record: nil, timestamp: nil) ClaudeOAuthCredentialsStore.clearCacheKeychain() } } Lines 1–500 of 2140. Load more
import Dispatch import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif #if os(macOS) import LocalAuthentication import Security #endif // swiftlint:disable type_body_length file_length public enum ClaudeOAuthCredentialsStore { private static let credentialsPath = ".claude/.credentials.json" static let claudeKeychainService = "Claude Code-credentials" private static let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude) public static let environmentTokenKey = "CODEXBAR_CLAUDE_OAUTH_TOKEN" public static let environmentScopesKey = "CODEXBAR_CLAUDE_OAUTH_SCOPES" // Claude CLI's OAuth client ID - this is a public identifier (not a secret). // It's the same client ID used by Claude Code CLI for OAuth PKCE flow. // Can be overridden via environment variable if Anthropic ever changes it. public static let defaultOAuthClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" public static let environmentClientIDKey = "CODEXBAR_CLAUDE_OAUTH_CLIENT_ID" private static let tokenRefreshEndpoint = "https://platform.claude.com/v1/oauth/token" private static var oauthClientID: String { ProcessInfo.processInfo.environment[self.environmentClientIDKey]? .trimmingCharacters(in: .whitespacesAndNewlines) ?? self.defaultOAuthClientID } static let log = CodexBarLog.logger(LogCategories.claudeUsage) private static let fileFingerprintKey = "ClaudeOAuthCredentialsFileFingerprintV2" private static let claudeKeychainPromptLock = NSLock() private static let claudeKeychainFingerprintKey = "ClaudeOAuthClaudeKeychainFingerprintV2" private static let claudeKeychainFingerprintLegacyKey = "ClaudeOAuthClaudeKeychainFingerprintV1" private static let claudeKeychainChangeCheckLock = NSLock() private nonisolated(unsafe) static var lastClaudeKeychainChangeCheckAt: Date? private static let claudeKeychainChangeCheckMinimumInterval: TimeInterval = 60 private static let reauthenticateHint = "Run `claude` to re-authenticate." struct ClaudeKeychainFingerprint: Codable, Equatable { let modifiedAt: Int? let createdAt: Int? let persistentRefHash: String? } struct CredentialsFileFingerprint: Codable, Equatable { let modifiedAtMs: Int? let size: Int } struct CacheEntry: Codable { let data: Data let storedAt: Date let owner: ClaudeOAuthCredentialOwner? init(data: Data, storedAt: Date, owner: ClaudeOAuthCredentialOwner? = nil) { self.data = data self.storedAt = storedAt self.owner = owner } } private nonisolated(unsafe) static var credentialsURLOverride: URL? #if DEBUG @TaskLocal private static var taskCredentialsURLOverride: URL? #endif @TaskLocal static var allowBackgroundPromptBootstrap: Bool = false // In-memory cache (nonisolated for synchronous access) private static let memoryCacheLock = NSLock() private nonisolated(unsafe) static var cachedCredentialRecord: ClaudeOAuthCredentialRecord? private nonisolated(unsafe) static var cacheTimestamp: Date? private static let memoryCacheValidityDuration: TimeInterval = 1800 private static func readMemoryCache() -> (record: ClaudeOAuthCredentialRecord?, timestamp: Date?) { #if DEBUG if let store = self.taskMemoryCacheStoreOverride { return (store.record, store.timestamp) } #endif self.memoryCacheLock.lock() defer { self.memoryCacheLock.unlock() } return (self.cachedCredentialRecord, self.cacheTimestamp) } private static func writeMemoryCache(record: ClaudeOAuthCredentialRecord?, timestamp: Date?) { #if DEBUG if let store = self.taskMemoryCacheStoreOverride { store.record = record store.timestamp = timestamp return } #endif self.memoryCacheLock.lock() self.cachedCredentialRecord = record self.cacheTimestamp = timestamp self.memoryCacheLock.unlock() } private struct CollaboratorContext { let allowBackgroundPromptBootstrap: Bool #if DEBUG let credentialsURLOverride: URL? let testingOverrides: TestingOverridesSnapshot #endif func run<T>(_ operation: () throws -> T) rethrows -> T { try ClaudeOAuthCredentialsStore.$allowBackgroundPromptBootstrap .withValue(self.allowBackgroundPromptBootstrap) { #if DEBUG try ClaudeOAuthCredentialsStore.withTestingOverridesSnapshotForTask(self.testingOverrides) { try ClaudeOAuthCredentialsStore .withCredentialsURLOverrideForTesting(self.credentialsURLOverride) { try operation() } } #else try operation() #endif } } func run<T>(_ operation: () async throws -> T) async rethrows -> T { try await ClaudeOAuthCredentialsStore.$allowBackgroundPromptBootstrap .withValue(self.allowBackgroundPromptBootstrap) { #if DEBUG try await ClaudeOAuthCredentialsStore.withTestingOverridesSnapshotForTask(self.testingOverrides) { try await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting( self.credentialsURLOverride) { try await operation() } } #else try await operation() #endif } } } private static func currentCollaboratorContext() -> CollaboratorContext { #if DEBUG CollaboratorContext( allowBackgroundPromptBootstrap: self.allowBackgroundPromptBootstrap, credentialsURLOverride: self.taskCredentialsURLOverride, testingOverrides: self.currentTestingOverridesSnapshotForTask) #else CollaboratorContext( allowBackgroundPromptBootstrap: self.allowBackgroundPromptBootstrap) #endif } private struct Repository { let context: CollaboratorContext func load(environment: [String: String], allowKeychainPrompt: Bool, respectKeychainPromptCooldown: Bool) throws -> ClaudeOAuthCredentials { try self.loadRecord( environment: environment, allowKeychainPrompt: allowKeychainPrompt, respectKeychainPromptCooldown: respectKeychainPromptCooldown, allowClaudeKeychainRepairWithoutPrompt: true).credentials } func loadRecord( environment: [String: String], allowKeychainPrompt: Bool, respectKeychainPromptCooldown: Bool, allowClaudeKeychainRepairWithoutPrompt: Bool) throws -> ClaudeOAuthCredentialRecord { try self.context.run { let shouldRespectKeychainPromptCooldownForSilentProbes = respectKeychainPromptCooldown || !allowKeychainPrompt if let credentials = ClaudeOAuthCredentialsStore.loadFromEnvironment(environment) { return ClaudeOAuthCredentialRecord( credentials: credentials, owner: .environment, source: .environment) } _ = self.invalidateCacheIfCredentialsFileChanged() let recovery = Recovery(context: self.context) let memory = ClaudeOAuthCredentialsStore.readMemoryCache() if let cachedRecord = memory.record, let timestamp = memory.timestamp, Date().timeIntervalSince(timestamp) < ClaudeOAuthCredentialsStore.memoryCacheValidityDuration, !cachedRecord.credentials.isExpired { let owner = self.resolvedCacheOwner(cachedRecord.owner) let record = ClaudeOAuthCredentialRecord( credentials: cachedRecord.credentials, owner: owner, source: .memoryCache) if recovery.shouldAttemptFreshnessSyncFromClaudeKeychain(cached: record), let synced = recovery.syncWithClaudeKeychainIfChanged( cached: record, respectKeychainPromptCooldown: shouldRespectKeychainPromptCooldownForSilentProbes) { return synced } return record } var lastError: Error? var expiredRecord: ClaudeOAuthCredentialRecord? var cacheTemporarilyUnavailable = false switch KeychainCacheStore.load(key: ClaudeOAuthCredentialsStore.cacheKey, as: CacheEntry.self) { case let .found(entry): if let creds = try? ClaudeOAuthCredentials.parse(data: entry.data) { let owner = self.resolvedCacheOwner(entry.owner ?? .claudeCLI) let record = ClaudeOAuthCredentialRecord( credentials: creds, owner: owner, source: .cacheKeychain) if creds.isExpired { expiredRecord = record } else { if recovery.shouldAttemptFreshnessSyncFromClaudeKeychain(cached: record), let synced = recovery.syncWithClaudeKeychainIfChanged( cached: record, respectKeychainPromptCooldown: shouldRespectKeychainPromptCooldownForSilentProbes) { return synced } ClaudeOAuthCredentialsStore.writeMemoryCache( record: ClaudeOAuthCredentialRecord( credentials: creds, owner: owner, source: .memoryCache), timestamp: Date()) return record } } else { KeychainCacheStore.clear(key: ClaudeOAuthCredentialsStore.cacheKey) } case .invalid: KeychainCacheStore.clear(key: ClaudeOAuthCredentialsStore.cacheKey) case .temporarilyUnavailable: cacheTemporarilyUnavailable = true case .missing: break } do { let fileData = try ClaudeOAuthCredentialsStore.loadFromFile() let creds = try ClaudeOAuthCredentials.parse(data: fileData) let record = ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .credentialsFile) if creds.isExpired { expiredRecord = record } else { ClaudeOAuthCredentialsStore.writeMemoryCache( record: ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .memoryCache), timestamp: Date()) if !cacheTemporarilyUnavailable { ClaudeOAuthCredentialsStore.saveToCacheKeychain(fileData, owner: .claudeCLI) } return record } } catch let error as ClaudeOAuthCredentialsError { if case .notFound = error { } else { lastError = error } } catch { lastError = error } if allowClaudeKeychainRepairWithoutPrompt, !allowKeychainPrompt { if let repaired = recovery.repairFromClaudeKeychainWithoutPromptIfAllowed( now: Date(), respectKeychainPromptCooldown: shouldRespectKeychainPromptCooldownForSilentProbes, allowCacheKeychainWrite: !cacheTemporarilyUnavailable) { return repaired } } if let prompted = self.loadFromClaudeKeychainWithPromptIfAllowed( allowKeychainPrompt: allowKeychainPrompt, respectKeychainPromptCooldown: respectKeychainPromptCooldown, allowCacheKeychainWrite: !cacheTemporarilyUnavailable, lastError: &lastError) { return prompted } if let expiredRecord { return expiredRecord } if let lastError { throw lastError } throw ClaudeOAuthCredentialsError.notFound } } private func loadFromClaudeKeychainWithPromptIfAllowed( allowKeychainPrompt: Bool, respectKeychainPromptCooldown: Bool, allowCacheKeychainWrite: Bool, lastError: inout Error?) -> ClaudeOAuthCredentialRecord? { let shouldApplyPromptCooldown = ClaudeOAuthCredentialsStore.isPromptPolicyApplicable && respectKeychainPromptCooldown let promptAllowed = allowKeychainPrompt && (!shouldApplyPromptCooldown || ClaudeOAuthKeychainAccessGate.shouldAllowPrompt()) guard promptAllowed else { return nil } do { ClaudeOAuthCredentialsStore.claudeKeychainPromptLock.lock() defer { ClaudeOAuthCredentialsStore.claudeKeychainPromptLock.unlock() } let memory = ClaudeOAuthCredentialsStore.readMemoryCache() if let cachedRecord = memory.record, let timestamp = memory.timestamp, Date().timeIntervalSince(timestamp) < ClaudeOAuthCredentialsStore.memoryCacheValidityDuration, !cachedRecord.credentials.isExpired { let owner = self.resolvedCacheOwner(cachedRecord.owner) return ClaudeOAuthCredentialRecord( credentials: cachedRecord.credentials, owner: owner, source: .memoryCache) } if case let .found(entry) = KeychainCacheStore.load( key: ClaudeOAuthCredentialsStore.cacheKey, as: CacheEntry.self), let creds = try? ClaudeOAuthCredentials.parse(data: entry.data), !creds.isExpired { let owner = self.resolvedCacheOwner(entry.owner ?? .claudeCLI) return ClaudeOAuthCredentialRecord( credentials: creds, owner: owner, source: .cacheKeychain) } let promptMode = ClaudeOAuthKeychainPromptPreference.current() guard ClaudeOAuthCredentialsStore.shouldAllowClaudeCodeKeychainAccess(mode: promptMode) else { return nil } if ClaudeOAuthCredentialsStore.shouldPreferSecurityCLIKeychainRead(), let keychainData = ClaudeOAuthCredentialsStore.loadFromClaudeKeychainViaSecurityCLIIfEnabled( interaction: ProviderInteractionContext.current) { let creds = try ClaudeOAuthCredentials.parse(data: keychainData) let record = ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .claudeKeychain) ClaudeOAuthCredentialsStore.writeMemoryCache( record: ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .memoryCache), timestamp: Date()) if allowCacheKeychainWrite { ClaudeOAuthCredentialsStore.saveToCacheKeychain(keychainData, owner: .claudeCLI) } return record } let shouldPreferSecurityCLIKeychainRead = ClaudeOAuthCredentialsStore.shouldPreferSecurityCLIKeychainRead() var fallbackPromptMode = promptMode if shouldPreferSecurityCLIKeychainRead { fallbackPromptMode = ClaudeOAuthKeychainPromptPreference.securityFrameworkFallbackMode() let fallbackDecision = ClaudeOAuthCredentialsStore.securityFrameworkFallbackPromptDecision( promptMode: fallbackPromptMode, allowKeychainPrompt: allowKeychainPrompt, respectKeychainPromptCooldown: respectKeychainPromptCooldown) ClaudeOAuthCredentialsStore.log.debug( "Claude keychain Security.framework fallback prompt policy evaluated", metadata: [ "reader": "securityFrameworkFallback", "fallbackPromptMode": fallbackPromptMode.rawValue, "fallbackPromptAllowed": "\(fallbackDecision.allowed)", "fallbackBlockedReason": fallbackDecision.blockedReason ?? "none", ]) guard fallbackDecision.allowed else { return nil } } if ClaudeOAuthCredentialsStore.shouldShowClaudeKeychainPreAlert() { KeychainPromptHandler.notify( KeychainPromptContext( kind: .claudeOAuth, service: ClaudeOAuthCredentialsStore.claudeKeychainService, account: nil)) } let keychainData: Data = if shouldPreferSecurityCLIKeychainRead { try ClaudeOAuthCredentialsStore.loadFromClaudeKeychainUsingSecurityFramework( promptMode: fallbackPromptMode, allowKeychainPrompt: true) } else { try ClaudeOAuthCredentialsStore.loadFromClaudeKeychain() } let creds = try ClaudeOAuthCredentials.parse(data: keychainData) let record = ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .claudeKeychain) ClaudeOAuthCredentialsStore.writeMemoryCache( record: ClaudeOAuthCredentialRecord( credentials: creds, owner: .claudeCLI, source: .memoryCache), timestamp: Date()) if allowCacheKeychainWrite { ClaudeOAuthCredentialsStore.saveToCacheKeychain(keychainData, owner: .claudeCLI) } return record } catch let error as ClaudeOAuthCredentialsError { if case .notFound = error { } else { lastError = error } } catch { lastError = error } return nil } private func resolvedCacheOwner(_ owner: ClaudeOAuthCredentialOwner) -> ClaudeOAuthCredentialOwner { guard owner == .codexbar else { return owner } guard self.hasClaudeCLIStorageWithoutPrompt() else { return owner } // Claude Code rotates refresh tokens; when its storage exists, it owns the refresh lifecycle. return .claudeCLI } private func hasClaudeCLIStorageWithoutPrompt() -> Bool { if ClaudeOAuthCredentialsStore.currentFileFingerprint() != nil { return true } return ClaudeOAuthCredentialsStore.hasClaudeKeychainItemWithoutPrompt() } @discardableResult func invalidateCacheIfCredentialsFileChanged() -> Bool { self.context.run { let current = ClaudeOAuthCredentialsStore.currentFileFingerprint() let stored = ClaudeOAuthCredentialsStore.loadFileFingerprint() guard current != stored else { return false } ClaudeOAuthCredentialsStore.log.info("Claude OAuth credentials file changed; invalidating cache") ClaudeOAuthCredentialsStore.writeMemoryCache(record: nil, timestamp: nil) var shouldClearKeychainCache = false var shouldSaveFileFingerprint = true if let current { if let modifiedAtMs = current.modifiedAtMs { let modifiedAt = Date(timeIntervalSince1970: TimeInterval(Double(modifiedAtMs) / 1000.0)) switch KeychainCacheStore.load( key: ClaudeOAuthCredentialsStore.cacheKey, as: CacheEntry.self) { case let .found(entry): if entry.storedAt < modifiedAt { shouldClearKeychainCache = true } case .missing, .invalid: shouldClearKeychainCache = true case .temporarilyUnavailable: shouldClearKeychainCache = false shouldSaveFileFingerprint = false } } else { shouldClearKeychainCache = true } } else { shouldClearKeychainCache = true } if shouldClearKeychainCache { ClaudeOAuthCredentialsStore.clearCacheKeychain() } if shouldSaveFileFingerprint { ClaudeOAuthCredentialsStore.saveFileFingerprint(current) } return true } } func invalidateCache() { self.context.run { ClaudeOAuthCredentialsStore.writeMemoryCache(record: nil, timestamp: nil) ClaudeOAuthCredentialsStore.clearCacheKeychain() } }