import Foundation#if canImport(CryptoKit)import CryptoKit#endif#if canImport(Darwin)import Darwin#elseif canImport(Glibc)import Glibc#elseif canImport(Musl)import Musl#endif public enum CookieHeaderCache { public enum Scope: Sendable, Equatable { case managedAccount(UUID) case managedStoreUnreadable case profileHome(String) public var isolationIdentifier: String { switch self { case let .managedAccount(accountID): "managed.\(accountID.uuidString.lowercased())" case .managedStoreUnreadable: "managed-store-unreadable" case let .profileHome(path): "profile-home.\(Self.profileHomeDigest(path))" } } fileprivate var keychainIdentifier: String { self.isolationIdentifier } private static func profileHomeDigest(_ path: String) -> String { let standardized = URL(fileURLWithPath: path, isDirectory: true).standardizedFileURL.path #if canImport(CryptoKit) return SHA256.hash(data: Data(standardized.utf8)) .map { String(format: "%02x", $0) } .joined() #else let digest = standardized.utf8.reduce(UInt64(14_695_981_039_346_656_037)) { partial, byte in (partial ^ UInt64(byte)) &* 1_099_511_628_211 } return String(digest, radix: 16) #endif } } public struct Entry: Codable, Sendable { public let cookieHeader: String public let storedAt: Date public let sourceLabel: String public init(cookieHeader: String, storedAt: Date, sourceLabel: String) { self.cookieHeader = cookieHeader self.storedAt = storedAt self.sourceLabel = sourceLabel } } public struct ClearSummary: Equatable, Sendable { public let clearedCount: Int public let failedCount: Int public init(clearedCount: Int, failedCount: Int) { self.clearedCount = clearedCount self.failedCount = failedCount } } private static let log = CodexBarLog.logger(LogCategories.cookieCache) private static let legacyBaseURLOverrideLock = NSLock() private nonisolated(unsafe) static var legacyBaseURLOverride: URL? private struct DisplaySnapshot { let entry: Entry? let refreshAfter: Date } private enum LoadOutcome { case authoritative(Entry?, loadedFromLegacy: Bool) case temporarilyUnavailable } private static let legacyMutationLock = NSLock() private static let displayCacheLock = NSLock() private nonisolated(unsafe) static var displayCache: [KeychainCacheStore.Key: DisplaySnapshot] = [:] private nonisolated(unsafe) static var displayGenerations: [KeychainCacheStore.Key: UInt64] = [:] private nonisolated(unsafe) static var displayRevalidationsInFlight: Set<KeychainCacheStore.Key> = [] private nonisolated(unsafe) static var legacyMigrationsInFlight: Set<UsageProvider> = [] private nonisolated(unsafe) static var displayStalenessIntervalOverride: TimeInterval? private nonisolated(unsafe) static var displayUnavailableRetryIntervalOverride: TimeInterval? private static let displayStalenessInterval: TimeInterval = 30 private static let displayUnavailableRetryInterval: TimeInterval = 1 #if DEBUG @TaskLocal private static var taskLegacyBaseURLOverride: URL? @TaskLocal private static var legacyRemovalFailureOverride = false #endif private enum LegacyRemovalResult: Equatable { case removed case missing case failed } /// Settings rows render the "Cached: …" cookie label inside SwiftUI body evaluations, which /// run repeatedly within a single AppKit layout pass. Each `load` pays a synchronous /// securityd round-trip and decrypt, so display paths use this memoized variant instead: it /// returns the last known entry immediately and revalidates a stale snapshot off the calling /// path. In-process `store` and `clear` calls update the snapshot synchronously; only the /// first lookup per key pays the keychain read. public static func loadForDisplay(provider: UsageProvider, scope: Scope? = nil) -> Entry? { let key = self.key(for: provider, scope: scope) let (cached, generation) = self.beginDisplayRead(key: key) guard let cached else { switch self.loadOutcome(provider: provider, scope: scope, migrateLegacy: false) { case let .authoritative(entry, loadedFromLegacy): let committed = self.commitDisplaySnapshotIfCurrent(key: key, entry: entry, generation: generation) if loadedFromLegacy { self.scheduleLegacyMigration(provider: provider) } return committed case .temporarilyUnavailable: return self.commitTemporaryDisplaySnapshotIfCurrent(key: key, generation: generation) } } if Date() >= cached.refreshAfter { self.scheduleDisplayRevalidation(provider: provider, scope: scope, key: key, generation: generation) } return cached.entry } /// Registers the key before the Keychain read starts so `clearAll` can invalidate an /// in-flight first population even when no display snapshot exists yet. private static func beginDisplayRead(key: KeychainCacheStore.Key) -> (DisplaySnapshot?, UInt64) { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } let generation = self.displayGenerations[key] ?? 0 self.displayGenerations[key] = generation return (self.displayCache[key], generation) } private static func scheduleDisplayRevalidation( provider: UsageProvider, scope: Scope?, key: KeychainCacheStore.Key, generation: UInt64) { self.displayCacheLock.lock() let inserted = self.displayRevalidationsInFlight.insert(key).inserted self.displayCacheLock.unlock() guard inserted else { return } Task(priority: .utility) { self.revalidateDisplaySnapshot(provider: provider, scope: scope, key: key, generation: generation) } } private static func revalidateDisplaySnapshot( provider: UsageProvider, scope: Scope?, key: KeychainCacheStore.Key, generation: UInt64) { switch self.loadOutcome(provider: provider, scope: scope, migrateLegacy: false) { case let .authoritative(entry, loadedFromLegacy): _ = self.commitDisplaySnapshotIfCurrent(key: key, entry: entry, generation: generation) if loadedFromLegacy { self.scheduleLegacyMigration(provider: provider) } case .temporarilyUnavailable: self.deferDisplayRetryIfCurrent(key: key, generation: generation) } self.displayCacheLock.lock() self.displayRevalidationsInFlight.remove(key) self.displayCacheLock.unlock() } private static func scheduleLegacyMigration(provider: UsageProvider) { self.displayCacheLock.lock() let inserted = self.legacyMigrationsInFlight.insert(provider).inserted self.displayCacheLock.unlock() guard inserted else { return } Task(priority: .utility) { _ = self.migrateLegacyEntryIfNeeded(provider: provider) _ = self.displayCacheLock.withLock { self.legacyMigrationsInFlight.remove(provider) } } } /// Keychain reads for the display cache happen outside the lock, so a concurrent `store` or /// `clear` can publish newer state before the read commits. Each mutation bumps the per-key /// generation; a read only commits if the generation it started from is still current, and /// otherwise returns whatever newer snapshot won the race. private static func commitDisplaySnapshotIfCurrent( key: KeychainCacheStore.Key, entry: Entry?, generation: UInt64) -> Entry? { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } guard self.displayGenerations[key, default: 0] == generation else { return self.displayCache[key]?.entry } self.displayCache[key] = self.authoritativeDisplaySnapshot(entry: entry) return entry } private static func commitTemporaryDisplaySnapshotIfCurrent( key: KeychainCacheStore.Key, generation: UInt64) -> Entry? { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } guard self.displayGenerations[key, default: 0] == generation else { return self.displayCache[key]?.entry } if let current = self.displayCache[key] { return current.entry } self.displayCache[key] = self.temporaryDisplaySnapshot(entry: nil) return nil } private static func deferDisplayRetryIfCurrent(key: KeychainCacheStore.Key, generation: UInt64) { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } guard self.displayGenerations[key, default: 0] == generation, let current = self.displayCache[key] else { return } self.displayCache[key] = self.temporaryDisplaySnapshot(entry: current.entry) } private static func updateDisplaySnapshot(key: KeychainCacheStore.Key, entry: Entry?) { self.displayCacheLock.lock() self.displayCache[key] = self.authoritativeDisplaySnapshot(entry: entry) self.displayGenerations[key, default: 0] += 1 self.displayCacheLock.unlock() } private static func invalidateDisplaySnapshot(key: KeychainCacheStore.Key) { self.displayCacheLock.lock() self.displayCache.removeValue(forKey: key) self.displayGenerations[key, default: 0] += 1 self.displayCacheLock.unlock() } private static func currentDisplayEntry(key: KeychainCacheStore.Key) -> Entry? { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } return self.displayCache[key]?.entry } private static func authoritativeDisplaySnapshot(entry: Entry?) -> DisplaySnapshot { DisplaySnapshot(entry: entry, refreshAfter: Date().addingTimeInterval(self.currentDisplayStalenessInterval)) } private static func temporaryDisplaySnapshot(entry: Entry?) -> DisplaySnapshot { DisplaySnapshot( entry: entry, refreshAfter: Date().addingTimeInterval(self.currentDisplayUnavailableRetryInterval)) } private static var currentDisplayStalenessInterval: TimeInterval { self.displayStalenessIntervalOverride ?? self.displayStalenessInterval } private static var currentDisplayUnavailableRetryInterval: TimeInterval { self.displayUnavailableRetryIntervalOverride ?? self.displayUnavailableRetryInterval } static func setDisplayStalenessIntervalOverrideForTesting(_ interval: TimeInterval?) { self.displayStalenessIntervalOverride = interval } static func setDisplayUnavailableRetryIntervalOverrideForTesting(_ interval: TimeInterval?) { self.displayUnavailableRetryIntervalOverride = interval } static func resetDisplayCacheForTesting() { self.displayCacheLock.lock() self.displayCache.removeAll() self.displayGenerations.removeAll() self.displayRevalidationsInFlight.removeAll() self.legacyMigrationsInFlight.removeAll() self.displayCacheLock.unlock() } static func beginDisplayReadGenerationForTesting(provider: UsageProvider, scope: Scope? = nil) -> UInt64 { self.beginDisplayRead(key: self.key(for: provider, scope: scope)).1 } static func currentDisplayEntryForTesting(provider: UsageProvider, scope: Scope? = nil) -> Entry? { self.currentDisplayEntry(key: self.key(for: provider, scope: scope)) } #if DEBUG static func withLegacyRemovalFailureForTesting<T>(_ operation: () throws -> T) rethrows -> T { try self.$legacyRemovalFailureOverride.withValue(true) { try operation() } } #endif @discardableResult static func commitDisplaySnapshotIfCurrentForTesting( provider: UsageProvider, scope: Scope? = nil, entry: Entry?, generation: UInt64) -> Entry? { self.commitDisplaySnapshotIfCurrent( key: self.key(for: provider, scope: scope), entry: entry, generation: generation) } public static func load(provider: UsageProvider, scope: Scope? = nil) -> Entry? { switch self.loadOutcome(provider: provider, scope: scope, migrateLegacy: true) { case let .authoritative(entry, _): entry case .temporarilyUnavailable: nil } } static func loadSerialized(provider: UsageProvider, scope: Scope? = nil) -> Entry? { do { return try self.withLegacyMutationLock { let key = self.key(for: provider, scope: scope) switch KeychainCacheStore.load(key: key, as: Entry.self) { case let .found(entry): return entry case .temporarilyUnavailable: return nil case .invalid: KeychainCacheStore.clear(key: key) case .missing: break } guard scope == nil else { return nil } return self.migrateLegacyEntryIfNeededLocked(provider: provider) } } catch { self.log.error("Cookie cache serialized load failed: \(error)") return nil } } private static func loadOutcome( provider: UsageProvider, scope: Scope?, migrateLegacy: Bool) -> LoadOutcome { let key = self.key(for: provider, scope: scope) switch KeychainCacheStore.load(key: key, as: Entry.self) { case let .found(entry): self.log.debug("Cookie cache hit", metadata: ["provider": provider.rawValue]) return .authoritative(entry, loadedFromLegacy: false) case .temporarilyUnavailable: self.log.debug("Cookie cache temporarily unavailable", metadata: ["provider": provider.rawValue]) return .temporarilyUnavailable case .invalid: self.log.warning("Cookie cache invalid; clearing", metadata: ["provider": provider.rawValue]) KeychainCacheStore.clear(key: key) case .missing: self.log.debug("Cookie cache miss", metadata: ["provider": provider.rawValue]) } guard scope == nil else { return .authoritative(nil, loadedFromLegacy: false) } if migrateLegacy { return .authoritative( self.migrateLegacyEntryIfNeeded(provider: provider), loadedFromLegacy: false) } guard let legacy = self.loadLegacyEntry(for: provider) else { return .authoritative(nil, loadedFromLegacy: false) } return .authoritative(legacy, loadedFromLegacy: true) } /// Re-reads both stores while serialized with global cookie mutations. A display-triggered /// migration may be queued before a clear, so using the captured legacy entry here would /// otherwise allow the delayed task to restore credentials the user just removed. private static func migrateLegacyEntryIfNeeded(provider: UsageProvider) -> Entry? { do { return try self.withLegacyMutationLock { self.migrateLegacyEntryIfNeededLocked(provider: provider) } } catch { self.log.error("Cookie cache migration lock failed: \(error)") return nil } } private static func migrateLegacyEntryIfNeededLocked(provider: UsageProvider) -> Entry? { let key = self.key(for: provider, scope: nil) switch KeychainCacheStore.load(key: key, as: Entry.self) { case let .found(entry): _ = self.removeLegacyEntry(for: provider) return entry case .temporarilyUnavailable: return nil case .invalid: KeychainCacheStore.clear(key: key) case .missing: break } guard let legacy = self.loadLegacyEntry(for: provider) else { return nil } if KeychainCacheStore.storeResult(key: key, entry: legacy), self.removeLegacyEntry(for: provider) == .removed { self.log.debug("Cookie cache migrated from legacy store", metadata: ["provider": provider.rawValue]) } return legacy } public static func store( provider: UsageProvider, scope: Scope? = nil, cookieHeader: String, sourceLabel: String, now: Date = Date()) { let trimmed = cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines) guard let normalized = CookieHeaderNormalizer.normalize(trimmed), !normalized.isEmpty else { self.clear(provider: provider, scope: scope) return } let entry = Entry(cookieHeader: normalized, storedAt: now, sourceLabel: sourceLabel) do { try self.withLegacyMutationLock { _ = self.store(entry: entry, provider: provider, scope: scope, sourceLabel: sourceLabel) } } catch { self.log.error("Cookie cache store lock failed: \(error)") } } /// Stores only while the cache still matches the entry observed before an asynchronous refresh. /// A nil expected entry means the cache must still be empty. @discardableResult static func storeIfCurrent( provider: UsageProvider, scope: Scope? = nil, expected: Entry?, cookieHeader: String, sourceLabel: String, now: Date = Date()) -> Bool { let trimmed = cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines) guard let normalized = CookieHeaderNormalizer.normalize(trimmed), !normalized.isEmpty else { return false } let entry = Entry(cookieHeader: normalized, storedAt: now, sourceLabel: sourceLabel) do { return try self.withLegacyMutationLock { guard self.currentEntryMatches(expected, provider: provider, scope: scope) else { return false } return self.store(entry: entry, provider: provider, scope: scope, sourceLabel: sourceLabel) } } catch { self.log.error("Cookie cache conditional store lock failed: \(error)") return false } } /// Clears only while the cache still matches the entry that produced a failed asynchronous refresh. @discardableResult static func clearIfCurrent( provider: UsageProvider, scope: Scope? = nil, expected: Entry?) -> Bool { do { return try self.withLegacyMutationLock { guard self.currentEntryMatches(expected, provider: provider, scope: scope) else { return false } // Keep the expected Keychain row intact when legacy cleanup fails so fallback can replace it. if scope == nil, self.removeLegacyEntry(for: provider) == .failed { return false } let key = self.key(for: provider, scope: scope) let result = KeychainCacheStore.clearResult(key: key) guard result != .failed else { return false } self.updateDisplaySnapshot(key: key, entry: nil) return true } } catch { self.log.error("Cookie cache conditional clear lock failed: \(error)") return false } } private static func currentEntryMatches( _ expected: Entry?, provider: UsageProvider, scope: Scope?) -> Bool { let key = self.key(for: provider, scope: scope) switch KeychainCacheStore.load(key: key, as: Entry.self) { case let .found(current): return self.entriesMatch(current, expected)Lines 1–500 of 966. Load more
import Foundation #if canImport(CryptoKit) import CryptoKit #endif #if canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc #elseif canImport(Musl) import Musl #endif public enum CookieHeaderCache { public enum Scope: Sendable, Equatable { case managedAccount(UUID) case managedStoreUnreadable case profileHome(String) public var isolationIdentifier: String { switch self { case let .managedAccount(accountID): "managed.\(accountID.uuidString.lowercased())" case .managedStoreUnreadable: "managed-store-unreadable" case let .profileHome(path): "profile-home.\(Self.profileHomeDigest(path))" } } fileprivate var keychainIdentifier: String { self.isolationIdentifier } private static func profileHomeDigest(_ path: String) -> String { let standardized = URL(fileURLWithPath: path, isDirectory: true).standardizedFileURL.path #if canImport(CryptoKit) return SHA256.hash(data: Data(standardized.utf8)) .map { String(format: "%02x", $0) } .joined() #else let digest = standardized.utf8.reduce(UInt64(14_695_981_039_346_656_037)) { partial, byte in (partial ^ UInt64(byte)) &* 1_099_511_628_211 } return String(digest, radix: 16) #endif } } public struct Entry: Codable, Sendable { public let cookieHeader: String public let storedAt: Date public let sourceLabel: String public init(cookieHeader: String, storedAt: Date, sourceLabel: String) { self.cookieHeader = cookieHeader self.storedAt = storedAt self.sourceLabel = sourceLabel } } public struct ClearSummary: Equatable, Sendable { public let clearedCount: Int public let failedCount: Int public init(clearedCount: Int, failedCount: Int) { self.clearedCount = clearedCount self.failedCount = failedCount } } private static let log = CodexBarLog.logger(LogCategories.cookieCache) private static let legacyBaseURLOverrideLock = NSLock() private nonisolated(unsafe) static var legacyBaseURLOverride: URL? private struct DisplaySnapshot { let entry: Entry? let refreshAfter: Date } private enum LoadOutcome { case authoritative(Entry?, loadedFromLegacy: Bool) case temporarilyUnavailable } private static let legacyMutationLock = NSLock() private static let displayCacheLock = NSLock() private nonisolated(unsafe) static var displayCache: [KeychainCacheStore.Key: DisplaySnapshot] = [:] private nonisolated(unsafe) static var displayGenerations: [KeychainCacheStore.Key: UInt64] = [:] private nonisolated(unsafe) static var displayRevalidationsInFlight: Set<KeychainCacheStore.Key> = [] private nonisolated(unsafe) static var legacyMigrationsInFlight: Set<UsageProvider> = [] private nonisolated(unsafe) static var displayStalenessIntervalOverride: TimeInterval? private nonisolated(unsafe) static var displayUnavailableRetryIntervalOverride: TimeInterval? private static let displayStalenessInterval: TimeInterval = 30 private static let displayUnavailableRetryInterval: TimeInterval = 1 #if DEBUG @TaskLocal private static var taskLegacyBaseURLOverride: URL? @TaskLocal private static var legacyRemovalFailureOverride = false #endif private enum LegacyRemovalResult: Equatable { case removed case missing case failed } /// Settings rows render the "Cached: …" cookie label inside SwiftUI body evaluations, which /// run repeatedly within a single AppKit layout pass. Each `load` pays a synchronous /// securityd round-trip and decrypt, so display paths use this memoized variant instead: it /// returns the last known entry immediately and revalidates a stale snapshot off the calling /// path. In-process `store` and `clear` calls update the snapshot synchronously; only the /// first lookup per key pays the keychain read. public static func loadForDisplay(provider: UsageProvider, scope: Scope? = nil) -> Entry? { let key = self.key(for: provider, scope: scope) let (cached, generation) = self.beginDisplayRead(key: key) guard let cached else { switch self.loadOutcome(provider: provider, scope: scope, migrateLegacy: false) { case let .authoritative(entry, loadedFromLegacy): let committed = self.commitDisplaySnapshotIfCurrent(key: key, entry: entry, generation: generation) if loadedFromLegacy { self.scheduleLegacyMigration(provider: provider) } return committed case .temporarilyUnavailable: return self.commitTemporaryDisplaySnapshotIfCurrent(key: key, generation: generation) } } if Date() >= cached.refreshAfter { self.scheduleDisplayRevalidation(provider: provider, scope: scope, key: key, generation: generation) } return cached.entry } /// Registers the key before the Keychain read starts so `clearAll` can invalidate an /// in-flight first population even when no display snapshot exists yet. private static func beginDisplayRead(key: KeychainCacheStore.Key) -> (DisplaySnapshot?, UInt64) { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } let generation = self.displayGenerations[key] ?? 0 self.displayGenerations[key] = generation return (self.displayCache[key], generation) } private static func scheduleDisplayRevalidation( provider: UsageProvider, scope: Scope?, key: KeychainCacheStore.Key, generation: UInt64) { self.displayCacheLock.lock() let inserted = self.displayRevalidationsInFlight.insert(key).inserted self.displayCacheLock.unlock() guard inserted else { return } Task(priority: .utility) { self.revalidateDisplaySnapshot(provider: provider, scope: scope, key: key, generation: generation) } } private static func revalidateDisplaySnapshot( provider: UsageProvider, scope: Scope?, key: KeychainCacheStore.Key, generation: UInt64) { switch self.loadOutcome(provider: provider, scope: scope, migrateLegacy: false) { case let .authoritative(entry, loadedFromLegacy): _ = self.commitDisplaySnapshotIfCurrent(key: key, entry: entry, generation: generation) if loadedFromLegacy { self.scheduleLegacyMigration(provider: provider) } case .temporarilyUnavailable: self.deferDisplayRetryIfCurrent(key: key, generation: generation) } self.displayCacheLock.lock() self.displayRevalidationsInFlight.remove(key) self.displayCacheLock.unlock() } private static func scheduleLegacyMigration(provider: UsageProvider) { self.displayCacheLock.lock() let inserted = self.legacyMigrationsInFlight.insert(provider).inserted self.displayCacheLock.unlock() guard inserted else { return } Task(priority: .utility) { _ = self.migrateLegacyEntryIfNeeded(provider: provider) _ = self.displayCacheLock.withLock { self.legacyMigrationsInFlight.remove(provider) } } } /// Keychain reads for the display cache happen outside the lock, so a concurrent `store` or /// `clear` can publish newer state before the read commits. Each mutation bumps the per-key /// generation; a read only commits if the generation it started from is still current, and /// otherwise returns whatever newer snapshot won the race. private static func commitDisplaySnapshotIfCurrent( key: KeychainCacheStore.Key, entry: Entry?, generation: UInt64) -> Entry? { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } guard self.displayGenerations[key, default: 0] == generation else { return self.displayCache[key]?.entry } self.displayCache[key] = self.authoritativeDisplaySnapshot(entry: entry) return entry } private static func commitTemporaryDisplaySnapshotIfCurrent( key: KeychainCacheStore.Key, generation: UInt64) -> Entry? { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } guard self.displayGenerations[key, default: 0] == generation else { return self.displayCache[key]?.entry } if let current = self.displayCache[key] { return current.entry } self.displayCache[key] = self.temporaryDisplaySnapshot(entry: nil) return nil } private static func deferDisplayRetryIfCurrent(key: KeychainCacheStore.Key, generation: UInt64) { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } guard self.displayGenerations[key, default: 0] == generation, let current = self.displayCache[key] else { return } self.displayCache[key] = self.temporaryDisplaySnapshot(entry: current.entry) } private static func updateDisplaySnapshot(key: KeychainCacheStore.Key, entry: Entry?) { self.displayCacheLock.lock() self.displayCache[key] = self.authoritativeDisplaySnapshot(entry: entry) self.displayGenerations[key, default: 0] += 1 self.displayCacheLock.unlock() } private static func invalidateDisplaySnapshot(key: KeychainCacheStore.Key) { self.displayCacheLock.lock() self.displayCache.removeValue(forKey: key) self.displayGenerations[key, default: 0] += 1 self.displayCacheLock.unlock() } private static func currentDisplayEntry(key: KeychainCacheStore.Key) -> Entry? { self.displayCacheLock.lock() defer { self.displayCacheLock.unlock() } return self.displayCache[key]?.entry } private static func authoritativeDisplaySnapshot(entry: Entry?) -> DisplaySnapshot { DisplaySnapshot(entry: entry, refreshAfter: Date().addingTimeInterval(self.currentDisplayStalenessInterval)) } private static func temporaryDisplaySnapshot(entry: Entry?) -> DisplaySnapshot { DisplaySnapshot( entry: entry, refreshAfter: Date().addingTimeInterval(self.currentDisplayUnavailableRetryInterval)) } private static var currentDisplayStalenessInterval: TimeInterval { self.displayStalenessIntervalOverride ?? self.displayStalenessInterval } private static var currentDisplayUnavailableRetryInterval: TimeInterval { self.displayUnavailableRetryIntervalOverride ?? self.displayUnavailableRetryInterval } static func setDisplayStalenessIntervalOverrideForTesting(_ interval: TimeInterval?) { self.displayStalenessIntervalOverride = interval } static func setDisplayUnavailableRetryIntervalOverrideForTesting(_ interval: TimeInterval?) { self.displayUnavailableRetryIntervalOverride = interval } static func resetDisplayCacheForTesting() { self.displayCacheLock.lock() self.displayCache.removeAll() self.displayGenerations.removeAll() self.displayRevalidationsInFlight.removeAll() self.legacyMigrationsInFlight.removeAll() self.displayCacheLock.unlock() } static func beginDisplayReadGenerationForTesting(provider: UsageProvider, scope: Scope? = nil) -> UInt64 { self.beginDisplayRead(key: self.key(for: provider, scope: scope)).1 } static func currentDisplayEntryForTesting(provider: UsageProvider, scope: Scope? = nil) -> Entry? { self.currentDisplayEntry(key: self.key(for: provider, scope: scope)) } #if DEBUG static func withLegacyRemovalFailureForTesting<T>(_ operation: () throws -> T) rethrows -> T { try self.$legacyRemovalFailureOverride.withValue(true) { try operation() } } #endif @discardableResult static func commitDisplaySnapshotIfCurrentForTesting( provider: UsageProvider, scope: Scope? = nil, entry: Entry?, generation: UInt64) -> Entry? { self.commitDisplaySnapshotIfCurrent( key: self.key(for: provider, scope: scope), entry: entry, generation: generation) } public static func load(provider: UsageProvider, scope: Scope? = nil) -> Entry? { switch self.loadOutcome(provider: provider, scope: scope, migrateLegacy: true) { case let .authoritative(entry, _): entry case .temporarilyUnavailable: nil } } static func loadSerialized(provider: UsageProvider, scope: Scope? = nil) -> Entry? { do { return try self.withLegacyMutationLock { let key = self.key(for: provider, scope: scope) switch KeychainCacheStore.load(key: key, as: Entry.self) { case let .found(entry): return entry case .temporarilyUnavailable: return nil case .invalid: KeychainCacheStore.clear(key: key) case .missing: break } guard scope == nil else { return nil } return self.migrateLegacyEntryIfNeededLocked(provider: provider) } } catch { self.log.error("Cookie cache serialized load failed: \(error)") return nil } } private static func loadOutcome( provider: UsageProvider, scope: Scope?, migrateLegacy: Bool) -> LoadOutcome { let key = self.key(for: provider, scope: scope) switch KeychainCacheStore.load(key: key, as: Entry.self) { case let .found(entry): self.log.debug("Cookie cache hit", metadata: ["provider": provider.rawValue]) return .authoritative(entry, loadedFromLegacy: false) case .temporarilyUnavailable: self.log.debug("Cookie cache temporarily unavailable", metadata: ["provider": provider.rawValue]) return .temporarilyUnavailable case .invalid: self.log.warning("Cookie cache invalid; clearing", metadata: ["provider": provider.rawValue]) KeychainCacheStore.clear(key: key) case .missing: self.log.debug("Cookie cache miss", metadata: ["provider": provider.rawValue]) } guard scope == nil else { return .authoritative(nil, loadedFromLegacy: false) } if migrateLegacy { return .authoritative( self.migrateLegacyEntryIfNeeded(provider: provider), loadedFromLegacy: false) } guard let legacy = self.loadLegacyEntry(for: provider) else { return .authoritative(nil, loadedFromLegacy: false) } return .authoritative(legacy, loadedFromLegacy: true) } /// Re-reads both stores while serialized with global cookie mutations. A display-triggered /// migration may be queued before a clear, so using the captured legacy entry here would /// otherwise allow the delayed task to restore credentials the user just removed. private static func migrateLegacyEntryIfNeeded(provider: UsageProvider) -> Entry? { do { return try self.withLegacyMutationLock { self.migrateLegacyEntryIfNeededLocked(provider: provider) } } catch { self.log.error("Cookie cache migration lock failed: \(error)") return nil } } private static func migrateLegacyEntryIfNeededLocked(provider: UsageProvider) -> Entry? { let key = self.key(for: provider, scope: nil) switch KeychainCacheStore.load(key: key, as: Entry.self) { case let .found(entry): _ = self.removeLegacyEntry(for: provider) return entry case .temporarilyUnavailable: return nil case .invalid: KeychainCacheStore.clear(key: key) case .missing: break } guard let legacy = self.loadLegacyEntry(for: provider) else { return nil } if KeychainCacheStore.storeResult(key: key, entry: legacy), self.removeLegacyEntry(for: provider) == .removed { self.log.debug("Cookie cache migrated from legacy store", metadata: ["provider": provider.rawValue]) } return legacy } public static func store( provider: UsageProvider, scope: Scope? = nil, cookieHeader: String, sourceLabel: String, now: Date = Date()) { let trimmed = cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines) guard let normalized = CookieHeaderNormalizer.normalize(trimmed), !normalized.isEmpty else { self.clear(provider: provider, scope: scope) return } let entry = Entry(cookieHeader: normalized, storedAt: now, sourceLabel: sourceLabel) do { try self.withLegacyMutationLock { _ = self.store(entry: entry, provider: provider, scope: scope, sourceLabel: sourceLabel) } } catch { self.log.error("Cookie cache store lock failed: \(error)") } } /// Stores only while the cache still matches the entry observed before an asynchronous refresh. /// A nil expected entry means the cache must still be empty. @discardableResult static func storeIfCurrent( provider: UsageProvider, scope: Scope? = nil, expected: Entry?, cookieHeader: String, sourceLabel: String, now: Date = Date()) -> Bool { let trimmed = cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines) guard let normalized = CookieHeaderNormalizer.normalize(trimmed), !normalized.isEmpty else { return false } let entry = Entry(cookieHeader: normalized, storedAt: now, sourceLabel: sourceLabel) do { return try self.withLegacyMutationLock { guard self.currentEntryMatches(expected, provider: provider, scope: scope) else { return false } return self.store(entry: entry, provider: provider, scope: scope, sourceLabel: sourceLabel) } } catch { self.log.error("Cookie cache conditional store lock failed: \(error)") return false } } /// Clears only while the cache still matches the entry that produced a failed asynchronous refresh. @discardableResult static func clearIfCurrent( provider: UsageProvider, scope: Scope? = nil, expected: Entry?) -> Bool { do { return try self.withLegacyMutationLock { guard self.currentEntryMatches(expected, provider: provider, scope: scope) else { return false } // Keep the expected Keychain row intact when legacy cleanup fails so fallback can replace it. if scope == nil, self.removeLegacyEntry(for: provider) == .failed { return false } let key = self.key(for: provider, scope: scope) let result = KeychainCacheStore.clearResult(key: key) guard result != .failed else { return false } self.updateDisplaySnapshot(key: key, entry: nil) return true } } catch { self.log.error("Cookie cache conditional clear lock failed: \(error)") return false } } private static func currentEntryMatches( _ expected: Entry?, provider: UsageProvider, scope: Scope?) -> Bool { let key = self.key(for: provider, scope: scope) switch KeychainCacheStore.load(key: key, as: Entry.self) { case let .found(current): return self.entriesMatch(current, expected)