1import { normalizeHostedServer } from "@t3tools/shared/hostedServerUrl";
2import { afterEach, describe, expect, it, vi } from "vite-plus/test";
3import { forgetHostedWebSession, hostedAuthHeaders } from "./hostedAuth";
4
5afterEach(() => {
6 forgetHostedWebSession();
7 vi.unstubAllGlobals();
8 vi.useRealTimers();
9});
10
11const server = "https://knife.example.test";
12const account = (csrfToken: string, expiresAt: string) => ({
13 accountId: "owner",
14 deviceId: "browser",
15 csrfToken,
16 expiresAt,
17});
18
19describe("hosted account client custody", () => {
20 it("uses same-origin cookies and renews with CSRF without requesting a bearer token", async () => {
21 vi.useFakeTimers();
22 vi.setSystemTime(new Date("2026-10-17T00:00:00Z"));
23 vi.stubGlobal("window", { location: { origin: server } });
24 const fetch = vi
25 .fn()
26 .mockResolvedValueOnce(Response.json(account("first-csrf", "2026-10-17T00:00:30Z")))
27 .mockResolvedValueOnce(Response.json(account("renewed-csrf", "2026-10-17T01:00:00Z")));
28 vi.stubGlobal("fetch", fetch);
29 expect(await hostedAuthHeaders(server)).toEqual({ "x-csrf-token": "first-csrf" });
30 expect(await hostedAuthHeaders(server)).toEqual({ "x-csrf-token": "renewed-csrf" });
31 expect(fetch.mock.calls[0]).toEqual([
32 `${server}/api/auth/account`,
33 { credentials: "same-origin", redirect: "error" },
34 ]);
35 expect(fetch.mock.calls[1]).toEqual([
36 `${server}/api/auth/session/refresh`,
37 {
38 method: "POST",
39 credentials: "same-origin",
40 redirect: "error",
41 headers: { "x-csrf-token": "first-csrf" },
42 },
43 ]);
44 });
45
46 it("shares concurrent browser renewal requests", async () => {
47 vi.useFakeTimers();
48 vi.setSystemTime(new Date("2026-10-17T00:00:00Z"));
49 vi.stubGlobal("window", { location: { origin: server } });
50 const fetch = vi
51 .fn()
52 .mockResolvedValueOnce(Response.json(account("first", "2026-10-17T00:15:00Z")))
53 .mockResolvedValueOnce(Response.json(account("renewed", "2026-10-17T00:29:30Z")));
54 vi.stubGlobal("fetch", fetch);
55 await hostedAuthHeaders(server);
56 vi.setSystemTime(new Date("2026-10-17T00:14:30Z"));
57 expect(await Promise.all([hostedAuthHeaders(server), hostedAuthHeaders(server)])).toEqual([
58 { "x-csrf-token": "renewed" },
59 { "x-csrf-token": "renewed" },
60 ]);
61 expect(fetch).toHaveBeenCalledTimes(2);
62 });
63
64 it("recovers a second tab's stale CSRF after another tab rotates the shared cookie", async () => {
65 vi.useFakeTimers();
66 vi.setSystemTime(new Date("2026-10-17T00:00:00Z"));
67 vi.stubGlobal("window", { location: { origin: server } });
68 let session = 1;
69 const calls: Array<{ path: string; csrf: string | null }> = [];
70 vi.stubGlobal(
71 "fetch",
72 vi.fn(async (url: string, init: RequestInit) => {
73 const path = new URL(url).pathname;
74 const csrf = new Headers(init.headers).get("x-csrf-token");
75 calls.push({ path, csrf });
76 if (path === "/api/auth/account")
77 return Response.json(account(`opaque-${session}`, "2026-10-17T00:15:00Z"));
78 if (csrf !== `opaque-${session}`) return new Response(null, { status: 401 });
79 if (path === "/api/auth/session/refresh") {
80 session++;
81 return Response.json(account(`opaque-${session}`, "2026-10-17T00:30:00Z"));
82 }
83 return Response.json({ revoked: true });
84 }),
85 );
86 vi.resetModules();
87 const firstTab = await import("./hostedAuth");
88 vi.resetModules();
89 const secondTab = await import("./hostedAuth");
90 expect(firstTab).not.toBe(secondTab);
91 await firstTab.hostedAuthHeaders(server);
92 await secondTab.hostedAuthHeaders(server);
93 await firstTab.hostedAccountRequest(server, "/api/auth/session/refresh", "POST");
94 expect(
95 (await secondTab.hostedAccountRequest(server, "/api/auth/devices/other/revoke", "POST")).ok,
96 ).toBe(true);
97 expect(calls.slice(-3)).toEqual([
98 { path: "/api/auth/devices/other/revoke", csrf: "opaque-1" },
99 { path: "/api/auth/account", csrf: null },
100 { path: "/api/auth/devices/other/revoke", csrf: "opaque-2" },
101 ]);
102 });
103
104 it("serializes each tab's concurrent renewals and repairs a shared-cookie rotation once", async () => {
105 vi.useFakeTimers();
106 vi.setSystemTime(new Date("2026-10-17T00:00:00Z"));
107 vi.stubGlobal("window", { location: { origin: server } });
108 let session = 1;
109 const refreshProofs: string[] = [];
110 const reads: number[] = [];
111 vi.stubGlobal(
112 "fetch",
113 vi.fn(async (url: string, init: RequestInit) => {
114 if (new URL(url).pathname === "/api/auth/account") {
115 reads.push(session);
116 return Response.json(
117 account(
118 `opaque-${session}`,
119 session === 1 ? "2026-10-17T00:15:00Z" : "2026-10-17T00:30:00Z",
120 ),
121 );
122 }
123 const csrf = new Headers(init.headers).get("x-csrf-token")!;
124 refreshProofs.push(csrf);
125 if (csrf !== `opaque-${session}`) return new Response(null, { status: 401 });
126 session++;
127 return Response.json(account(`opaque-${session}`, "2026-10-17T00:30:00Z"));
128 }),
129 );
130 vi.resetModules();
131 const firstTab = await import("./hostedAuth");
132 vi.resetModules();
133 const secondTab = await import("./hostedAuth");
134 await Promise.all([firstTab.hostedAuthHeaders(server), secondTab.hostedAuthHeaders(server)]);
135 vi.setSystemTime(new Date("2026-10-17T00:14:30Z"));
136 const results = await Promise.all([
137 firstTab.hostedAuthHeaders(server),
138 firstTab.hostedAuthHeaders(server),
139 secondTab.hostedAuthHeaders(server),
140 secondTab.hostedAuthHeaders(server),
141 ]);
142 expect(results[0]).toEqual(results[1]);
143 expect(results[2]).toEqual(results[3]);
144 expect(refreshProofs).toEqual(["opaque-1", "opaque-1", "opaque-2"]);
145 expect(reads).toEqual([1, 1, 2]);
146 expect(results[2]).toEqual({ "x-csrf-token": "opaque-3" });
147 });
148
149 it("bounds expired-cookie recovery to one metadata request without repeating a mutation", async () => {
150 vi.stubGlobal("window", { location: { origin: server } });
151 const fetch = vi
152 .fn()
153 .mockResolvedValueOnce(Response.json(account("expired-proof", "2035-01-01T00:00:00Z")))
154 .mockResolvedValueOnce(new Response(null, { status: 401 }))
155 .mockResolvedValueOnce(new Response(null, { status: 401 }));
156 vi.stubGlobal("fetch", fetch);
157 vi.resetModules();
158 const tab = await import("./hostedAuth");
159 await expect(
160 tab.hostedAccountRequest(server, "/api/auth/devices/other/revoke", "POST"),
161 ).rejects.toThrow("Sign in");
162 expect(fetch).toHaveBeenCalledTimes(3);
163 });
164
165 it("never sends browser cookies or CSRF to another origin", async () => {
166 vi.stubGlobal("window", { location: { origin: server } });
167 const fetch = vi.fn();
168 vi.stubGlobal("fetch", fetch);
169 await expect(hostedAuthHeaders("https://other.example.test")).rejects.toThrow(
170 "Open this server",
171 );
172 expect(fetch).not.toHaveBeenCalled();
173 });
174
175 it("obtains ephemeral native access from main and refreshes it on each request", async () => {
176 const getHostedAccessToken = vi
177 .fn()
178 .mockResolvedValueOnce("ephemeral-one")
179 .mockResolvedValueOnce("ephemeral-two");
180 vi.stubGlobal("window", {
181 location: { origin: "http://localhost:3773" },
182 desktopBridge: { getHostedAccessToken },
183 });
184 const fetch = vi.fn();
185 vi.stubGlobal("fetch", fetch);
186 expect(await hostedAuthHeaders(server)).toEqual({ authorization: "Bearer ephemeral-one" });
187 expect(await hostedAuthHeaders(server)).toEqual({ authorization: "Bearer ephemeral-two" });
188 expect(fetch).not.toHaveBeenCalled();
189 expect(getHostedAccessToken.mock.calls).toEqual([[server], [server]]);
190 });
191
192 it.each([
193 "http://knife.example.test",
194 `${server}/path`,
195 "https://user:secret@knife.example.test",
196 `${server}?token=secret`,
197 `${server}#token`,
198 ])("rejects unsafe server addresses: %s", (value) => {
199 expect(() => normalizeHostedServer(value)).toThrow();
200 });
201});