Files
OmniRoute/open-sse/services/tokenRefresh/providers/codex.ts
Diego Rodrigues de Sa e Souza 5e96d52544 refactor(sse): extract per-provider token-refresh functions from tokenRefresh.ts (#7817)
Split the 13 export async function refresh<Provider>Token() implementations
out of open-sse/services/tokenRefresh.ts (2249 lines, frozen) into their own
co-located leaf modules under open-sse/services/tokenRefresh/providers/, with
shared OAuth-error classification (extractOAuthErrorCode/readRefreshErrorBody)
and the form-body builder moved to tokenRefresh/shared.ts. tokenRefresh.ts now
keeps only the cross-provider orchestrator (refreshAccessToken dispatcher,
getAccessToken dedup/mutex/CAS-guard layers, refreshWithRetry circuit
breaker) and re-exports every previously-public symbol so no importer needed
to change (open-sse/index.ts, executors, src/sse/services/tokenRefresh.ts,
tests). File shrinks 2249 -> 989 lines; every new leaf is well under the
800-line cap.

Pure move, zero behavior change — verified via typecheck:core, check:cycles,
eslint (0 new any), file-size/complexity/cognitive-complexity ratchets (all
under baseline), and the full token-refresh test surface (token-refresh-*,
oauth-providers-*, executor-{kiro,github,gitlab,codex,antigravity,default-base},
token-health-check*, kiro-external-idp, codebuddy-cn-provider,
windsurf-devin-executors, grok-cli-*, agy-*, xai/zed-oauth-provider,
deepseek-web-autorefresh, ghe-copilot). Updated the 8 structural (text-based)
assertions in oauth-providers-error-handling.test.ts that read function
bodies straight from tokenRefresh.ts to read from the new per-provider files
instead — same assertions, new location.

The provider-module split was originally proposed by KooshaPari in PR #7338
against a base too old to merge cleanly; redone here from scratch against the
current release/v3.8.49 tip, credit preserved via co-authorship.

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
2026-07-20 00:05:25 -03:00

115 lines
4.3 KiB
TypeScript

// @ts-nocheck
// Extracted from open-sse/services/tokenRefresh.ts — see ../shared.ts for
// provenance notes (ported idea from KooshaPari's PR #7338, redone on tip).
import { PROVIDERS, OAUTH_ENDPOINTS } from "../../../config/constants.ts";
import { runWithProxyContext } from "../../../utils/proxyFetch.ts";
import { buildFormParams } from "../shared.ts";
/**
* Specialized refresh for Codex (OpenAI) OAuth tokens.
* OpenAI uses rotating (one-time-use) refresh tokens.
* Returns { error: 'unrecoverable_refresh_error', code } when the token has already been
* consumed or is invalid, so callers can stop retrying and request re-authentication.
*/
export async function refreshCodexToken(refreshToken, log, proxyConfig: unknown = null) {
try {
const response = await runWithProxyContext(proxyConfig, () =>
fetch(OAUTH_ENDPOINTS.openai.token, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
// Body intentionally omits `scope`. RFC 6749 §6 makes scope optional on a
// refresh_token grant (the server reuses the originally-granted scope when
// absent). Including `scope` causes Auth0 (which OpenAI Codex OAuth is
// built on) to treat the request as a re-scope, which can invalidate
// sibling refresh_token families on the same client_id. Matches the
// pattern used by ndycode/codex-multi-auth, the only known tool that
// sustains multiple Codex accounts without cross-invalidation.
body: buildFormParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: PROVIDERS.codex.clientId,
}),
})
);
if (!response.ok) {
const errorText = await response.text();
// Detect unrecoverable "refresh_token_reused" or "invalid_grant" error from OpenAI
// This means the token was already consumed or has expired.
// Retrying with the same token will never succeed.
let errorCode = null;
try {
const parsed = JSON.parse(errorText);
errorCode =
parsed?.error?.code || (typeof parsed?.error === "string" ? parsed.error : null);
} catch {
// not JSON, ignore
}
if (
errorCode === "refresh_token_reused" ||
errorCode === "invalid_grant" ||
errorCode === "token_expired" ||
errorCode === "invalid_token"
) {
log?.error?.(
"TOKEN_REFRESH",
"Codex refresh token already used or invalid. Re-authentication required.",
{
status: response.status,
errorCode,
}
);
return { error: "unrecoverable_refresh_error", code: errorCode };
}
// Defense-in-depth (port from decolua/9router#1821): any 401 from OpenAI's
// OAuth token endpoint means the refresh credential itself was rejected
// (e.g. rotated away, or a payload variant whose code we do not yet
// recognize — OpenAI has shipped both `token_expired` and the bare
// "Could not validate your token" message). Retrying with the same dead
// refresh token will never succeed; surface re-auth instead of looping.
// 429 / 5xx remain transient and fall through to the retryable branch.
if (response.status === 401) {
const code = errorCode || "unauthorized";
log?.error?.(
"TOKEN_REFRESH",
"Codex OAuth token endpoint returned 401. Re-authentication required.",
{
status: response.status,
errorCode: code,
}
);
return { error: "unrecoverable_refresh_error", code };
}
log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", {
status: response.status,
error: errorText,
});
return null;
}
const tokens = await response.json();
log?.info?.("TOKEN_REFRESH", "Successfully refreshed Codex token", {
hasNewAccessToken: !!tokens.access_token,
hasNewRefreshToken: !!tokens.refresh_token,
expiresIn: tokens.expires_in,
});
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
};
} catch (error) {
log?.error?.("TOKEN_REFRESH", `Network error refreshing Codex token: ${error.message}`);
return null;
}
}