1import { AuthAdministrativeScopes, AuthStandardClientScopes } from "@t3tools/contracts";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "@effect/vitest";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as Stream from "effect/Stream";
8import * as Queue from "effect/Queue";
9import * as TestClock from "effect/testing/TestClock";
10import {
11 DESKTOP_BOOTSTRAP_TOKEN_WINDOW_MS,
12 currentDesktopBootstrapToken,
13} from "@t3tools/shared/desktopBootstrapToken";
14
15import * as ServerConfig from "../config.ts";
16import * as AuthPairingLinks from "../persistence/AuthPairingLinks.ts";
17import { PersistenceSqlError } from "../persistence/Errors.ts";
18import * as SqlitePersistence from "../persistence/Sqlite.ts";
19import * as PairingGrantStore from "./PairingGrantStore.ts";
20
21const layerServerConfig = (
22 overrides?: Partial<
23 Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken" | "desktopBootstrapSecret">
24 >,
25) =>
26 Layer.effect(
27 ServerConfig.ServerConfig,
28 Effect.gen(function* () {
29 const config = yield* ServerConfig.ServerConfig;
30 return {
31 ...config,
32 ...overrides,
33 } satisfies ServerConfig.ServerConfig["Service"];
34 }),
35 ).pipe(
36 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-bootstrap-test-" })),
37 );
38
39const layerPairingGrantStore = (
40 overrides?: Partial<
41 Pick<ServerConfig.ServerConfig["Service"], "desktopBootstrapToken" | "desktopBootstrapSecret">
42 >,
43) =>
44 PairingGrantStore.layer.pipe(
45 Layer.provide(SqlitePersistence.layerMemory),
46 Layer.provide(layerServerConfig(overrides)),
47 );
48
49const layerPairingGrantStoreTest = (
50 overrides: Partial<AuthPairingLinks.AuthPairingLinkRepository["Service"]>,
51) =>
52 Layer.effect(PairingGrantStore.PairingGrantStore, PairingGrantStore.make).pipe(
53 Layer.provide(
54 Layer.succeed(
55 AuthPairingLinks.AuthPairingLinkRepository,
56 AuthPairingLinks.AuthPairingLinkRepository.of({
57 create: () => Effect.void,
58 consumeAvailable: () => Effect.succeedNone,
59 listActive: () => Effect.succeed([]),
60 revoke: () => Effect.succeed(false),
61 getByCredential: () => Effect.succeedNone,
62 ...overrides,
63 }),
64 ),
65 ),
66 Layer.provide(layerServerConfig()),
67 );
68
69it.layer(NodeServices.layer)("PairingGrantStore.layer", (it) => {
70 it.effect("issues pairing tokens in a short manual-entry format", () =>
71 Effect.gen(function* () {
72 const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
73 const issued = yield* bootstrapCredentials.issueOneTimeToken();
74
75 expect(issued.credential).toMatch(/^[23456789ABCDEFGHJKLMNPQRSTUVWXYZ]{12}$/);
76 }).pipe(Effect.provide(layerPairingGrantStore())),
77 );
78
79 it.effect("issues one-time bootstrap tokens that can only be consumed once", () =>
80 Effect.gen(function* () {
81 const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
82 const issued = yield* bootstrapCredentials.issueOneTimeToken({ label: "Julius iPhone" });
83 const first = yield* bootstrapCredentials.consume(issued.credential);
84 const second = yield* Effect.flip(bootstrapCredentials.consume(issued.credential));
85
86 expect(first.method).toBe("one-time-token");
87 expect(first.scopes).toEqual(AuthStandardClientScopes);
88 expect(first.subject).toBe("one-time-token");
89 expect(first.label).toBe("Julius iPhone");
90 expect(issued.label).toBe("Julius iPhone");
91 expect(second._tag).toBe("UnknownBootstrapCredentialError");
92 expect(second.message).toContain("Unknown bootstrap credential");
93 }).pipe(Effect.provide(layerPairingGrantStore())),
94 );
95
96 it.effect("atomically consumes a one-time token when multiple requests race", () =>
97 Effect.gen(function* () {
98 const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
99 const token = yield* bootstrapCredentials.issueOneTimeToken();
100 const results = yield* Effect.all(
101 Array.from({ length: 8 }, () =>
102 Effect.result(
103 bootstrapCredentials.consume(token.credential, {
104 requestedScopes: ["orchestration:read"],
105 }),
106 ),
107 ),
108 {
109 concurrency: "unbounded",
110 },
111 );
112
113 const successes = results.filter((result) => result._tag === "Success");
114 const failures = results.filter((result) => result._tag === "Failure");
115
116 expect(successes).toHaveLength(1);
117 expect(failures).toHaveLength(7);
118 for (const failure of failures) {
119 expect(failure.failure._tag).toBe("UnknownBootstrapCredentialError");
120 expect(failure.failure.message).toContain("Unknown bootstrap credential");
121 }
122 }).pipe(Effect.provide(layerPairingGrantStore())),
123 );
124
125 it.effect("requires the bound proof key thumbprint when present", () =>
126 Effect.gen(function* () {
127 const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
128 const token = yield* bootstrapCredentials.issueOneTimeToken({
129 proofKeyThumbprint: "client-proof-key-thumbprint",
130 scopes: ["orchestration:read"],
131 });
132
133 const missing = yield* Effect.flip(bootstrapCredentials.consume(token.credential));
134 const wrong = yield* Effect.flip(
135 bootstrapCredentials.consume(token.credential, {
136 proofKeyThumbprint: "other-proof-key-thumbprint",
137 requestedScopes: ["access:write"],
138 }),
139 );
140 const forbiddenScope = yield* Effect.flip(
141 bootstrapCredentials.consume(token.credential, {
142 proofKeyThumbprint: "client-proof-key-thumbprint",
143 requestedScopes: ["access:write"],
144 }),
145 );
146 const consumed = yield* bootstrapCredentials.consume(token.credential, {
147 proofKeyThumbprint: "client-proof-key-thumbprint",
148 requestedScopes: ["orchestration:read"],
149 });
150
151 expect(missing.message).toContain("proof key mismatch");
152 expect(wrong.message).toContain("proof key mismatch");
153 expect(forbiddenScope._tag).toBe("BootstrapCredentialScopeNotGrantedError");
154 expect(consumed.proofKeyThumbprint).toBe("client-proof-key-thumbprint");
155 }).pipe(Effect.provide(layerPairingGrantStore())),
156 );
157
158 it.effect("seeds the desktop bootstrap credential as a reusable grant", () =>
159 Effect.gen(function* () {
160 const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
161 const first = yield* bootstrapCredentials.consume("desktop-bootstrap-token");
162 const second = yield* bootstrapCredentials.consume("desktop-bootstrap-token");
163 const third = yield* bootstrapCredentials.consume("desktop-bootstrap-token");
164
165 expect(first.method).toBe("desktop-bootstrap");
166 expect(first.scopes).toEqual(AuthAdministrativeScopes);
167 expect(first.subject).toBe("desktop-bootstrap");
168 expect(second.method).toBe("desktop-bootstrap");
169 expect(third.method).toBe("desktop-bootstrap");
170 }).pipe(
171 Effect.provide(
172 layerPairingGrantStore({
173 desktopBootstrapToken: "desktop-bootstrap-token",
174 }),
175 ),
176 ),
177 );
178
179 it.effect("reports seeded desktop bootstrap credentials as expired after their ttl", () =>
180 Effect.gen(function* () {
181 const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
182
183 // The desktop-bootstrap grant lives for 24h. Within that window
184 // it stays reusable.
185 yield* TestClock.adjust(Duration.hours(12));
186 const stillValid = yield* bootstrapCredentials.consume("desktop-bootstrap-token");
187 expect(stillValid.method).toBe("desktop-bootstrap");
188
189 yield* TestClock.adjust(Duration.hours(13));
190 const expired = yield* Effect.flip(bootstrapCredentials.consume("desktop-bootstrap-token"));
191
192 expect(expired._tag).toBe("ExpiredBootstrapCredentialError");
193 expect(expired.message).toContain("Bootstrap credential expired");
194 }).pipe(
195 Effect.provide(
196 Layer.merge(
197 layerPairingGrantStore({
198 desktopBootstrapToken: "desktop-bootstrap-token",
199 }),
200 TestClock.layer(),
201 ),
202 ),
203 ),
204 );
205
206 it.effect("accepts rotating desktop bootstrap tokens derived from the desktop secret", () =>
207 Effect.gen(function* () {
208 const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
209 const window = DESKTOP_BOOTSTRAP_TOKEN_WINDOW_MS;
210
211 // A desktop open for days hands the renderer a fresh token each window.
212 yield* TestClock.adjust(Duration.days(5));
213 const now = 5 * 24 * 60 * 60 * 1000;
214 const current = yield* bootstrapCredentials.consume(
215 currentDesktopBootstrapToken("desktop-secret", now),
216 );
217 expect(current.method).toBe("desktop-bootstrap");
218
219 const stale = yield* Effect.flip(
220 bootstrapCredentials.consume(
221 currentDesktopBootstrapToken("desktop-secret", now - 2 * window),
222 ),
223 );
224 expect(stale._tag).toBe("UnknownBootstrapCredentialError");
225
226 // The launch token is not accepted on its own once a secret is present.
227 const launch = yield* Effect.flip(bootstrapCredentials.consume("desktop-bootstrap-token"));
228 expect(launch._tag).toBe("UnknownBootstrapCredentialError");
229 }).pipe(
230 Effect.provide(
231 Layer.merge(
232 layerPairingGrantStore({
233 desktopBootstrapToken: "desktop-bootstrap-token",
234 desktopBootstrapSecret: "desktop-secret",
235 }),
236 TestClock.layer(),
237 ),
238 ),
239 ),
240 );
241
242 it.effect("keeps credentials out of pairing lists and change events", () =>
243 Effect.gen(function* () {
244 const grants = yield* PairingGrantStore.PairingGrantStore;
245 const changes = yield* Queue.unbounded<PairingGrantStore.BootstrapCredentialChange>();
246 yield* grants.streamChanges.pipe(
247 Stream.runForEach((change) => Queue.offer(changes, change)),
248 Effect.forkScoped({ startImmediately: true }),
249 );
250 for (const input of [{}, { label: "Synthetic phone" }]) {
251 const issued = yield* grants.issueOneTimeToken(input);
252 const change = yield* Queue.take(changes);
253 expect(change?.type).toBe("pairingLinkUpserted");
254 if (change?.type !== "pairingLinkUpserted")
255 throw new Error("Expected a pairing link update");
256 expect(change.pairingLink.id).toBe(issued.id);
257 expect(change.pairingLink).not.toHaveProperty("credential");
258 const listed = (yield* grants.listActive()).find((link) => link.id === issued.id);
259 expect(listed).toEqual(change.pairingLink);
260 const consumed = yield* grants.consume(issued.credential);
261 expect(consumed.scopes).toEqual(change.pairingLink.scopes);
262 expect(yield* Queue.take(changes)).toEqual({ type: "pairingLinkRemoved", id: issued.id });
263 }
264 }).pipe(Effect.scoped, Effect.provide(layerPairingGrantStore())),
265 );
266
267 it.effect("lists and revokes active pairing links", () =>
268 Effect.gen(function* () {
269 const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;
270 const first = yield* bootstrapCredentials.issueOneTimeToken();
271 const second = yield* bootstrapCredentials.issueOneTimeToken({
272 scopes: ["orchestration:read", "access:write"],
273 });
274
275 const activeBeforeRevoke = yield* bootstrapCredentials.listActive();
276 expect(activeBeforeRevoke.map((entry) => entry.id)).toContain(first.id);
277 expect(activeBeforeRevoke.map((entry) => entry.id)).toContain(second.id);
278 for (const entry of activeBeforeRevoke) {
279 expect(entry).not.toHaveProperty("credential");
280 }
281
282 const revoked = yield* bootstrapCredentials.revoke(first.id);
283 const activeAfterRevoke = yield* bootstrapCredentials.listActive();
284 const revokedConsume = yield* Effect.flip(bootstrapCredentials.consume(first.credential));
285
286 expect(revoked).toBe(true);
287 expect(activeAfterRevoke.map((entry) => entry.id)).not.toContain(first.id);
288 expect(activeAfterRevoke.map((entry) => entry.id)).toContain(second.id);
289 expect(revokedConsume.message).toContain("no longer available");
290 expect(revokedConsume._tag).toBe("UnavailableBootstrapCredentialError");
291 }).pipe(Effect.provide(layerPairingGrantStore())),
292 );
293
294 it.effect("identifies consume-available failures and preserves their cause", () => {
295 const repositoryFailure = new PersistenceSqlError({
296 operation: "consume-pairing-link",
297 detail: "Database unavailable",
298 cause: new Error("database unavailable"),
299 });
300
301 return Effect.gen(function* () {
302 const pairingGrants = yield* PairingGrantStore.PairingGrantStore;
303 const error = yield* Effect.flip(pairingGrants.consume("credential"));
304
305 if (error._tag !== "BootstrapCredentialConsumeAvailableError") {
306 return yield* Effect.die(error);
307 }
308 expect(error.cause).toBe(repositoryFailure);
309 }).pipe(
310 Effect.provide(
311 layerPairingGrantStoreTest({
312 consumeAvailable: () => Effect.fail(repositoryFailure),
313 }),
314 ),
315 );
316 });
317});