{"record":{"id":"f61ce4bc823a6e2c","repo":"can1357/oh-my-pi","slug":"label-missing-expires-in","errorCode":null,"errorMessage":"${label} missing expires_in","messagePattern":"(.+?) missing expires_in","errorType":"validation","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/registry/oauth/xai-oauth.ts","lineNumber":348,"sourceCode":"\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(\n\tfetchImpl: FetchImpl,\n\tsignal?: AbortSignal,\n): Promise<XAIDeviceAuthorization> {\n\tlet response: Response;\n\ttry {\n\t\tconst timeoutSignal = AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS);","sourceCodeStart":330,"sourceCodeEnd":366,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/xai-oauth.ts#L330-L366","documentation":"Thrown by parseXAITokenResponse when expires_in is missing or not a finite number. The library computes the credential expiry timestamp (Date.now() + expires_in*1000, minus client skew) from this field; without it, token expiry cannot be tracked and proactive refresh would be impossible, so the response is rejected.","triggerScenarios":"pollXAIDeviceToken gets a 200 body with access_token and refresh_token present but expires_in absent, null, a string (e.g. \"3600\"), or non-finite; or the same shape appears on the credentials refresh path.","commonSituations":"xAI endpoint variant returning expires_in as a string instead of a number; proxy/API-gateway stripping unknown fields; custom fetchImpl normalizing numbers to strings; xAI schema change.","solutions":["Retry the exchange — a one-off malformed response is often transient.","Inspect the raw token response with curl to see how expires_in is actually encoded.","If your fetchImpl or middleware coerces numbers to strings, remove that transformation.","Update the ai package if xAI changed the expires_in representation."],"exampleFix":"// before: accepting a string expiry silently\nconst expiresIn = String(body.expires_in);\nstoreExpiry(expiresIn);\n// after: require a finite number before computing expiry\nif (typeof body.expires_in !== \"number\" || !Number.isFinite(body.expires_in)) {\n  throw new Error(\"token response missing numeric expires_in\");\n}\nstoreExpiry(Date.now() + body.expires_in * 1000);","handlingStrategy":"validation","validationCode":"// preflight: require a numeric expires_in before computing credential expiry\nconst body: Record<string, unknown> = await res.json();\nconst expiresIn = body.expires_in;\nif (typeof expiresIn !== \"number\" || !Number.isFinite(expiresIn)) {\n  throw new Error(`xAI token response has invalid expires_in: ${JSON.stringify(expiresIn)}`);\n}\nconst expiresAt = Date.now() + expiresIn * 1000;","typeGuard":"function hasNumericExpiresIn(v: unknown): v is { expires_in: number } & Record<string, unknown> {\n  const n = (typeof v === \"object\" && v !== null) ? (v as Record<string, unknown>).expires_in : undefined;\n  return typeof n === \"number\" && Number.isFinite(n);\n}","tryCatchPattern":"try {\n  await xaiProvider.credentials();\n} catch (err) {\n  if (err instanceof AIError.OAuthError && err.message.includes(\"missing expires_in\")) {\n    // token usable but expiry untrackable — force a fresh login so expiry is known\n    await deleteStoredXAICredentials();\n    return xaiProvider.credentials();\n  }\n  throw err;\n}","preventionTips":["Remove middleware that coerces JSON numbers to strings (common with schema-normalizing proxies).","Spot-check the token endpoint's raw response with curl when setting up a new environment.","Do not hand-normalize token payloads before handing them to the library.","Update the ai package if xAI changes expires_in representation."],"tags":["oauth","xai","expires-in","missing-field","response-validation"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}