189 lines · 6.3 KB · Swift
RawDownload
import Foundation #if os(macOS)import Security#endif public struct ClaudeOAuthCredentials: Sendable {    public let accessToken: String    public let refreshToken: String?    public let expiresAt: Date?    public let scopes: [String]    public let rateLimitTier: String?    public let subscriptionType: String?     public init(        accessToken: String,        refreshToken: String?,        expiresAt: Date?,        scopes: [String],        rateLimitTier: String?,        subscriptionType: String? = nil)    {        self.accessToken = accessToken        self.refreshToken = refreshToken        self.expiresAt = expiresAt        self.scopes = scopes        self.rateLimitTier = rateLimitTier        self.subscriptionType = subscriptionType    }     public var isExpired: Bool {        guard let expiresAt else { return true }        return Date() >= expiresAt    }     public var expiresIn: TimeInterval? {        guard let expiresAt else { return nil }        return expiresAt.timeIntervalSinceNow    }     public static func parse(data: Data) throws -> ClaudeOAuthCredentials {        let decoder = JSONDecoder()        guard let root = try? decoder.decode(Root.self, from: data) else {            throw ClaudeOAuthCredentialsError.decodeFailed        }        guard let oauth = root.claudeAiOauth else {            throw ClaudeOAuthCredentialsError.missingOAuth        }        let accessToken = oauth.accessToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""        guard !accessToken.isEmpty else {            throw ClaudeOAuthCredentialsError.missingAccessToken        }        let expiresAt = oauth.expiresAt.map { millis in            Date(timeIntervalSince1970: millis / 1000.0)        }        return ClaudeOAuthCredentials(            accessToken: accessToken,            refreshToken: oauth.refreshToken,            expiresAt: expiresAt,            scopes: oauth.scopes ?? [],            rateLimitTier: oauth.rateLimitTier,            subscriptionType: oauth.subscriptionType)    }     private struct Root: Decodable {        let claudeAiOauth: OAuth?    }     private struct OAuth: Decodable {        let accessToken: String?        let refreshToken: String?        let expiresAt: Double?        let scopes: [String]?        let rateLimitTier: String?        let subscriptionType: String?         enum CodingKeys: String, CodingKey {            case accessToken            case refreshToken            case expiresAt            case scopes            case rateLimitTier            case subscriptionType        }    }} extension ClaudeOAuthCredentials {    func diagnosticsMetadata(now: Date = Date()) -> [String: String] {        let hasRefreshToken = !(self.refreshToken?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)        let hasUserProfileScope = self.scopes.contains("user:profile")         var metadata: [String: String] = [            "hasRefreshToken": "\(hasRefreshToken)",            "scopesCount": "\(self.scopes.count)",            "hasUserProfileScope": "\(hasUserProfileScope)",        ]         if let expiresAt = self.expiresAt {            let expiresAtMs = Int(expiresAt.timeIntervalSince1970 * 1000.0)            let expiresInSec = Int(expiresAt.timeIntervalSince(now).rounded())            metadata["expiresAtMs"] = "\(expiresAtMs)"            metadata["expiresInSec"] = "\(expiresInSec)"            metadata["isExpired"] = "\(now >= expiresAt)"        } else {            metadata["expiresAtMs"] = "nil"            metadata["expiresInSec"] = "nil"            metadata["isExpired"] = "true"        }         return metadata    }} public enum ClaudeOAuthCredentialOwner: String, Codable, Sendable {    case claudeCLI    case codexbar    case environment} public enum ClaudeOAuthCredentialSource: String, Sendable {    case environment    case memoryCache    case cacheKeychain    case credentialsFile    case claudeKeychain} public struct ClaudeOAuthCredentialRecord: Sendable {    public let credentials: ClaudeOAuthCredentials    public let owner: ClaudeOAuthCredentialOwner    public let source: ClaudeOAuthCredentialSource     public init(        credentials: ClaudeOAuthCredentials,        owner: ClaudeOAuthCredentialOwner,        source: ClaudeOAuthCredentialSource)    {        self.credentials = credentials        self.owner = owner        self.source = source    }} public enum ClaudeOAuthCredentialsError: LocalizedError, Sendable {    case decodeFailed    case missingOAuth    case missingAccessToken    case notFound    case keychainError(Int)    case readFailed(String)    case refreshFailed(String)    case noRefreshToken    case refreshDelegatedToClaudeCLI     public var errorDescription: String? {        switch self {        case .decodeFailed:            return "Claude OAuth credentials are invalid."        case .missingOAuth:            return "Claude OAuth credentials missing. Run `claude` to authenticate."        case .missingAccessToken:            return "Claude OAuth access token missing. Run `claude` to authenticate."        case .notFound:            return "Claude OAuth credentials not found. Run `claude` to authenticate."        case let .keychainError(status):            #if os(macOS)            if status == Int(errSecUserCanceled)                || status == Int(errSecAuthFailed)                || status == Int(errSecInteractionNotAllowed)                || status == Int(errSecNoAccessForItem)            {                return "Claude Keychain access was denied. CodexBar will back off in the background until you retry "                    + "via a user action (menu open / manual refresh). "                    + "Switch Claude Usage source to Web/CLI, or allow access in Keychain Access."            }            #endif            return "Claude OAuth keychain error: \(status)"        case let .readFailed(message):            return "Claude OAuth credentials read failed: \(message)"        case let .refreshFailed(message):            return "Claude OAuth token refresh failed: \(message)"        case .noRefreshToken:            return "Claude OAuth refresh token missing. Run `claude` to authenticate."        case .refreshDelegatedToClaudeCLI:            return "Claude OAuth refresh is delegated to Claude CLI."        }    }}