{"record":{"id":"c5999dd6a9900ea7","repo":"decolua/9router","slug":"windsurf-callback-missing-access-token","errorCode":null,"errorMessage":"Windsurf callback missing access_token","messagePattern":"Windsurf callback missing access_token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/providers/windsurf.js","lineNumber":41,"sourceCode":"\n// Parse Windsurf callback (query string or full URL): ?access_token=...&state=...\nfunction parseWindsurfCallback(raw, expectedState) {\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\"]);\n  if (err) {\n    const desc = pick([\"error_description\"]);\n    throw new Error(desc ? `Windsurf auth failed: ${err} (${desc})` : `Windsurf auth failed: ${err}`);\n  }\n  const accessToken = pick([\"access_token\", \"token\"]);\n  if (!accessToken) throw new Error(\"Windsurf callback missing access_token\");\n  const state = pick([\"state\"]);\n  if (expectedState && state && state !== expectedState) {\n    throw new Error(\"Windsurf callback state mismatch\");\n  }\n  return { firebaseIdToken: accessToken };\n}\n\n// POST RegisterUser {firebase_id_token} → {apiKey, apiServerUrl, name}\nasync function fetchWindsurfRegisterUser(firebaseIdToken) {\n  const data = await windsurfSeatRequest(WINDSURF_CONFIG.registerApiBaseUrl, WINDSURF_CONFIG.registerPath, {\n    firebase_id_token: firebaseIdToken,\n  });\n  const apiKey = extractJsonPath(data, [[\"apiKey\"], [\"api_key\"]]);\n  if (!apiKey) throw new Error(\"Windsurf RegisterUser missing apiKey\");\n  const apiServerUrl = extractJsonPath(data, [[\"apiServerUrl\"], [\"api_server_url\"]]) || WINDSURF_CONFIG.defaultApiServerUrl;\n  const name = extractJsonPath(data, [[\"name\"]]);\n  return { apiKey, apiServerUrl, name };\n}","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/providers/windsurf.js#L23-L59","documentation":"parseWindsurfCallback throws this when the callback URL contains no error but also no access_token (or token) parameter, so there is no Firebase id token to continue with. The Windsurf flow uses response_type=token (implicit flow), so the token must arrive in the redirect fragment/query; its absence means the redirect was not a successful token callback.","triggerScenarios":"User pastes the wrong URL (e.g. the signin page URL instead of the final redirect); the redirect lost the hash fragment (fragments are not forwarded by intermediate servers); Windsurf redirected to a different page after login; empty/garbage string passed as raw.","commonSituations":"Manual paste-token flow where the user copies the wrong URL from the browser; redirect middleware stripping query params; hash-based tokens lost because the callback went through an HTTP redirect.","solutions":["Ensure the user copies the FULL final URL from the browser address bar, including the #access_token=... fragment","Confirm the local callback server (callbackPath) captured the fragment before redirecting — fragments only exist client-side","Verify buildAuthUrl uses response_type=token and redirect_parameters_type=query so Windsurf appends the token","Check the raw string is non-empty and is the redirect URL, not the auth page URL","Retry the OAuth flow in a normal (non-embedded) browser"],"exampleFix":"// before: caller passes whatever the user typed\nconst tokens = await exchangeToken(config, rawPastedUrl, redirectUri, null, state);\n// after: sanity-check before calling\nif (!/(access_token=|token=)/.test(rawPastedUrl)) {\n  throw new Error('Pasted URL has no access_token — copy the full redirect URL including the # fragment');\n}\nconst tokens = await exchangeToken(config, rawPastedUrl, redirectUri, null, state);","handlingStrategy":"validation","validationCode":"// validate the pasted/redirected URL contains a token before parsing\nconst hasToken = (raw) => {\n  const s = String(raw || '');\n  const q = s.slice(s.indexOf('?') + 1);\n  const frag = s.includes('#') ? s.slice(s.indexOf('#') + 1) : '';\n  const p = new URLSearchParams(q + (frag ? '&' + frag : ''));\n  return Boolean(p.get('access_token') || p.get('token'));\n};\nif (!hasToken(callbackUrl)) throw new Error('Callback URL has no access_token — copy the FULL final redirect URL');","typeGuard":"const isTokenCallback = (raw) => /(access_token=|token=)/.test(String(raw || ''));","tryCatchPattern":"try { ({ firebaseIdToken } = parseWindsurfCallback(raw, state)); }\ncatch (e) {\n  if (e.message === 'Windsurf callback missing access_token') {\n    throw new Error('No token in redirect — re-run sign-in and copy the complete URL including the #fragment');\n  }\n  throw e;\n}","preventionTips":["Instruct users to copy the entire final URL, never just the visible path","Remember URL fragments (#access_token) never reach a server — capture them client-side","Confirm buildAuthUrl uses response_type=token with query redirect parameters","Test the callback capture with a real browser before shipping"],"tags":["oauth","implicit-flow","windsurf","missing-parameter"],"backgroundTag":"missing-oauth-token","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}