{"record":{"id":"8c52d7d277aec289","repo":"coleam00/Archon","slug":"openai-token-operation-response-missing-access","errorCode":null,"errorMessage":"OpenAI token ${operation} response missing access_token/expires_in.","messagePattern":"OpenAI token (.+?) response missing access_token/expires_in\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/credentials/openai-oauth.ts","lineNumber":232,"sourceCode":"  }\n  return raw as OpenAiTokenResponse;\n}\n\n/**\n * 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) {","sourceCodeStart":214,"sourceCodeEnd":250,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/core/src/credentials/openai-oauth.ts#L214-L250","documentation":"credentialsFromTokenResponse validates the JSON returned by OpenAI's OAuth token endpoint during an authorization-code exchange or refresh. It throws when the response lacks a usable access_token (non-empty string) or expires_in (finite number), meaning OpenAI's reply did not contain the fields needed to build a credential. Throwing here prevents storing a half-formed credential that would fail later at request time.","triggerScenarios":"An OpenAI token exchange or refresh HTTP response body parses to JSON that is missing access_token or has a non-string access_token, or missing/non-numeric expires_in (e.g. expires_in: null or a string).","commonSituations":"OpenAI returns a 4xx error body (invalid_grant, expired code) that still parses as JSON; a proxy or captive portal returns HTML/JSON without token fields; OpenAI changes the token response shape; the caller accidentally passes the wrong endpoint's JSON.","solutions":["Log the full response body (redacting secrets) to see what OpenAI actually returned.","Retry the OAuth flow from scratch: the authorization code may be expired or already consumed — start a new PKCE login.","Check for a proxy/interceptor altering the token endpoint response (custom OPENAI_BASE_URL, corporate MITM proxy).","Verify you are POSTing to https://auth.openai.com/oauth/token with grant_type=authorization_code and the correct client_id.","If it persists across attempts, check OpenAI status/Auth0 tenant issues and the library's pinned client config for staleness."],"exampleFix":"// before: passing raw response without checking\nconst creds = await exchangeOpenAiAuthorizationCode(res.json());\n// after: inspect and fail on non-token responses\nconst json = await res.json();\nif (json.error) throw new Error(`Token endpoint error: ${json.error} - ${json.error_description}`);\nconst creds = await exchangeOpenAiAuthorizationCode(json);","handlingStrategy":"validation","validationCode":"const looksLikeTokenResponse = (j) =>\n  j && typeof j.access_token === 'string' && j.access_token.length > 0 &&\n  Number.isFinite(j.expires_in);\nconst json = await res.json();\nif (json?.error) throw new Error(`token endpoint: ${json.error}: ${json.error_description}`);\nif (!looksLikeTokenResponse(json)) throw new Error('unexpected token response shape: ' + JSON.stringify(Object.keys(json ?? {})));","typeGuard":"function isTokenResponse(j: unknown): j is { access_token: string; expires_in: number } {\n  const o = j as Record<string, unknown>;\n  return !!o && typeof o.access_token === 'string' && o.access_token !== '' &&\n    typeof o.expires_in === 'number' && Number.isFinite(o.expires_in);\n}","tryCatchPattern":"try {\n  const creds = await exchangeOpenAiAuthorizationCode(code, verifier);\n} catch (e) {\n  if (e.message.includes('missing access_token/expires_in')) {\n    logRawTokenResponse(lastResponseBody); // inspect, redact secrets\n    throw new Error('OpenAI token endpoint returned a non-token response; restart the OAuth login');\n  }\n  throw e;\n}","preventionTips":["Always check for `error`/`error_description` in the parsed token response before consuming it.","Log (redacted) response bodies on failure to distinguish proxy/HTML responses from real token errors.","Restart the OAuth flow after this error — codes are single-use and often expired.","Avoid custom proxies on the token endpoint that can rewrite or strip fields.","Pin and review the OpenAI client config (client_id, issuer URL) when upgrading."],"tags":["oauth","openai","http-response","auth"],"backgroundTag":"oauth-token-response-missing-fields","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}