{"record":{"id":"02f8f31da585ca5a","repo":"paperclipai/paperclip","slug":"cli-auth-challenge-expired-before-approval","errorCode":null,"errorMessage":"CLI auth challenge expired before approval.","messagePattern":"CLI auth challenge expired before approval\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/client/board-auth.ts","lineNumber":281,"sourceCode":"      );\n      setStoredBoardCredential({\n        apiBase,\n        token: challenge.boardApiToken,\n        userId: me.userId ?? me.user?.id ?? null,\n        storePath: params.storePath,\n      });\n      return {\n        token: challenge.boardApiToken,\n        approvalUrl,\n        userId: me.userId ?? me.user?.id ?? null,\n      };\n    }\n\n    if (status.status === \"cancelled\") {\n      throw new Error(\"CLI auth challenge was cancelled.\");\n    }\n    if (status.status === \"expired\") {\n      throw new Error(\"CLI auth challenge expired before approval.\");\n    }\n\n    await sleep(pollMs);\n  }\n\n  throw new Error(\"CLI auth challenge expired before approval.\");\n}\n\nexport async function revokeStoredBoardCredential(params: {\n  apiBase: string;\n  token: string;\n}): Promise<void> {\n  const apiBase = normalizeApiBase(params.apiBase);\n  await requestJson<{ revoked: boolean }>(`${apiBase}/api/cli-auth/revoke-current`, {\n    method: \"POST\",\n    headers: {\n      authorization: `Bearer ${params.token}`,\n    },","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/client/board-auth.ts#L263-L299","documentation":"Thrown by loginBoardCli() when the polled challenge status is `expired`. The challenge has a server-issued expiresAt; if the poll returns status === \"expired\" before it is approved, the CLI aborts immediately rather than waiting out its own clock. Same user-facing message as [3] but reached via the server's explicit expired status.","triggerScenarios":"The approving user did not open/approve the URL before the challenge TTL elapsed, and the server marked it expired. The poll then observes status === \"expired\" and throws. Clock skew between client and server can also make the server report expired while the client thinks time remains.","commonSituations":"User walked away after `paperclipai login`. Approval page never loaded (browser did not open — see PAPERCLIP_NO_BROWSER). Server challenge TTL shortened by config. Significant client/server clock drift.","solutions":["Re-run `paperclipai login` and approve the printed URL promptly.","If the browser did not open automatically, copy the URL from stderr into a browser manually.","Check for clock skew between the CLI host and the server (`date` on both); correct NTP if drift is large.","If TTL is too short for your workflow, ask the operator to raise the server-side challenge expiry."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function loginWithRetry(params: Parameters<typeof loginBoardCli>[0], attempts = 2) {\n  for (let i = 0; i < attempts; i++) {\n    try {\n      return await loginBoardCli(params);\n    } catch (err) {\n      const msg = err instanceof Error ? err.message : '';\n      if (msg.includes('expired before approval') && i < attempts - 1) {\n        console.error('Challenge expired, retrying...');\n        continue;\n      }\n      throw err;\n    }\n  }\n  throw new Error('unreachable');\n}","preventionTips":["Approve the printed URL immediately after running login.","Sync system clocks via NTP on both CLI host and server.","If TTL is short, ask the operator to raise the server challenge expiry.","Ensure PAPERCLIP_NO_BROWSER is unset so the browser opens automatically."],"tags":["auth","cli","board-auth","timeout"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}