{"record":{"id":"1c6c33e3f45728f0","repo":"decolua/9router","slug":"trae-callback-missing-refreshtoken","errorCode":null,"errorMessage":"\"Trae callback missing refreshToken\"","messagePattern":"\"Trae callback missing refreshToken\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/trae.js","lineNumber":99,"sourceCode":"// Parse the Trae OAuth callback (query string or full URL).\n// Expected: ?isRedirect=true&refreshToken=...&loginHost=...[&x-cloudide-token=...]\nfunction parseTraeCallback(raw) {\n  const text = String(raw || \"\").trim();\n  let queryStr = text;\n  if (text.includes(\"?\")) queryStr = text.slice(text.indexOf(\"?\") + 1);\n  if (text.startsWith(\"#\")) queryStr = text.slice(1);\n  const params = Object.fromEntries(new URLSearchParams(queryStr));\n  const pick = (keys) => {\n    for (const k of keys) { const v = params[k]; if (v && String(v).trim()) return String(v).trim(); }\n    return null;\n  };\n  const err = pick([\"error\", \"error_code\", \"errorCode\"]);\n  if (err) {\n    const desc = pick([\"error_description\", \"error_desc\", \"message\"]);\n    throw new Error(desc ? `Trae auth failed: ${err} (${desc})` : `Trae auth failed: ${err}`);\n  }\n  const refreshToken = pick([\"refreshToken\", \"refresh_token\", \"RefreshToken\"]);\n  if (!refreshToken) throw new Error(\"Trae callback missing refreshToken\");\n  const loginHost = pick([\"loginHost\", \"login_host\", \"LoginHost\", \"host\", \"consoleHost\"]);\n  if (!loginHost) throw new Error(\"Trae callback missing loginHost\");\n  const cloudideToken = pick([\"x-cloudide-token\", \"xCloudideToken\", \"accessToken\", \"access_token\", \"token\"]);\n  return { refreshToken, loginHost, cloudideToken };\n}\n\n// Allowed API origins for ExchangeToken/GetUserInfo — hardcoded HTTPS allowlist only.\n// loginHost from the callback is intentionally NOT honored (SSRF guard: a callback\n// attacker could otherwise point this at internal hosts/cloud metadata).\nfunction traeApiOrigins() {\n  return [...TRAE_CONFIG.apiOrigins];\n}\n\n// POST ExchangeToken {ClientID, RefreshToken, ClientSecret, UserID} → {Result:{AccessToken,RefreshToken,ExpiresAt}}\nasync function fetchTraeExchangeToken(refreshToken, cloudideToken) {\n  const body = JSON.stringify({\n    ClientID: TRAE_CONFIG.clientId,\n    RefreshToken: refreshToken,","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/trae.js#L81-L117","documentation":"Thrown by parseTraeCallback when no error param is present but none of the accepted refresh-token keys (refreshToken, refresh_token, RefreshToken) appear in the callback parameters. The callback succeeded from Trae's perspective but the credential this library requires is absent, so the account cannot be persisted.","triggerScenarios":"Trae redirects back with a success payload that lacks any refresh-token field: provider changed the callback parameter names, callback was truncated/modified by a redirect chain, or a partial/malformed callback URL was pasted manually.","commonSituations":"Trae renames callback params in a new version; a middleware/reverse proxy strips query params; the user hand-edited the callback URL; copying the URL dropped part of the query string.","solutions":["Inspect the full callback URL/query string and confirm which parameters Trae actually returned.","If Trae renamed the field, add the new key name to the pick() list (refresh_token variants) in parseTraeCallback.","Re-run the login flow and paste the complete, unmodified callback URL.","Check any proxy/redirect layer for query-param stripping."],"exampleFix":"// before\nconst refreshToken = pick([\"refreshToken\", \"refresh_token\", \"RefreshToken\"]);\n// after\nconst refreshToken = pick([\"refreshToken\", \"refresh_token\", \"RefreshToken\", \"refreshTokenKey\"]); // add renamed key","handlingStrategy":"validation","validationCode":"const params = new URLSearchParams(callbackUrl.split('?')[1] || '');\nconst hasRefresh = ['refreshToken', 'refresh_token', 'RefreshToken'].some(k => params.get(k)?.trim());\nif (!hasRefresh) throw new Error('Callback has no refresh token; re-run Trae login');","typeGuard":"const isNonEmptyString = (v) => typeof v === 'string' && v.trim().length > 0;","tryCatchPattern":"try {\n  const creds = parseTraeCallback(callbackUrl);\n} catch (e) {\n  if (e.message === 'Trae callback missing refreshToken') {\n    // log raw query params; re-run login or update accepted key names\n  } else throw e;\n}","preventionTips":["Log the raw callback query string (minus secrets) to catch provider-side param renames quickly.","Never hand-edit or truncate the callback URL when pasting it.","Check redirect chains/proxies for query-param stripping."],"tags":["oauth","oauth-callback","missing-credential","api-schema-change"],"backgroundTag":"missing-credential","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}