123 lines · 4.5 KB · Swift
RawDownload
import Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif public enum CodexTokenRefresher {    private static let refreshEndpoint = URL(string: "https://auth.openai.com/oauth/token")!    private static let clientID = "app_EMoamEEZ73f0CkXaXp7hrann"     public enum RefreshError: LocalizedError, Sendable {        case expired        case revoked        case reused        case networkError(Error)        case invalidResponse(String)         public var errorDescription: String? {            switch self {            case .expired:                "Refresh token expired. Please run `codex` to log in again."            case .revoked:                "Refresh token was revoked. Please run `codex` to log in again."            case .reused:                "Refresh token was already used. Please run `codex` to log in again."            case let .networkError(error):                "Network error during token refresh: \(error.localizedDescription)"            case let .invalidResponse(message):                "Invalid refresh response: \(message)"            }        }    }     public static func refresh(_ credentials: CodexOAuthCredentials) async throws -> CodexOAuthCredentials {        try await self.refresh(credentials, session: CodexAuthenticatedHTTPTransport.current)    }     static func refresh(        _ credentials: CodexOAuthCredentials,        session transport: any ProviderHTTPTransport) async throws -> CodexOAuthCredentials    {        guard !credentials.refreshToken.isEmpty else {            return credentials        }         var request = URLRequest(            url: Self.refreshEndpoint,            cachePolicy: .reloadIgnoringLocalCacheData,            timeoutInterval: 30)        request.httpMethod = "POST"        request.setValue("application/json", forHTTPHeaderField: "Content-Type")         let body: [String: String] = [            "client_id": Self.clientID,            "grant_type": "refresh_token",            "refresh_token": credentials.refreshToken,            "scope": "openid profile email",        ]        request.httpBody = try JSONSerialization.data(withJSONObject: body)         do {            let response = try await transport.response(for: request)            let data = response.data            guard response.statusCode == 200 else {                throw Self.refreshFailureError(statusCode: response.statusCode, data: data)            }             guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {                throw RefreshError.invalidResponse("Invalid JSON")            }             let newAccessToken = json["access_token"] as? String ?? credentials.accessToken            let newRefreshToken = json["refresh_token"] as? String ?? credentials.refreshToken            let newIdToken = json["id_token"] as? String ?? credentials.idToken             return CodexOAuthCredentials(                accessToken: newAccessToken,                refreshToken: newRefreshToken,                idToken: newIdToken,                accountId: credentials.accountId,                lastRefresh: Date())        } catch let error as RefreshError {            throw error        } catch {            throw RefreshError.networkError(error)        }    }     private static func extractErrorCode(from data: Data) -> String? {        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil }        if let error = json["error"] as? [String: Any], let code = error["code"] as? String { return code }        if let error = json["error"] as? String { return error }        return json["code"] as? String    }     private static func refreshFailureError(statusCode: Int, data: Data) -> RefreshError {        if let errorCode = extractErrorCode(from: data) {            switch errorCode.lowercased() {            case "refresh_token_expired":                return .expired            case "refresh_token_reused":                return .reused            case "invalid_grant", "refresh_token_invalidated":                return .revoked            default:                break            }        }         if statusCode == 401 {            return .expired        }         return .invalidResponse("Status \(statusCode)")    }} #if DEBUGextension CodexTokenRefresher {    static func _refreshFailureErrorForTesting(statusCode: Int, data: Data) -> RefreshError {        self.refreshFailureError(statusCode: statusCode, data: data)    }}#endif