{"record":{"id":"f91a3c25b3cbf0d4","repo":"jlcodes99/cockpit-tools","slug":"sub2api-access-token-expiry-missing","errorCode":"SUB2API_ACCESS_TOKEN_EXPIRY_MISSING","errorMessage":"SUB2API_ACCESS_TOKEN_EXPIRY_MISSING","messagePattern":"SUB2API_ACCESS_TOKEN_EXPIRY_MISSING","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/utils/codexExportFormats.ts","lineNumber":417,"sourceCode":"    };\n  }\n  if (!account.tokens.access_token?.trim()) {\n    throw new Error('SUB2API_ACCESS_TOKEN_MISSING');\n  }\n\n  const credentials = buildSub2apiCredentials(account);\n  const extra = buildSub2apiExtra(account);\n  const item: Sub2apiCreateAccountItem = {\n    ...base,\n    type: 'oauth',\n    credentials,\n    ...(extra ? { extra } : {}),\n  };\n\n  if (!account.tokens.refresh_token?.trim()) {\n    const tokenExpiresAt = resolveAccessTokenExpiry(account);\n    if (!tokenExpiresAt) {\n      throw new Error('SUB2API_ACCESS_TOKEN_EXPIRY_MISSING');\n    }\n    item.expires_at = Math.floor(new Date(tokenExpiresAt).getTime() / 1000);\n    item.auto_pause_on_expired = true;\n  }\n\n  return item;\n}\n\nfunction toPortableTokenStorage(\n  account: CodexAccount,\n  options: CodexExportBuildOptions = {},\n): CodexPortableTokenStorage {\n  const payload: CodexPortableTokenStorage = {\n    id_token: account.tokens.id_token || '',\n    access_token: account.tokens.access_token || '',\n    refresh_token: account.tokens.refresh_token?.trim() || '',\n    account_id: resolveAccountId(account) || '',\n    last_refresh: resolveLastRefresh(account),","sourceCodeStart":399,"sourceCodeEnd":435,"githubUrl":"https://github.com/jlcodes99/cockpit-tools/blob/1ed8b77992d62ca81fabf744deb0839ad361d5bf/src/utils/codexExportFormats.ts#L399-L435","documentation":"toSub2apiAccount throws SUB2API_ACCESS_TOKEN_EXPIRY_MISSING when an OAuth account has no refresh_token and the access token expiry (resolveAccessTokenExpiry) cannot be determined. Sub2api requires either refresh_token or an expires_at timestamp to manage token lifecycle, so the export aborts.","triggerScenarios":"Calling toSub2apiAccount for an OAuth account with empty tokens.refresh_token where resolveAccessTokenExpiry(account) returns null/undefined (no expiry stored anywhere in the account data).","commonSituations":"Imported account data lacking both refresh_token and expiry metadata; hand-edited account records; older account format that did not persist token expiry.","solutions":["Re-run OAuth for the account to obtain a fresh token set including refresh_token","Store the access token expiry on the account before exporting","Skip accounts missing both refresh_token and expiry in the export batch","Verify resolveAccessTokenExpiry covers all account variants your data contains"],"exampleFix":"// before\nconst item = toSub2apiAccount(account); // throws SUB2API_ACCESS_TOKEN_EXPIRY_MISSING\n// after\nif (account.tokens?.refresh_token?.trim() || resolveAccessTokenExpiry(account)) {\n  const item = toSub2apiAccount(account);\n}","handlingStrategy":"validation","validationCode":"const canExport = !!account.tokens?.refresh_token?.trim() || !!resolveAccessTokenExpiry(account);\nif (!canExport) throw new Error('cannot export: no refresh_token and no expiry');","typeGuard":"function hasTokenLifecycle(a: CodexAccount): boolean {\n  return !!a.tokens?.refresh_token?.trim() || !!resolveAccessTokenExpiry(a);\n}","tryCatchPattern":"try {\n  const item = toSub2apiAccount(account);\n} catch (e) {\n  if ((e as Error).message === 'SUB2API_ACCESS_TOKEN_EXPIRY_MISSING') {\n    // re-auth to obtain refresh_token or record expiry\n  }\n}","preventionTips":["Persist token expiry metadata on every account","Prefer OAuth flows that yield refresh tokens","Pre-flight batch exports for refresh_token or expiry presence"],"tags":["token-expiry","refresh-token","validation","export"],"backgroundTag":"missing-token-expiry","analyzedSha":"1ed8b77992d62ca81fabf744deb0839ad361d5bf","analyzedAt":"2026-09-05T09:51:41.178Z","contentChangedAt":"2026-09-05T09:51:41.178Z","schemaVersion":2},"datasetVersion":"2026-09-12T12:17:11.808Z"}