fix(codex): durable OAuth refresh lifecycle

Add shared OAuth credential lifecycle manager with provider-aware refresh
decisions. Implement CodexExecutor.refreshCredentials so 401/403 retry
refresh works for Codex, track lastRefreshAt and refresh before the
upstream stale-token window, preserve omitted idToken, and add
per-connection single-flight refresh to avoid refresh-token rotation races.

Merged from PR #1664.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Le
2026-06-06 11:04:36 +07:00
committed by decolua
co-authored by Cursor
parent 38b73bfc6b
commit c233c7c8fc
15 changed files with 484 additions and 140 deletions
+2 -3
View File
@@ -1,4 +1,5 @@
import { HTTP_STATUS, RETRY_CONFIG, DEFAULT_RETRY_CONFIG, resolveRetryEntry, FETCH_CONNECT_TIMEOUT_MS } from "../config/runtimeConfig.js";
import { shouldRefreshCredentials } from "../services/oauthCredentialManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { dbg } from "../utils/debugLog.js";
@@ -87,9 +88,7 @@ export class BaseExecutor {
}
needsRefresh(credentials) {
if (!credentials.expiresAt) return false;
const expiresAtMs = new Date(credentials.expiresAt).getTime();
return expiresAtMs - Date.now() < 5 * 60 * 1000;
return shouldRefreshCredentials(this.provider, credentials);
}
parseError(response, bodyText) {
+13
View File
@@ -2,6 +2,10 @@ import { createHash } from "crypto";
import { BaseExecutor } from "./base.js";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
import { PROVIDERS } from "../config/providers.js";
import {
refreshProviderCredentials,
shouldRefreshCredentials,
} from "../services/oauthCredentialManager.js";
import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js";
import { fetchImageAsBase64 } from "../translator/helpers/imageHelper.js";
import { getModelUpstreamId } from "../config/providerModels.js";
@@ -212,6 +216,15 @@ export class CodexExecutor extends BaseExecutor {
return this._isCompact ? `${base}/compact` : base;
}
async refreshCredentials(credentials, log) {
if (!credentials?.refreshToken) return null;
return refreshProviderCredentials("codex", credentials, log);
}
needsRefresh(credentials) {
return shouldRefreshCredentials("codex", credentials);
}
/**
* Prefetch remote image URLs and inline them as base64 data URIs.
* Runs before execute() because Codex backend cannot fetch remote images.