summary: "Codex OAuth resolver: tokens, refresh, usage endpoint, and fetch strategy wiring." read_when:
- Adding or modifying Codex OAuth usage fetching
- Debugging auth.json parsing or token refresh behavior
- Adjusting Codex provider source selection
Codex OAuth Resolver Implementation Plan
Replicate Codex's direct OAuth token usage in CodexBar instead of calling the CLI.
Background
Currently, CodexBar fetches Codex usage by:
- Running
codexCLI in PTY mode - Sending
/statuscommand - Parsing the text output
This is slow and unreliable. The goal is to directly read Codex's OAuth tokens and call the same API endpoints that Codex uses internally.
Codex OAuth Architecture (from source analysis)
Token Storage
Location: ~/.codex/auth.json
{
"OPENAI_API_KEY": null,
"tokens": {
"id_token": "eyJ...",
"access_token": "eyJ...",
"refresh_token": "...",
"account_id": "account-..."
},
"last_refresh": "2025-12-28T12:34:56Z"
}
Source: codex-rs/core/src/auth/storage.rs
Token Refresh
Endpoint: POST https://auth.openai.com/oauth/token
Request:
{
"client_id": "app_EMoamEEZ73f0CkXaXp7hrann",
"grant_type": "refresh_token",
"refresh_token": "<refresh_token>",
"scope": "openid profile email"
}
Response:
{
"id_token": "eyJ...",
"access_token": "eyJ...",
"refresh_token": "..."
}
Refresh Interval: 8 days (from TOKEN_REFRESH_INTERVAL constant)
Source: codex-rs/core/src/auth.rs:504-545
Usage API
Endpoint: GET {chatgpt_base_url}/wham/usage (default: https://chatgpt.com/backend-api/wham/usage)
If chatgpt_base_url does not include /backend-api, Codex falls back to
{base_url}/api/codex/usage (see PathStyle in backend-client/src/client.rs).
Headers:
Authorization: Bearer <access_token>
ChatGPT-Account-Id: <account_id>
User-Agent: codex-cli
Quick checks
- Command:
cat ~/.codex/auth.json - Command:
curl -H "Authorization: Bearer <access_token>" -H "ChatGPT-Account-Id: <account_id>" -H "User-Agent: codex-cli" https://chatgpt.com/backend-api/wham/usage - Command:
CodexBarCLI usage --provider codex --source oauth --json --pretty
Response:
{
"plan_type": "pro",
"rate_limit": {
"primary_window": {
"used_percent": 15,
"reset_at": 1735401600,
"limit_window_seconds": 18000
},
"secondary_window": {
"used_percent": 5,
"reset_at": 1735920000,
"limit_window_seconds": 604800
}
},
"credits": {
"has_credits": true,
"unlimited": false,
"balance": 150.0
}
}
Source: codex-rs/backend-client/src/client.rs:161-170
Implementation
Files to Create
| File | Location | Purpose |
|---|---|---|
CodexOAuthCredentials.swift |
Sources/CodexBarCore/Providers/Codex/CodexOAuth/ |
Token storage model + loader |
CodexOAuthUsageFetcher.swift |
Sources/CodexBarCore/Providers/Codex/CodexOAuth/ |
API client for usage endpoint |
CodexTokenRefresher.swift |
Sources/CodexBarCore/Providers/Codex/CodexOAuth/ |
Token refresh logic |
Files to Modify
| File | Changes |
|---|---|
CodexProviderDescriptor.swift |
Add CodexOAuthFetchStrategy, update resolveStrategies() |
Step 1: CodexOAuthCredentials.swift
import Foundation
public struct CodexOAuthCredentials: Sendable {
public let accessToken: String
public let refreshToken: String
public let idToken: String?
public let accountId: String?
public let lastRefresh: Date?
public var needsRefresh: Bool {
guard let last = lastRefresh else { return true }
let eightDays: TimeInterval = 8 * 24 * 3600
return Date().timeIntervalSince(last) > eightDays
}
}
public enum CodexOAuthCredentialsError: LocalizedError {
case notFound
case decodeFailed(String)
case missingTokens
public var errorDescription: String? {
switch self {
case .notFound:
"Codex auth.json not found. Run `codex` to log in."
case .decodeFailed(let msg):
"Failed to decode Codex credentials: \(msg)"
case .missingTokens:
"Codex auth.json exists but contains no tokens."
}
}
}
public enum CodexOAuthCredentialsStore {
private static var authFilePath: URL {
let home = FileManager.default.homeDirectoryForCurrentUser
// Respect CODEX_HOME if set
if let codexHome = ProcessInfo.processInfo.environment["CODEX_HOME"],
!codexHome.isEmpty {
return URL(fileURLWithPath: codexHome).appendingPathComponent("auth.json")
}
return home.appendingPathComponent(".codex/auth.json")
}
public static func load() throws -> CodexOAuthCredentials {
let url = authFilePath
guard FileManager.default.fileExists(atPath: url.path) else {
throw CodexOAuthCredentialsError.notFound
}
let data = try Data(contentsOf: url)
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw CodexOAuthCredentialsError.decodeFailed("Invalid JSON")
}
// Check for API key auth (no tokens needed for refresh)
if let apiKey = json["OPENAI_API_KEY"] as? String, !apiKey.isEmpty {
return CodexOAuthCredentials(
accessToken: apiKey,
refreshToken: "",
idToken: nil,
accountId: nil,
lastRefresh: nil)
}
guard let tokens = json["tokens"] as? [String: Any],
let accessToken = tokens["access_token"] as? String,
let refreshToken = tokens["refresh_token"] as? String else {
throw CodexOAuthCredentialsError.missingTokens
}
let idToken = tokens["id_token"] as? String
let accountId = tokens["account_id"] as? String
let lastRefresh: Date? = {
guard let str = json["last_refresh"] as? String else { return nil }
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter.date(from: str) ?? ISO8601DateFormatter().date(from: str)
}()
return CodexOAuthCredentials(
accessToken: accessToken,
refreshToken: refreshToken,
idToken: idToken,
accountId: accountId,
lastRefresh: lastRefresh)
}
public static func save(_ credentials: CodexOAuthCredentials) throws {
let url = authFilePath
// Read existing file to preserve structure
var json: [String: Any] = [:]
if let data = try? Data(contentsOf: url),
let existing = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
json = existing
}
var tokens: [String: Any] = [
"access_token": credentials.accessToken,
"refresh_token": credentials.refreshToken
]
if let idToken = credentials.idToken {
tokens["id_token"] = idToken
}
if let accountId = credentials.accountId {
tokens["account_id"] = accountId
}
json["tokens"] = tokens
json["last_refresh"] = ISO8601DateFormatter().string(from: Date())
let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])
try data.write(to: url, options: .atomic)
}
}
Step 2: CodexOAuthUsageFetcher.swift
import Foundation
public struct CodexUsageResponse: Decodable, Sendable {
public let planType: PlanType
public let rateLimit: RateLimitDetails?
public let credits: CreditDetails?
enum CodingKeys: String, CodingKey {
case planType = "plan_type"
case rateLimit = "rate_limit"
case credits
}
public enum PlanType: String, Decodable, Sendable {
case guest, free, go, plus, pro
case freeWorkspace = "free_workspace"
case team, business, education, quorum, k12, enterprise, edu
}
public struct RateLimitDetails: Decodable, Sendable {
public let primaryWindow: WindowSnapshot?
public let secondaryWindow: WindowSnapshot?
enum CodingKeys: String, CodingKey {
case primaryWindow = "primary_window"
case secondaryWindow = "secondary_window"
}
}
public struct WindowSnapshot: Decodable, Sendable {
public let usedPercent: Int
public let resetAt: Int
public let limitWindowSeconds: Int
enum CodingKeys: String, CodingKey {
case usedPercent = "used_percent"
case resetAt = "reset_at"
case limitWindowSeconds = "limit_window_seconds"
}
}
public struct CreditDetails: Decodable, Sendable {
public let hasCredits: Bool
public let unlimited: Bool
public let balance: Double?
enum CodingKeys: String, CodingKey {
case hasCredits = "has_credits"
case unlimited
case balance
}
}
}
public enum CodexOAuthFetchError: LocalizedError, Sendable {
case unauthorized
case invalidResponse
case serverError(Int, String?)
case networkError(Error)
public var errorDescription: String? {
switch self {
case .unauthorized:
"Codex OAuth token expired or invalid. Run `codex` to re-authenticate."
case .invalidResponse:
"Invalid response from Codex usage API."
case .serverError(let code, let msg):
"Codex API error \(code): \(msg ?? "unknown")"
case .networkError(let error):
"Network error: \(error.localizedDescription)"
}
}
}
public enum CodexOAuthUsageFetcher {
private static let defaultChatGPTBaseURL = "https://chatgpt.com/backend-api/"
private static let chatGPTUsagePath = "/wham/usage"
private static let codexUsagePath = "/api/codex/usage"
public static func fetchUsage(
accessToken: String,
accountId: String?
) async throws -> CodexUsageResponse {
var request = URLRequest(url: resolveUsageURL())
request.httpMethod = "GET"
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.setValue("CodexBar", forHTTPHeaderField: "User-Agent")
request.setValue("application/json", forHTTPHeaderField: "Accept")
if let accountId {
request.setValue(accountId, forHTTPHeaderField: "ChatGPT-Account-Id")
}
let (data, response): (Data, URLResponse)
do {
(data, response) = try await URLSession.shared.data(for: request)
} catch {
throw CodexOAuthFetchError.networkError(error)
}
guard let http = response as? HTTPURLResponse else {
throw CodexOAuthFetchError.invalidResponse
}
switch http.statusCode {
case 200...299:
do {
return try JSONDecoder().decode(CodexUsageResponse.self, from: data)
} catch {
throw CodexOAuthFetchError.invalidResponse
}
case 401, 403:
throw CodexOAuthFetchError.unauthorized
default:
let body = String(data: data, encoding: .utf8)
throw CodexOAuthFetchError.serverError(http.statusCode, body)
}
}
}
Step 3: CodexTokenRefresher.swift
import Foundation
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 {
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 .networkError(let error):
"Network error during token refresh: \(error.localizedDescription)"
case .invalidResponse(let msg):
"Invalid refresh response: \(msg)"
}
}
}
public static func refresh(_ credentials: CodexOAuthCredentials) async throws -> CodexOAuthCredentials {
guard !credentials.refreshToken.isEmpty else {
// API key auth - no refresh needed
return credentials
}
var request = URLRequest(url: refreshEndpoint)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: String] = [
"client_id": clientID,
"grant_type": "refresh_token",
"refresh_token": credentials.refreshToken,
"scope": "openid profile email"
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response): (Data, URLResponse)
do {
(data, response) = try await URLSession.shared.data(for: request)
} catch {
throw RefreshError.networkError(error)
}
guard let http = response as? HTTPURLResponse else {
throw RefreshError.invalidResponse("No HTTP response")
}
if http.statusCode == 401 {
// Parse error code to classify failure
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let errorCode = (json["error"] as? [String: Any])?["code"] as? String
?? json["error"] as? String
?? json["code"] as? String {
switch errorCode.lowercased() {
case "refresh_token_expired": throw RefreshError.expired
case "refresh_token_reused": throw RefreshError.reused
case "refresh_token_invalidated": throw RefreshError.revoked
default: throw RefreshError.expired
}
}
throw RefreshError.expired
}
guard http.statusCode == 200 else {
throw RefreshError.invalidResponse("Status \(http.statusCode)")
}
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())
}
}
Step 4: Update CodexProviderDescriptor.swift
Add OAuth to sourceModes and create new strategy:
// In makeDescriptor(), update fetchPlan:
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .oauth, .web, .cli], // Add .oauth
pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)),
// Update resolveStrategies:
private static func resolveStrategies(context: ProviderFetchContext) async -> [any ProviderFetchStrategy] {
let oauth = CodexOAuthFetchStrategy()
Lines 1–500 of 635. Load more
--- summary: "Codex OAuth resolver: tokens, refresh, usage endpoint, and fetch strategy wiring." read_when: - Adding or modifying Codex OAuth usage fetching - Debugging auth.json parsing or token refresh behavior - Adjusting Codex provider source selection --- # Codex OAuth Resolver Implementation Plan > Replicate Codex's direct OAuth token usage in CodexBar instead of calling the CLI. ## Background Currently, CodexBar fetches Codex usage by: 1. Running `codex` CLI in PTY mode 2. Sending `/status` command 3. Parsing the text output This is slow and unreliable. The goal is to directly read Codex's OAuth tokens and call the same API endpoints that Codex uses internally. --- ## Codex OAuth Architecture (from source analysis) ### Token Storage **Location:** `~/.codex/auth.json` ```json { "OPENAI_API_KEY": null, "tokens": { "id_token": "eyJ...", "access_token": "eyJ...", "refresh_token": "...", "account_id": "account-..." }, "last_refresh": "2025-12-28T12:34:56Z" } ``` **Source:** `codex-rs/core/src/auth/storage.rs` ### Token Refresh **Endpoint:** `POST https://auth.openai.com/oauth/token` **Request:** ```json { "client_id": "app_EMoamEEZ73f0CkXaXp7hrann", "grant_type": "refresh_token", "refresh_token": "<refresh_token>", "scope": "openid profile email" } ``` **Response:** ```json { "id_token": "eyJ...", "access_token": "eyJ...", "refresh_token": "..." } ``` **Refresh Interval:** 8 days (from `TOKEN_REFRESH_INTERVAL` constant) **Source:** `codex-rs/core/src/auth.rs:504-545` ### Usage API **Endpoint:** `GET {chatgpt_base_url}/wham/usage` (default: `https://chatgpt.com/backend-api/wham/usage`) If `chatgpt_base_url` does not include `/backend-api`, Codex falls back to `{base_url}/api/codex/usage` (see `PathStyle` in `backend-client/src/client.rs`). **Headers:** ``` Authorization: Bearer <access_token> ChatGPT-Account-Id: <account_id> User-Agent: codex-cli ``` **Quick checks** - Command: `cat ~/.codex/auth.json` - Command: `curl -H "Authorization: Bearer <access_token>" -H "ChatGPT-Account-Id: <account_id>" -H "User-Agent: codex-cli" https://chatgpt.com/backend-api/wham/usage` - Command: `CodexBarCLI usage --provider codex --source oauth --json --pretty` **Response:** ```json { "plan_type": "pro", "rate_limit": { "primary_window": { "used_percent": 15, "reset_at": 1735401600, "limit_window_seconds": 18000 }, "secondary_window": { "used_percent": 5, "reset_at": 1735920000, "limit_window_seconds": 604800 } }, "credits": { "has_credits": true, "unlimited": false, "balance": 150.0 } } ``` **Source:** `codex-rs/backend-client/src/client.rs:161-170` --- ## Implementation ### Files to Create | File | Location | Purpose | |------|----------|---------| | `CodexOAuthCredentials.swift` | `Sources/CodexBarCore/Providers/Codex/CodexOAuth/` | Token storage model + loader | | `CodexOAuthUsageFetcher.swift` | `Sources/CodexBarCore/Providers/Codex/CodexOAuth/` | API client for usage endpoint | | `CodexTokenRefresher.swift` | `Sources/CodexBarCore/Providers/Codex/CodexOAuth/` | Token refresh logic | ### Files to Modify | File | Changes | |------|---------| | `CodexProviderDescriptor.swift` | Add `CodexOAuthFetchStrategy`, update `resolveStrategies()` | --- ### Step 1: CodexOAuthCredentials.swift ```swift import Foundation public struct CodexOAuthCredentials: Sendable { public let accessToken: String public let refreshToken: String public let idToken: String? public let accountId: String? public let lastRefresh: Date? public var needsRefresh: Bool { guard let last = lastRefresh else { return true } let eightDays: TimeInterval = 8 * 24 * 3600 return Date().timeIntervalSince(last) > eightDays } } public enum CodexOAuthCredentialsError: LocalizedError { case notFound case decodeFailed(String) case missingTokens public var errorDescription: String? { switch self { case .notFound: "Codex auth.json not found. Run `codex` to log in." case .decodeFailed(let msg): "Failed to decode Codex credentials: \(msg)" case .missingTokens: "Codex auth.json exists but contains no tokens." } } } public enum CodexOAuthCredentialsStore { private static var authFilePath: URL { let home = FileManager.default.homeDirectoryForCurrentUser // Respect CODEX_HOME if set if let codexHome = ProcessInfo.processInfo.environment["CODEX_HOME"], !codexHome.isEmpty { return URL(fileURLWithPath: codexHome).appendingPathComponent("auth.json") } return home.appendingPathComponent(".codex/auth.json") } public static func load() throws -> CodexOAuthCredentials { let url = authFilePath guard FileManager.default.fileExists(atPath: url.path) else { throw CodexOAuthCredentialsError.notFound } let data = try Data(contentsOf: url) guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw CodexOAuthCredentialsError.decodeFailed("Invalid JSON") } // Check for API key auth (no tokens needed for refresh) if let apiKey = json["OPENAI_API_KEY"] as? String, !apiKey.isEmpty { return CodexOAuthCredentials( accessToken: apiKey, refreshToken: "", idToken: nil, accountId: nil, lastRefresh: nil) } guard let tokens = json["tokens"] as? [String: Any], let accessToken = tokens["access_token"] as? String, let refreshToken = tokens["refresh_token"] as? String else { throw CodexOAuthCredentialsError.missingTokens } let idToken = tokens["id_token"] as? String let accountId = tokens["account_id"] as? String let lastRefresh: Date? = { guard let str = json["last_refresh"] as? String else { return nil } let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter.date(from: str) ?? ISO8601DateFormatter().date(from: str) }() return CodexOAuthCredentials( accessToken: accessToken, refreshToken: refreshToken, idToken: idToken, accountId: accountId, lastRefresh: lastRefresh) } public static func save(_ credentials: CodexOAuthCredentials) throws { let url = authFilePath // Read existing file to preserve structure var json: [String: Any] = [:] if let data = try? Data(contentsOf: url), let existing = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { json = existing } var tokens: [String: Any] = [ "access_token": credentials.accessToken, "refresh_token": credentials.refreshToken ] if let idToken = credentials.idToken { tokens["id_token"] = idToken } if let accountId = credentials.accountId { tokens["account_id"] = accountId } json["tokens"] = tokens json["last_refresh"] = ISO8601DateFormatter().string(from: Date()) let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys]) try data.write(to: url, options: .atomic) } } ``` --- ### Step 2: CodexOAuthUsageFetcher.swift ```swift import Foundation public struct CodexUsageResponse: Decodable, Sendable { public let planType: PlanType public let rateLimit: RateLimitDetails? public let credits: CreditDetails? enum CodingKeys: String, CodingKey { case planType = "plan_type" case rateLimit = "rate_limit" case credits } public enum PlanType: String, Decodable, Sendable { case guest, free, go, plus, pro case freeWorkspace = "free_workspace" case team, business, education, quorum, k12, enterprise, edu } public struct RateLimitDetails: Decodable, Sendable { public let primaryWindow: WindowSnapshot? public let secondaryWindow: WindowSnapshot? enum CodingKeys: String, CodingKey { case primaryWindow = "primary_window" case secondaryWindow = "secondary_window" } } public struct WindowSnapshot: Decodable, Sendable { public let usedPercent: Int public let resetAt: Int public let limitWindowSeconds: Int enum CodingKeys: String, CodingKey { case usedPercent = "used_percent" case resetAt = "reset_at" case limitWindowSeconds = "limit_window_seconds" } } public struct CreditDetails: Decodable, Sendable { public let hasCredits: Bool public let unlimited: Bool public let balance: Double? enum CodingKeys: String, CodingKey { case hasCredits = "has_credits" case unlimited case balance } } } public enum CodexOAuthFetchError: LocalizedError, Sendable { case unauthorized case invalidResponse case serverError(Int, String?) case networkError(Error) public var errorDescription: String? { switch self { case .unauthorized: "Codex OAuth token expired or invalid. Run `codex` to re-authenticate." case .invalidResponse: "Invalid response from Codex usage API." case .serverError(let code, let msg): "Codex API error \(code): \(msg ?? "unknown")" case .networkError(let error): "Network error: \(error.localizedDescription)" } } } public enum CodexOAuthUsageFetcher { private static let defaultChatGPTBaseURL = "https://chatgpt.com/backend-api/" private static let chatGPTUsagePath = "/wham/usage" private static let codexUsagePath = "/api/codex/usage" public static func fetchUsage( accessToken: String, accountId: String? ) async throws -> CodexUsageResponse { var request = URLRequest(url: resolveUsageURL()) request.httpMethod = "GET" request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") request.setValue("CodexBar", forHTTPHeaderField: "User-Agent") request.setValue("application/json", forHTTPHeaderField: "Accept") if let accountId { request.setValue(accountId, forHTTPHeaderField: "ChatGPT-Account-Id") } let (data, response): (Data, URLResponse) do { (data, response) = try await URLSession.shared.data(for: request) } catch { throw CodexOAuthFetchError.networkError(error) } guard let http = response as? HTTPURLResponse else { throw CodexOAuthFetchError.invalidResponse } switch http.statusCode { case 200...299: do { return try JSONDecoder().decode(CodexUsageResponse.self, from: data) } catch { throw CodexOAuthFetchError.invalidResponse } case 401, 403: throw CodexOAuthFetchError.unauthorized default: let body = String(data: data, encoding: .utf8) throw CodexOAuthFetchError.serverError(http.statusCode, body) } } } ``` --- ### Step 3: CodexTokenRefresher.swift ```swift import Foundation 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 { 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 .networkError(let error): "Network error during token refresh: \(error.localizedDescription)" case .invalidResponse(let msg): "Invalid refresh response: \(msg)" } } } public static func refresh(_ credentials: CodexOAuthCredentials) async throws -> CodexOAuthCredentials { guard !credentials.refreshToken.isEmpty else { // API key auth - no refresh needed return credentials } var request = URLRequest(url: refreshEndpoint) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body: [String: String] = [ "client_id": clientID, "grant_type": "refresh_token", "refresh_token": credentials.refreshToken, "scope": "openid profile email" ] request.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, response): (Data, URLResponse) do { (data, response) = try await URLSession.shared.data(for: request) } catch { throw RefreshError.networkError(error) } guard let http = response as? HTTPURLResponse else { throw RefreshError.invalidResponse("No HTTP response") } if http.statusCode == 401 { // Parse error code to classify failure if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let errorCode = (json["error"] as? [String: Any])?["code"] as? String ?? json["error"] as? String ?? json["code"] as? String { switch errorCode.lowercased() { case "refresh_token_expired": throw RefreshError.expired case "refresh_token_reused": throw RefreshError.reused case "refresh_token_invalidated": throw RefreshError.revoked default: throw RefreshError.expired } } throw RefreshError.expired } guard http.statusCode == 200 else { throw RefreshError.invalidResponse("Status \(http.statusCode)") } 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()) } } ``` --- ### Step 4: Update CodexProviderDescriptor.swift Add OAuth to `sourceModes` and create new strategy: ```swift // In makeDescriptor(), update fetchPlan: fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .oauth, .web, .cli], // Add .oauth pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)), // Update resolveStrategies: private static func resolveStrategies(context: ProviderFetchContext) async -> [any ProviderFetchStrategy] { let oauth = CodexOAuthFetchStrategy()