Files
2026-07-13 12:22:33 +08:00

178 lines
6.3 KiB
Swift

import Foundation
public enum ClaudeOAuthKeychainPromptMode: String, Sendable, Codable, CaseIterable {
case never
case onlyOnUserAction
case always
}
public enum ClaudeOAuthKeychainPromptPreference {
static let releaseApplicationDefaultsDomain = "com.steipete.codexbar"
static let debugApplicationDefaultsDomain = "com.steipete.codexbar.debug"
private static let userDefaultsKey = "claudeOAuthKeychainPromptMode"
static var applicationDefaultsDomain: String {
self.resolveApplicationDefaultsDomain(
bundleIdentifier: Bundle.main.bundleIdentifier,
bundleURL: Bundle.main.bundleURL,
executableURL: Bundle.main.executableURL,
invocationURL: CommandLine.arguments.first.map(URL.init(fileURLWithPath:)))
}
#if DEBUG
private final class UserDefaultsBox: @unchecked Sendable {
let value: UserDefaults
init(_ value: UserDefaults) {
self.value = value
}
}
@TaskLocal private static var taskOverride: ClaudeOAuthKeychainPromptMode?
@TaskLocal private static var taskApplicationUserDefaultsOverride: UserDefaultsBox?
#endif
public static func current(userDefaults: UserDefaults? = nil) -> ClaudeOAuthKeychainPromptMode {
self.effectiveMode(userDefaults: userDefaults)
}
public static func storedMode(userDefaults: UserDefaults? = nil) -> ClaudeOAuthKeychainPromptMode {
#if DEBUG
if let taskOverride {
return taskOverride
}
#endif
let userDefaults = userDefaults ?? self.applicationUserDefaults
if let raw = userDefaults.string(forKey: self.userDefaultsKey),
let mode = ClaudeOAuthKeychainPromptMode(rawValue: raw)
{
return mode
}
return .onlyOnUserAction
}
public static func isApplicable(
readStrategy: ClaudeOAuthKeychainReadStrategy = ClaudeOAuthKeychainReadStrategyPreference.current()) -> Bool
{
readStrategy == .securityFramework
}
public static func effectiveMode(
userDefaults: UserDefaults? = nil,
readStrategy: ClaudeOAuthKeychainReadStrategy = ClaudeOAuthKeychainReadStrategyPreference.current())
-> ClaudeOAuthKeychainPromptMode
{
guard self.isApplicable(readStrategy: readStrategy) else {
return .always
}
return self.storedMode(userDefaults: userDefaults)
}
public static func securityFrameworkFallbackMode(
userDefaults: UserDefaults? = nil,
readStrategy: ClaudeOAuthKeychainReadStrategy = ClaudeOAuthKeychainReadStrategyPreference.current())
-> ClaudeOAuthKeychainPromptMode
{
if readStrategy == .securityCLIExperimental {
return self.storedMode(userDefaults: userDefaults)
}
return self.effectiveMode(userDefaults: userDefaults, readStrategy: readStrategy)
}
static var applicationUserDefaults: UserDefaults {
#if DEBUG
if let taskApplicationUserDefaultsOverride {
return taskApplicationUserDefaultsOverride.value
}
#endif
return UserDefaults(suiteName: self.applicationDefaultsDomain) ?? .standard
}
static func resolveApplicationDefaultsDomain(
bundleIdentifier: String?,
bundleURL: URL?,
executableURL: URL?,
invocationURL: URL?,
bundleIdentifierForApp: (URL) -> String? = { Bundle(url: $0)?.bundleIdentifier }) -> String
{
if let domain = self.defaultsDomain(forBundleIdentifier: bundleIdentifier) {
return domain
}
let candidates = [bundleURL, executableURL, invocationURL].compactMap(\.self)
var visitedPaths = Set<String>()
for candidate in candidates {
var current = candidate.standardizedFileURL.resolvingSymlinksInPath()
let ancestorCount = current.pathComponents.count
for _ in 0..<ancestorCount {
if current.pathExtension == "app",
visitedPaths.insert(current.path).inserted,
let domain = self.defaultsDomain(forBundleIdentifier: bundleIdentifierForApp(current))
{
return domain
}
current.deleteLastPathComponent()
}
}
return self.releaseApplicationDefaultsDomain
}
private static func defaultsDomain(forBundleIdentifier bundleIdentifier: String?) -> String? {
guard let bundleIdentifier else { return nil }
// Check debug first because its identifier is a child of the release identifier.
if bundleIdentifier == self.debugApplicationDefaultsDomain
|| bundleIdentifier.hasPrefix("\(self.debugApplicationDefaultsDomain).")
{
return self.debugApplicationDefaultsDomain
}
if bundleIdentifier == self.releaseApplicationDefaultsDomain
|| bundleIdentifier.hasPrefix("\(self.releaseApplicationDefaultsDomain).")
{
return self.releaseApplicationDefaultsDomain
}
return nil
}
#if DEBUG
public static func withTaskOverrideForTesting<T>(
_ mode: ClaudeOAuthKeychainPromptMode?,
operation: () throws -> T) rethrows -> T
{
try self.$taskOverride.withValue(mode) {
try operation()
}
}
public static func withTaskOverrideForTesting<T>(
_ mode: ClaudeOAuthKeychainPromptMode?,
operation: () async throws -> T) async rethrows -> T
{
try await self.$taskOverride.withValue(mode) {
try await operation()
}
}
public static var currentTaskOverrideForTesting: ClaudeOAuthKeychainPromptMode? {
self.taskOverride
}
static func withApplicationUserDefaultsOverrideForTesting<T>(
_ userDefaults: UserDefaults?,
operation: () throws -> T) rethrows -> T
{
try self.$taskApplicationUserDefaultsOverride.withValue(userDefaults.map(UserDefaultsBox.init)) {
try operation()
}
}
static func withApplicationUserDefaultsOverrideForTesting<T>(
_ userDefaults: UserDefaults?,
operation: () async throws -> T) async rethrows -> T
{
try await self.$taskApplicationUserDefaultsOverride.withValue(userDefaults.map(UserDefaultsBox.init)) {
try await operation()
}
}
#endif
}