{"record":{"id":"83a7695ff59d4a3d","repo":"can1357/oh-my-pi","slug":"label-missing-refresh-token","errorCode":null,"errorMessage":"${label} missing refresh_token","messagePattern":"(.+?) missing refresh_token","errorType":"validation","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/registry/oauth/xai-oauth.ts","lineNumber":342,"sourceCode":"function parseXAITokenResponse(payload: unknown, label: string, refreshTokenFallback?: string): OAuthCredentials {\n\tif (!isRecord(payload)) {\n\t\tthrow new AIError.OAuthError(`${label} was not a JSON object`, {\n\t\t\tkind: \"validation\",\n\t\t\tprovider: \"xai\",\n\t\t});\n\t}\n\tconst accessToken = typeof payload.access_token === \"string\" ? payload.access_token : \"\";\n\tconst responseRefreshToken = typeof payload.refresh_token === \"string\" ? payload.refresh_token : \"\";\n\tconst refreshToken = responseRefreshToken || refreshTokenFallback || \"\";\n\tconst expiresInSeconds = payload.expires_in;\n\tif (!accessToken) {\n\t\tthrow new AIError.OAuthError(`${label} missing access_token`, {\n\t\t\tkind: \"validation\",\n\t\t\tprovider: \"xai\",\n\t\t});\n\t}\n\tif (!refreshToken) {\n\t\tthrow new AIError.OAuthError(`${label} missing refresh_token`, {\n\t\t\tkind: \"validation\",\n\t\t\tprovider: \"xai\",\n\t\t});\n\t}\n\tif (typeof expiresInSeconds !== \"number\" || !Number.isFinite(expiresInSeconds)) {\n\t\tthrow new AIError.OAuthError(`${label} missing expires_in`, {\n\t\t\tkind: \"validation\",\n\t\t\tprovider: \"xai\",\n\t\t});\n\t}\n\treturn {\n\t\taccess: accessToken,\n\t\trefresh: refreshToken,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - ACCESS_TOKEN_CLIENT_SKEW_MS,\n\t};\n}\n\nasync function requestXAIDeviceAuthorization(","sourceCodeStart":324,"sourceCodeEnd":360,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/xai-oauth.ts#L324-L360","documentation":"Thrown by parseXAITokenResponse when neither the response body nor the fallback (e.g. the refresh token supplied by the caller during a refresh exchange) yields a non-empty refresh token. The library persists a refresh token so sessions can be renewed; a response without one would silently break future refreshes, so it is rejected. Note the fallback: during refresh, a response that omits refresh_token is fine if the caller passed the old one.","triggerScenarios":"pollXAIDeviceToken completes with a 200 body whose refresh_token is absent/empty AND no refreshTokenFallback was provided; or the credentials refresh path gets a body with no refresh_token while also having no stored refresh token to fall back on.","commonSituations":"xAI token endpoint omitting refresh_token on some grant types; first-time login against an endpoint variant that does not rotate refresh tokens; caller not passing the previous refresh token into the refresh exchange.","solutions":["For refresh flows, pass the existing refresh token as refreshTokenFallback so a non-rotating response still succeeds.","Re-run device login to get a fresh token set that includes a refresh_token.","Inspect the raw token response — if xAI no longer returns refresh_token for your grant type, update the ai package.","Check account/product entitlements (SuperGrok plan) — some tiers may issue tokens without refresh capability."],"exampleFix":"// before: refresh call that drops the old refresh token\nconst creds = await refreshXAIToken(old.access, old.expires);\n// after: supply the old refresh token as fallback\nconst creds = await refreshXAIToken(old.access, old.expires, old.refresh);","handlingStrategy":"validation","validationCode":"// preflight before refresh: ensure you can always supply a refresh token\nif (!stored.refresh) {\n  throw new Error(\"No refresh token stored; full device login required instead of refresh\");\n}\n// pass it as the fallback so non-rotating responses still succeed\nconst creds = await refreshXAIToken(stored.access, stored.expires, stored.refresh);","typeGuard":"function hasRefreshToken(v: unknown, fallback?: string): boolean {\n  const rt = (typeof v === \"object\" && v !== null) ? (v as Record<string, unknown>).refresh_token : undefined;\n  return (typeof rt === \"string\" && rt !== \"\") || (typeof fallback === \"string\" && fallback !== \"\");\n}","tryCatchPattern":"try {\n  await xaiProvider.credentials();\n} catch (err) {\n  if (err instanceof AIError.OAuthError && err.message.includes(\"missing refresh_token\")) {\n    logger.warn(\"xAI did not return a refresh token; re-running full device login\", {});\n    await xaiProvider.device();\n  } else {\n    throw err;\n  }\n}","preventionTips":["Always thread the previous refresh token into refresh exchanges as the fallback.","Persist the refresh token durably — losing it forces full re-login when xAI omits rotation.","Confirm your SuperGrok/xAI account tier issues refresh tokens.","Log (redacted) token-response keys once to detect refresh_token omission early."],"tags":["oauth","xai","refresh-token","missing-field","device-flow"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}