{"record":{"id":"8c90293206cf6d84","repo":"coleam00/Archon","slug":"openai-token-operation-response-missing-refresh","errorCode":null,"errorMessage":"OpenAI token ${operation} response missing refresh_token.","messagePattern":"OpenAI token (.+?) response missing refresh_token\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/credentials/openai-oauth.ts","lineNumber":237,"sourceCode":" * Map a token response onto the stored credential blob. Fails loud on a\n * missing `id_token` at exchange time (the whole point of owning this flow);\n * on refresh, a response that omits `id_token`/`refresh_token` PRESERVES the\n * previous values instead of degrading the blob.\n */\nfunction credentialsFromTokenResponse(\n  json: OpenAiTokenResponse,\n  operation: 'exchange' | 'refresh',\n  previous?: OAuthCredentials\n): OpenAiOAuthCredentials {\n  const access = typeof json.access_token === 'string' ? json.access_token : '';\n  const expiresIn = typeof json.expires_in === 'number' ? json.expires_in : NaN;\n  if (!access || !Number.isFinite(expiresIn)) {\n    throw new Error(`OpenAI token ${operation} response missing access_token/expires_in.`);\n  }\n  const prevRefresh = typeof previous?.refresh === 'string' ? previous.refresh : '';\n  const refresh = typeof json.refresh_token === 'string' ? json.refresh_token : prevRefresh;\n  if (!refresh) {\n    throw new Error(`OpenAI token ${operation} response missing refresh_token.`);\n  }\n  const prevIdToken = typeof previous?.id_token === 'string' ? previous.id_token : '';\n  const idToken = typeof json.id_token === 'string' && json.id_token ? json.id_token : prevIdToken;\n  if (!idToken) {\n    // Fail loud: an id_token-less credential reproduces the exact #1924\n    // breakage (\"invalid ID token format\" in the Codex CLI) — never store one.\n    throw new Error(\n      `OpenAI token ${operation} response did not include an id_token (required by the Codex CLI).`\n    );\n  }\n  const prevAccountId = typeof previous?.accountId === 'string' ? previous.accountId : '';\n  const accountId = accountIdFromAccessToken(access) ?? prevAccountId;\n  if (!accountId) {\n    throw new Error('Failed to extract the ChatGPT account id from the OpenAI access token.');\n  }\n  return {\n    // Preserve any extra fields a future token response taught us to keep.\n    ...(previous ?? {}),","sourceCodeStart":219,"sourceCodeEnd":255,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/core/src/credentials/openai-oauth.ts#L219-L255","documentation":"credentialsFromTokenResponse requires a refresh token: it takes json.refresh_token if present, otherwise falls back to previous.refresh. If neither the new response nor the prior credential supplies a refresh token, it throws. Without a refresh token the stored credential can never be renewed and would dead-end at expiry.","triggerScenarios":"An exchange or refresh response omits refresh_token AND no previous credential (or a previous credential with a non-string refresh) was supplied to credentialsFromTokenResponse.","commonSituations":"OpenAI rotates refresh tokens and the refresh response legitimately omits refresh_token while the caller dropped the previous credential; a misconfigured client (no offline_access scope) never receives refresh tokens; code paths that construct credentials from a bare access token.","solutions":["Preserve and pass the existing credential as `previous` so its refresh token is carried forward on rotation.","Retry the initial OAuth login and ensure the request asks for offline access / refresh-token scope (offline_access).","Check you are not constructing OAuthCredentials manually from just an access_token — go through the exchange flow.","Capture the full token response; some providers send refresh_token only on first exchange, so never discard it."],"exampleFix":"// before\nconst creds = await refreshOpenAiOAuthCredentials(current) // throws later if response lacks refresh_token\n// after: keep prior credential so fallback refresh survives\nconst json = await postTokenRequest(...);\nconst creds = credentialsFromTokenResponse(json, 'refresh', currentCredential); // pass previous!","handlingStrategy":"try-catch","validationCode":"if (typeof json.refresh_token !== 'string' && !(previous && typeof previous.refresh === 'string')) {\n  throw new Error('Neither response nor previous credential carries a refresh_token; restart OAuth login');\n}","typeGuard":"function hasRefreshToken(c: { refresh?: unknown }): c is { refresh: string } {\n  return typeof c.refresh === 'string' && c.refresh.length > 0;\n}","tryCatchPattern":"try {\n  return await refreshOpenAiOAuthCredentials(creds);\n} catch (e) {\n  if (e.message.includes('missing refresh_token')) {\n    return await runFullOpenAiLogin(); // only recovery: fresh login\n  }\n  throw e;\n}","preventionTips":["Always pass the previous credential to credentialsFromTokenResponse so refresh tokens survive rotation.","Persist the refresh token immediately on first exchange; OpenAI may not resend it on refresh.","Request offline access/refresh scope in the OAuth login so a refresh token is issued at all.","Never build OAuthCredentials by hand from an access token alone."],"tags":["oauth","openai","refresh-token","auth"],"backgroundTag":"oauth-refresh-token-missing","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}