{"record":{"id":"f29b795752112d75","repo":"decolua/9router","slug":"missing-accesstoken","errorCode":null,"errorMessage":"Missing accessToken","messagePattern":"Missing accessToken","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/app/api/oauth/codex/bulk-import/route.js","lineNumber":73,"sourceCode":"  for (let i = 0; i < accounts.length; i++) {\n    const raw = accounts[i];\n    try {\n      if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n        throw new Error(\"Item is not an object\");\n      }\n\n      // Strip server-controlled fields\n      const {\n        id: _id,\n        provider: _provider,\n        authType: _authType,\n        createdAt: _createdAt,\n        updatedAt: _updatedAt,\n        ...item\n      } = raw;\n\n      if (!item.accessToken || typeof item.accessToken !== \"string\") {\n        throw new Error(\"Missing accessToken\");\n      }\n\n      // Backfill missing identity fields from JWT claims\n      const psd = item.providerSpecificData || {};\n      const needsEmail = !item.email;\n      const needsAccountId = !psd.chatgptAccountId;\n      const needsPlanType = !psd.chatgptPlanType;\n\n      if (needsEmail || needsAccountId || needsPlanType) {\n        const info = extractCodexAccountInfo(item.idToken || item.accessToken) || {};\n        if (needsEmail && info.email) item.email = info.email;\n        if (needsAccountId && info.chatgptAccountId) {\n          psd.chatgptAccountId = info.chatgptAccountId;\n        }\n        if (needsPlanType && info.chatgptPlanType) {\n          psd.chatgptPlanType = info.chatgptPlanType;\n        }\n      }","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/app/api/oauth/codex/bulk-import/route.js#L55-L91","documentation":"After stripping server-controlled fields, each codex import item must carry a non-empty string accessToken (the ChatGPT/Codex OAuth access token). This check throws before the JWT backfill and connection creation, since a connection cannot be built without a token.","triggerScenarios":"An accounts[] item in the codex bulk-import POST that lacks accessToken, has it under a different key (e.g. access_token), or has a non-string value (null, number, object).","commonSituations":"Import file exported from a tool using snake_case access_token instead of accessToken, redacted/stripped tokens from a shared config, or only refreshToken present.","solutions":["Add an accessToken string field to each item (snake_case access_token is not accepted here).","If you only have access_token, rename the key to accessToken before importing.","Re-export the accounts from the source tool ensuring tokens are included and not redacted."],"exampleFix":"// before\n{ \"accounts\": [{ \"access_token\": \"eyJ...\" }] }\n// after\n{ \"accounts\": [{ \"accessToken\": \"eyJ...\" }] }","handlingStrategy":"validation","validationCode":"for (const a of accounts) {\n  if (!a?.accessToken || typeof a.accessToken !== \"string\") {\n    throw new Error(\"each item needs a string accessToken\");\n  }\n}","typeGuard":"const hasAccessToken = (x) =>\n  typeof x?.accessToken === \"string\" && x.accessToken.length > 0;","tryCatchPattern":"try {\n  await bulkImport(items);\n} catch (e) {\n  if (e.message === \"Missing accessToken\") {\n    const fixed = items.map(({ access_token, ...r }) =>\n      ({ accessToken: access_token ?? r.accessToken, ...r }));\n  } else throw e;\n}","preventionTips":["Use the accessToken (camelCase) key — codex import does not read access_token","Re-export accounts with tokens included, not redacted","Run a schema check on the import file before POSTing"],"tags":["validation","bulk-import","codex","oauth"],"backgroundTag":"missing-access-token","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}