{"record":{"id":"9b222b18f825ccd5","repo":"tinyhumansai/openhuman","slug":"login-token-invalid-or-expired","errorCode":null,"errorMessage":"Login token invalid or expired","messagePattern":"Login token invalid or expired","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/src/services/api/authApi.ts","lineNumber":60,"sourceCode":"    throw error;\n  } finally {\n    window.clearTimeout(timeoutId);\n  }\n}\n\n/**\n * Consume a verified login token and return the JWT.\n * Works for both Telegram and OAuth login tokens.\n * POST /telegram/login-tokens/:token/consume (no auth required)\n */\nexport async function consumeLoginToken(loginToken: string): Promise<string> {\n  const response = await callCoreRpc<{ result: { jwtToken: string } }>({\n    method: 'openhuman.auth.consume_login_token',\n    params: { loginToken },\n  });\n  const jwtToken = response.result?.jwtToken;\n  if (!jwtToken) {\n    throw new Error('Login token invalid or expired');\n  }\n  return jwtToken;\n}\n","sourceCodeStart":42,"sourceCodeEnd":64,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/api/authApi.ts#L42-L64","documentation":"Thrown by consumeLoginToken() when the core RPC 'openhuman.auth.consume_login_token' resolves but the result has no jwtToken string. The core validates the one-time login token (Telegram/OAuth verified-link flow) against the backend; a missing JWT in a successful envelope means the core considered the token unusable — invalid, already consumed, or expired — or the token string passed in was garbage. This is the generic client-side translation for 'token did not yield a session'.","triggerScenarios":"User clicks an old magic/Telegram login link whose token already expired or was consumed on another device; the token was truncated when copied out of the URL; calling consumeLoginToken twice with the same token (one-time use); clock skew between client and backend shortening the validity window.","commonSituations":"Stale browser tab with a login link reopened later; email clients mangling long query strings; dev flows where the same fixture token is reused across test runs; user pasting only part of the URL.","solutions":["Request a fresh link/token and consume it promptly — expiry and single-use consumption are the dominant causes.","Verify the login token extracted from the URL is complete (no truncation, no HTML-escaping artifacts like &amp;).","Ensure the consume call happens exactly once per token — guard against React double-invoke (StrictMode) re-consuming.","If it always fails with fresh tokens, check backend clock sync and the token TTL configured on the backend."],"exampleFix":"// before\nconst jwtToken = response.result?.jwtToken;\nif (!jwtToken) {\n  throw new Error('Login token invalid or expired');\n}\n\n// after — one-shot consume with double-invoke guard\nlet consumed = false;\nasync function consumeOnce(token: string) {\n  if (consumed) return;\n  consumed = true;\n  const response = await callCoreRpc<{ result: { jwtToken: string } }>({\n    method: 'openhuman.auth.consume_login_token',\n    params: { loginToken: token },\n  });\n  const jwtToken = response.result?.jwtToken;\n  if (!jwtToken) {\n    consumed = false; // allow re-request of a NEW token, not replay of this one\n    throw new Error('Login token invalid or expired — request a new link');\n  }\n  return jwtToken;\n}","handlingStrategy":"validation","validationCode":"const token = new URL(loginUrl).searchParams.get('token') ?? '';\nif (!token || token.length < 16) {\n  // link is mangled/expired — request a fresh one instead of consuming\n  throw new Error('Login link is malformed — request a new one');\n}\nconst jwt = await consumeLoginToken(token);","typeGuard":"function hasJwtToken(res: unknown): res is { result: { jwtToken: string } } {\n  return typeof (res as { result?: { jwtToken?: unknown } })?.result?.jwtToken === 'string'\n    && ((res as { result: { jwtToken: string } }).result.jwtToken.length > 0);\n}","tryCatchPattern":"try {\n  return await consumeLoginToken(token);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Login token invalid or expired') {\n    redirect('/auth?expired=1'); // prompt for a fresh link\n  } else throw e;\n}","preventionTips":["Consume each token exactly once; guard against React StrictMode double-invoke.","Extract the token with URL/searchParams to avoid truncation.","Treat any failure as 'request a new link' UX rather than a dead end."],"tags":["auth","login-token","one-time-token","core-rpc","jwt"],"backgroundTag":"login-token-invalid-or-expired","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}