{"record":{"id":"98a9fcb607642800","repo":"paperclipai/paperclip","slug":"expired","errorCode":"expired","errorMessage":"This sign-in expired. Start the connection again.","messagePattern":"This sign-in expired\\. Start the connection again\\.","errorType":"exception","errorClass":"OAuthHandoffError","httpStatus":404,"severity":"warning","filePath":"ui/src/lib/oauthHandoff.ts","lineNumber":125,"sourceCode":"    return null;\n  }\n}\n\n/**\n * Resolve a start response into the next browser navigation.\n *\n * Managed Cloud sessions are exchanged only through the fixed same-origin\n * endpoint. Legacy, self-hosted, and direct provider OAuth keep using the\n * server-supplied authorization URL after the existing URL safety gate.\n */\nexport async function prepareOAuthNavigation(\n  start: Pick<ToolOAuthStartResult, \"authorizationUrl\" | \"handoff\">,\n  options: { signal?: AbortSignal; request?: typeof fetch } = {},\n): Promise<PreparedOAuthNavigation> {\n  const handoff = parseHandoff(start.handoff);\n  if (!handoff) {\n    const target = resolveAuthorizationTarget(start.authorizationUrl);\n    if (!target.ok) throw new OAuthHandoffError(target.message, \"invalid_handoff\");\n    return { kind: \"authorization\", url: target.url, host: target.host };\n  }\n\n  const response = await postCloudHandoff(handoff.session, options);\n  const body = await response.json().catch(() => null) as Record<string, unknown> | null;\n  if (!response.ok) {\n    if (body?.error === \"RECENT_LOGIN_REQUIRED\") {\n      const reauthentication = exactReauthenticationTarget(body.reauthenticationUrl, handoff.session);\n      if (reauthentication) return reauthentication;\n    }\n    throw handoffFailure(response.status, body?.error);\n  }\n  const authorization = resolveAuthorizationTarget(\n    typeof body?.authorizationUrl === \"string\" ? body.authorizationUrl : undefined,\n  );\n  if (!authorization.ok) {\n    throw new OAuthHandoffError(\"Paperclip Cloud returned an invalid provider sign-in address.\", \"invalid_handoff\");\n  }","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/ui/src/lib/oauthHandoff.ts#L107-L143","documentation":"prepareOAuthNavigation resolves the sign-in destination. When the start result includes a cloud handoff, it POSTs the session to Paperclip Cloud; if the cloud responds with an error indicating the session is no longer valid (404 or SESSION_NOT_AVAILABLE via handoffFailure), the user-facing error is code \"expired\" — the sign-in must be restarted. Handoff sessions are short-lived by design.","triggerScenarios":"Calling prepareOAuthNavigation with a handoff whose session was already consumed, superseded by a newer handoff, or aged past the cloud's session TTL, causing the POST to return 404 / SESSION_NOT_AVAILABLE.","commonSituations":"User left the setup dialog open too long before clicking through; double-clicking start so the first POST consumed the session; resuming a pending handoff saved in sessionStorage from an earlier (expired) attempt; server restarted with in-memory session store.","solutions":["Restart the connection sign-in flow to mint a fresh handoff session.","Clear the stale pending handoff from sessionStorage before retrying.","Avoid double-invoking the start/handoff step; debounce the start button.","Catch OAuthHandoffError code \"expired\" in UI and auto-restart the flow."],"exampleFix":"// before\nawait prepareOAuthNavigation(start); // throws if session expired\n// after\ntry {\n  await prepareOAuthNavigation(start);\n} catch (e) {\n  if (e instanceof OAuthHandoffError && e.code === \"expired\") {\n    clearPendingCloudHandoff();\n    await restartConnectionSignIn();\n  } else throw e;\n}","handlingStrategy":"retry","validationCode":"const pending = readPendingCloudHandoffSafe(sessionStorage);\nif (pending && Date.now() - pending.savedAt > MAX_HANDOFF_TTL_MS) {\n  clearPendingCloudHandoff(); // stale — restart sign-in proactively\n}","typeGuard":"function isFreshHandoff(p: { savedAt: number } | null, ttlMs = 5 * 60_000): p is { savedAt: number } {\n  return !!p && Date.now() - p.savedAt < ttlMs;\n}","tryCatchPattern":"try {\n  await prepareOAuthNavigation(start);\n} catch (e) {\n  if (e instanceof OAuthHandoffError && e.code === \"expired\") {\n    clearPendingCloudHandoff();\n    await restartSignIn(); // mint a fresh session transparently\n  } else throw e;\n}","preventionTips":["Expire pending sessionStorage handoffs client-side using savedAt + a conservative TTL.","Debounce the start control so a double-click cannot consume the session twice.","Auto-restart the flow on code \"expired\" instead of dead-ending the user.","Avoid long idle periods between starting sign-in and completing it."],"tags":["oauth","session-expired","retryable"],"backgroundTag":"jwt-token-expired","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}