184 lines · 5.8 KB · Swift
RawDownload
import Foundation #if os(macOS)import os.lock public enum ClaudeOAuthKeychainAccessGate {    private struct State {        var loaded = false        var deniedUntil: Date?    }     private static let lock = OSAllocatedUnfairLock<State>(initialState: State())    private static let defaultsKey = "claudeOAuthKeychainDeniedUntil"    private static let cooldownInterval: TimeInterval = 60 * 60 * 6    @TaskLocal private static var taskOverrideShouldAllowPromptForTesting: Bool?    #if DEBUG    public final class DeniedUntilStore: @unchecked Sendable {        public var deniedUntil: Date?         public init() {}    }     @TaskLocal private static var taskDeniedUntilStoreOverrideForTesting: DeniedUntilStore?    #endif     public static func shouldAllowPrompt(now: Date = Date()) -> Bool {        guard !KeychainAccessGate.isDisabled else { return false }        if let override = self.taskOverrideShouldAllowPromptForTesting { return override }        #if DEBUG        if let store = self.taskDeniedUntilStoreOverrideForTesting {            if let deniedUntil = store.deniedUntil, deniedUntil > now {                return false            }            store.deniedUntil = nil            return true        }        #endif        return self.lock.withLock { state in            self.loadIfNeeded(&state)            if let deniedUntil = state.deniedUntil {                if deniedUntil > now {                    return false                }                state.deniedUntil = nil                self.persist(state)            }            return true        }    }     public static func recordDenied(now: Date = Date()) {        let deniedUntil = now.addingTimeInterval(self.cooldownInterval)        #if DEBUG        if let store = self.taskDeniedUntilStoreOverrideForTesting {            store.deniedUntil = deniedUntil            return        }        #endif        self.lock.withLock { state in            self.loadIfNeeded(&state)            state.deniedUntil = deniedUntil            self.persist(state)        }    }     /// Clears the cooldown so the next attempt can proceed. Intended for user-initiated repairs.    /// - Returns: true if a cooldown was present and cleared.    public static func clearDenied(now: Date = Date()) -> Bool {        #if DEBUG        if let store = self.taskDeniedUntilStoreOverrideForTesting {            guard let deniedUntil = store.deniedUntil, deniedUntil > now else {                store.deniedUntil = nil                return false            }            store.deniedUntil = nil            return true        }        #endif        return self.lock.withLock { state in            self.loadIfNeeded(&state)            guard let deniedUntil = state.deniedUntil, deniedUntil > now else {                state.deniedUntil = nil                self.persist(state)                return false            }            state.deniedUntil = nil            self.persist(state)            return true        }    }     #if DEBUG    static func withShouldAllowPromptOverrideForTesting<T>(        _ value: Bool?,        operation: () throws -> T) rethrows -> T    {        try self.$taskOverrideShouldAllowPromptForTesting.withValue(value) {            try operation()        }    }     static func withShouldAllowPromptOverrideForTesting<T>(        _ value: Bool?,        operation: () async throws -> T) async rethrows -> T    {        try await self.$taskOverrideShouldAllowPromptForTesting.withValue(value) {            try await operation()        }    }     public static func withDeniedUntilStoreOverrideForTesting<T>(        _ store: DeniedUntilStore?,        operation: () throws -> T) rethrows -> T    {        try self.$taskDeniedUntilStoreOverrideForTesting.withValue(store) {            try operation()        }    }     public static func withDeniedUntilStoreOverrideForTesting<T>(        _ store: DeniedUntilStore?,        operation: () async throws -> T) async rethrows -> T    {        try await self.$taskDeniedUntilStoreOverrideForTesting.withValue(store) {            try await operation()        }    }     public static var currentDeniedUntilStoreOverrideForTesting: DeniedUntilStore? {        self.taskDeniedUntilStoreOverrideForTesting    }     public static func resetForTesting() {        self.lock.withLock { state in            // Keep deterministic during tests: avoid re-loading UserDefaults written by unrelated code paths.            state.loaded = true            state.deniedUntil = nil            UserDefaults.standard.removeObject(forKey: self.defaultsKey)        }    }     public static func resetInMemoryForTesting() {        self.lock.withLock { state in            state.loaded = false            state.deniedUntil = nil        }    }    #endif     private static func loadIfNeeded(_ state: inout State) {        guard !state.loaded else { return }        state.loaded = true        if let raw = UserDefaults.standard.object(forKey: self.defaultsKey) as? Double {            state.deniedUntil = Date(timeIntervalSince1970: raw)        }    }     private static func persist(_ state: State) {        if let deniedUntil = state.deniedUntil {            UserDefaults.standard.set(deniedUntil.timeIntervalSince1970, forKey: self.defaultsKey)        } else {            UserDefaults.standard.removeObject(forKey: self.defaultsKey)        }    }}#elsepublic enum ClaudeOAuthKeychainAccessGate {    public static func shouldAllowPrompt(now _: Date = Date()) -> Bool {        true    }     public static func recordDenied(now _: Date = Date()) {}     public static func clearDenied(now _: Date = Date()) -> Bool {        false    }     #if DEBUG    public static func resetForTesting() {}     public static func resetInMemoryForTesting() {}    #endif}#endif