paperclipai/paperclip · error · Error

CLI auth challenge expired before approval.

Error message

CLI auth challenge expired before approval.

What it means

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.

Source

Thrown at cli/src/client/board-auth.ts:281

      );
      setStoredBoardCredential({
        apiBase,
        token: challenge.boardApiToken,
        userId: me.userId ?? me.user?.id ?? null,
        storePath: params.storePath,
      });
      return {
        token: challenge.boardApiToken,
        approvalUrl,
        userId: me.userId ?? me.user?.id ?? null,
      };
    }

    if (status.status === "cancelled") {
      throw new Error("CLI auth challenge was cancelled.");
    }
    if (status.status === "expired") {
      throw new Error("CLI auth challenge expired before approval.");
    }

    await sleep(pollMs);
  }

  throw new Error("CLI auth challenge expired before approval.");
}

export async function revokeStoredBoardCredential(params: {
  apiBase: string;
  token: string;
}): Promise<void> {
  const apiBase = normalizeApiBase(params.apiBase);
  await requestJson<{ revoked: boolean }>(`${apiBase}/api/cli-auth/revoke-current`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${params.token}`,
    },

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Re-run `paperclipai login` and approve the printed URL promptly.
  2. If the browser did not open automatically, copy the URL from stderr into a browser manually.
  3. Check for clock skew between the CLI host and the server (`date` on both); correct NTP if drift is large.
  4. If TTL is too short for your workflow, ask the operator to raise the server-side challenge expiry.
Defensive patterns

Strategy: retry

Try / catch

async function loginWithRetry(params: Parameters<typeof loginBoardCli>[0], attempts = 2) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await loginBoardCli(params);
    } catch (err) {
      const msg = err instanceof Error ? err.message : '';
      if (msg.includes('expired before approval') && i < attempts - 1) {
        console.error('Challenge expired, retrying...');
        continue;
      }
      throw err;
    }
  }
  throw new Error('unreachable');
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/02f8f31da585ca5a. Report an issue: GitHub.