import { useMemo } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useParams, useSearchParams } from "@/lib/router"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { accessApi } from "../api/access"; import { ApiError } from "../api/client"; import { authApi } from "../api/auth"; import { queryKeys } from "../lib/queryKeys"; function cliAuthRetryDelay(error: unknown): number { const body = error instanceof ApiError && error.body && typeof error.body === "object" ? error.body as { details?: unknown } : null; const details = body?.details && typeof body.details === "object" ? body.details as { retryAfterSeconds?: unknown } : null; const milliseconds = typeof details?.retryAfterSeconds === "number" ? details.retryAfterSeconds * 1000 : Number.NaN; return Number.isFinite(milliseconds) && milliseconds > 0 ? Math.max(1000, milliseconds) : 60_000; } function isCliAuthRateLimit(error: unknown): error is ApiError { return error instanceof ApiError && error.status === 429; } export function CliAuthPage() { const queryClient = useQueryClient(); const params = useParams(); const [searchParams] = useSearchParams(); const challengeId = (params.id ?? "").trim(); const token = (searchParams.get("token") ?? "").trim(); const currentPath = useMemo( () => `/cli-auth/${encodeURIComponent(challengeId)}${token ? `?token=${encodeURIComponent(token)}` : ""}`, [challengeId, token], ); const sessionQuery = useQuery({ queryKey: queryKeys.auth.session, queryFn: () => authApi.getSession(), retry: false, }); const challengeQueryKey = useMemo(() => ["cli-auth-challenge", challengeId, token] as const, [challengeId, token]); const retryWindowEndsAt = useMemo(() => Date.now() + 10 * 60_000, [challengeId, token]); const challengeQuery = useQuery({ queryKey: challengeQueryKey, queryFn: () => accessApi.getCliAuthChallenge(challengeId, token), enabled: challengeId.length > 0 && token.length > 0, retry: (failureCount, error) => { if (!isCliAuthRateLimit(error) || failureCount >= 3) return false; const knownChallenge = queryClient.getQueryData<{ expiresAt: string }>(challengeQueryKey); const knownExpiry = Date.parse(knownChallenge?.expiresAt ?? ""); const deadline = Number.isFinite(knownExpiry) ? Math.min(knownExpiry, retryWindowEndsAt) : retryWindowEndsAt; return Date.now() + cliAuthRetryDelay(error) < deadline; }, retryDelay: (_failureCount, error) => cliAuthRetryDelay(error), }); const approveMutation = useMutation({ mutationFn: () => accessApi.approveCliAuthChallenge(challengeId, token), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: queryKeys.auth.session }); await challengeQuery.refetch(); }, }); const cancelMutation = useMutation({ mutationFn: () => accessApi.cancelCliAuthChallenge(challengeId, token), onSuccess: async () => { await challengeQuery.refetch(); }, }); if (!challengeId || !token) { return
Invalid CLI auth URL.
; } const waitingForRateLimit = isCliAuthRateLimit(challengeQuery.failureReason) && challengeQuery.isFetching; if (sessionQuery.isLoading || challengeQuery.isLoading || waitingForRateLimit) { return
{waitingForRateLimit ? "Too many CLI approval requests. Retrying shortly..." : "Loading CLI auth challenge..."}
; } if (challengeQuery.error) { return (

CLI auth challenge unavailable

{challengeQuery.error instanceof Error ? challengeQuery.error.message : "Challenge is invalid or expired."}

); } const challenge = challengeQuery.data; if (!challenge) { return
CLI auth challenge unavailable.
; } if (challenge.status === "approved") { return (

CLI access approved

The Paperclip CLI can now finish authentication on the requesting machine.

Command: {challenge.command}

); } if (challenge.status === "cancelled" || challenge.status === "expired") { return (

{challenge.status === "expired" ? "CLI auth challenge expired" : "CLI auth challenge cancelled"}

Start the CLI auth flow again from your terminal to generate a new approval request.

); } if (challenge.requiresSignIn || !sessionQuery.data) { return (

Sign in required

Sign in or create an account, then return to this page to approve the CLI access request.

); } return (

Approve Paperclip CLI access

A CLI client is requesting full board access to the organizations you can access on this instance. Approve only if you started this request in your own terminal.

Command
{challenge.command}
Client
{challenge.clientName ?? "paperclipai cli"}
Requested access
{challenge.requestedAccess === "instance_admin_required" ? "Instance admin" : "Board"}
{challenge.requestedCompanyName && (
Organization for audit context
{challenge.requestedCompanyName}
)}
{(approveMutation.error || cancelMutation.error) && (

{(approveMutation.error ?? cancelMutation.error) instanceof Error ? ((approveMutation.error ?? cancelMutation.error) as Error).message : "Failed to update CLI auth challenge"}

)} {!challenge.canApprove && (

{challenge.requestedAccess === "instance_admin_required" ? "This challenge requires instance-admin access. Sign in with an instance admin account to approve it." : "You do not have access to the requested organization."}

)}
); }