authorgravatar for saikrisha.ambeti1@gmail.comSaikrishna Ambeti <saikrisha.ambeti1@gmail.com> 2026-05-03 00:53:26+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-02 12:23:26-07:00
log8f50ca8e2c0e7dc4464fbbc17763dab2e109ac0c
tree563093e916e2a92354ec6a26f593e6015bff04d9
parent4ceabac76493315e67c65b8e2212d54b79a66113
signature Signed by PGP key B5690EEEBB952194

fix(web): show friendly error for invalid pairing token (#2222)


2 files changed, 63 insertions(+), 1 deletions(-)

apps/web/src/authBootstrap.test.ts+28
......@@ -353,6 +353,34 @@ describe("resolveInitialServerAuthGateState", () => {
353353 expect(fetchMock).toHaveBeenCalledTimes(3);
354354 });
355355
356 it("surfaces a friendly error message when an invalid pairing token is submitted", async () => {
357 const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
358 jsonResponse(
359 {
360 error: "Invalid bootstrap credential.",
361 },
362 {
363 status: 401,
364 },
365 ),
366 );
367 vi.stubGlobal("fetch", fetchMock);
368
369 const { submitServerAuthCredential } = await import("./environments/primary");
370
371 await expect(submitServerAuthCredential("bad-token")).rejects.toThrow(
372 "Invalid pairing token. Check the token and try again.",
373 );
374 expect(fetchMock).toHaveBeenCalledWith("http://localhost/api/auth/bootstrap", {
375 body: JSON.stringify({ credential: "bad-token" }),
376 credentials: "include",
377 headers: {
378 "content-type": "application/json",
379 },
380 method: "POST",
381 });
382 });
383
356384 it("waits for the authenticated session to become observable after silent desktop bootstrap", async () => {
357385 vi.useFakeTimers();
358386 const fetchMock = vi
apps/web/src/environments/primary/auth.ts+35-1
......@@ -110,6 +110,40 @@ async function readErrorMessage(response: Response, fallbackMessage: string): Pr
110110 return text || fallbackMessage;
111111}
112112
113const INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES = new Set([
114 "Invalid bootstrap credential.",
115 "Unknown bootstrap credential.",
116]);
117
118function parseBootstrapErrorMessage(message: string): string {
119 const trimmed = message.trim();
120 if (trimmed.length === 0) {
121 return "";
122 }
123
124 try {
125 const parsed = JSON.parse(trimmed) as {
126 error?: unknown;
127 };
128 if (typeof parsed.error === "string" && parsed.error.trim().length > 0) {
129 return parsed.error.trim();
130 }
131 } catch {
132 // Not JSON; fall back to plain text.
133 }
134
135 return trimmed;
136}
137
138function toFriendlyBootstrapErrorMessage(status: number, message: string): string {
139 const parsedMessage = parseBootstrapErrorMessage(message);
140 if (status === 401 && INVALID_BOOTSTRAP_CREDENTIAL_MESSAGES.has(parsedMessage)) {
141 return "Invalid pairing token. Check the token and try again.";
142 }
143
144 return parsedMessage;
145}
146
113147async function exchangeBootstrapCredential(credential: string): Promise<AuthBootstrapResult> {
114148 return retryTransientBootstrap(async () => {
115149 const payload: AuthBootstrapInput = { credential };
......@@ -123,7 +157,7 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBoot
123157 });
124158
125159 if (!response.ok) {
126 const message = await response.text();
160 const message = toFriendlyBootstrapErrorMessage(response.status, await response.text());
127161 throw new BootstrapHttpError({
128162 message: message || `Failed to bootstrap auth session (${response.status}).`,
129163 status: response.status,