{"record":{"id":"545b04fdd7c3d3e8","repo":"decolua/9router","slug":"trae-callback-missing-loginhost","errorCode":null,"errorMessage":"\"Trae callback missing loginHost\"","messagePattern":"\"Trae callback missing loginHost\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/trae.js","lineNumber":101,"sourceCode":"function 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,\n    ClientSecret: TRAE_CONFIG.clientSecret,\n    UserID: \"\",","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/trae.js#L83-L119","documentation":"Thrown by parseTraeCallback when the callback lacks any login-host field among loginHost, login_host, LoginHost, host, or consoleHost. The loginHost is needed to build API origins for subsequent ExchangeToken/GetUserInfo calls; note the library intentionally does NOT trust arbitrary hosts from the callback for API calls (SSRF guard), but a host field is still required by the flow.","triggerScenarios":"Trae's success redirect omits the login host parameter entirely — schema change on their side, sanitized callback URL, or a redirect chain that dropped params.","commonSituations":"Trae renames/removes the host param in a server update; manual URL pasting loses the parameter; intermediary proxy normalizes away unknown query keys.","solutions":["Dump the raw callback parameters to see exactly what Trae returned.","If the field was renamed, extend the pick() key list in parseTraeCallback with the new name.","Re-run the login flow to get an unmodified callback payload.","Verify no reverse proxy or browser extension is stripping query parameters."],"exampleFix":"// before\nconst loginHost = pick([\"loginHost\", \"login_host\", \"LoginHost\", \"host\", \"consoleHost\"]);\n// after\nconst loginHost = pick([\"loginHost\", \"login_host\", \"LoginHost\", \"host\", \"consoleHost\", \"apiHost\"]); // add renamed key","handlingStrategy":"validation","validationCode":"const params = new URLSearchParams(callbackUrl.split('?')[1] || '');\nconst hasHost = ['loginHost', 'login_host', 'LoginHost', 'host', 'consoleHost'].some(k => params.get(k)?.trim());\nif (!hasHost) throw new Error('Callback has no loginHost; re-run Trae login');","typeGuard":null,"tryCatchPattern":"try {\n  const creds = parseTraeCallback(callbackUrl);\n} catch (e) {\n  if (e.message === 'Trae callback missing loginHost') {\n    // dump raw params, re-run login, or extend accepted key names after a Trae schema change\n  } else throw e;\n}","preventionTips":["Capture the complete callback URL including all query parameters.","Watch for Trae API updates that rename the host parameter and update the key list.","Avoid intermediary proxies that normalize away unknown query keys."],"tags":["oauth","oauth-callback","missing-credential","ssrf"],"backgroundTag":"missing-credential","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}