1import {
2 AuthAdministrativeScopes,
3 AuthStandardClientScopes,
4 EnvironmentId,
5} from "@t3tools/contracts";
6import {
7 RelayEnvironmentConnectScope,
8 type RelayEnvironmentConnectResponse,
9} from "@t3tools/contracts/relay";
10import { describe, expect, it } from "@effect/vitest";
11import * as Clock from "effect/Clock";
12import * as Deferred from "effect/Deferred";
13import * as Effect from "effect/Effect";
14import * as Fiber from "effect/Fiber";
15import * as Layer from "effect/Layer";
16import * as Option from "effect/Option";
17import * as Queue from "effect/Queue";
18import * as Ref from "effect/Ref";
19import * as TestClock from "effect/testing/TestClock";
20
21import { DPOP_UNKNOWN_HINT } from "../relay/errorPresentation.ts";
22import * as ManagedRelay from "../relay/managedRelay.ts";
23import * as RpcHttp from "../rpc/http.ts";
24import * as ClientCapabilities from "../platform/capabilities.ts";
25import * as RemoteEnvironmentAuthorization from "./service.ts";
26import * as TokenStore from "./tokenStore.ts";
27
28const ENVIRONMENT_ID = EnvironmentId.make("environment-1");
29const ENDPOINT = {
30 httpBaseUrl: "https://environment.example.test",
31 wsBaseUrl: "wss://environment.example.test",
32 providerKind: "cloudflare_tunnel" as const,
33};
34const DESCRIPTOR = {
35 environmentId: ENVIRONMENT_ID,
36 label: "Remote environment",
37 platform: {
38 os: "linux",
39 arch: "x64",
40 },
41 serverVersion: "0.0.0-test",
42 capabilities: {
43 repositoryIdentity: true,
44 },
45};
46const BOOTSTRAP: RelayEnvironmentConnectResponse = {
47 environmentId: ENVIRONMENT_ID,
48 endpoint: ENDPOINT,
49 credential: "relay-bootstrap",
50 expiresAt: "2026-06-06T01:00:00.000Z",
51};
52
53type RecordedResponse = Response | ((init: RequestInit) => Response);
54
55function recordedFetch(responses: ReadonlyArray<RecordedResponse>) {
56 const calls: Array<readonly [RequestInfo | URL, RequestInit]> = [];
57 let responseIndex = 0;
58 const fetchFn = ((input, init) => {
59 calls.push([input, init ?? {}]);
60 const response = responses[responseIndex++];
61 return response === undefined
62 ? Promise.reject(new Error(`Unexpected fetch call to ${String(input)}`))
63 : Promise.resolve(typeof response === "function" ? response(init ?? {}) : response);
64 }) satisfies typeof fetch;
65 return { calls, fetchFn };
66}
67
68const websocketTicket = (ticket: string) =>
69 Response.json({
70 ticket,
71 expiresAt: "2026-06-06T01:00:00.000Z",
72 });
73
74const accessToken = (token: string, scope = AuthStandardClientScopes.join(" ")) =>
75 Response.json({
76 access_token: token,
77 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
78 token_type: "DPoP",
79 expires_in: 3_600,
80 scope,
81 });
82
83const authInvalid = () =>
84 Response.json(
85 {
86 _tag: "EnvironmentAuthInvalidError",
87 code: "auth_invalid",
88 reason: "invalid_credential",
89 traceId: "trace-auth-invalid",
90 },
91 { status: 401 },
92 );
93
94const persistedToken = (
95 input: {
96 readonly environmentId?: EnvironmentId;
97 readonly accessToken?: string;
98 readonly expiresAtEpochMs?: number;
99 readonly dpopThumbprint?: string;
100 } = {},
101) =>
102 new TokenStore.RemoteDpopAccessToken({
103 environmentId: input.environmentId ?? ENVIRONMENT_ID,
104 accountId: "account-1",
105 label: DESCRIPTOR.label,
106 endpoint: ENDPOINT,
107 accessToken: input.accessToken ?? "cached-access-token",
108 expiresAtEpochMs: input.expiresAtEpochMs ?? Number.MAX_SAFE_INTEGER,
109 dpopThumbprint: input.dpopThumbprint ?? "thumbprint-1",
110 });
111
112const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* (input: {
113 readonly initialToken?: TokenStore.RemoteDpopAccessToken;
114 readonly responses: ReadonlyArray<RecordedResponse>;
115 readonly bootstrap?: RelayEnvironmentConnectResponse;
116 readonly beforeBootstrap?: Effect.Effect<void, ManagedRelay.ManagedRelayClientError>;
117 readonly beforePut?: Effect.Effect<void>;
118 readonly clerkToken?: ClientCapabilities.CloudSession["Service"]["clerkToken"];
119}) {
120 const tokens = yield* Ref.make(
121 new Map(
122 input.initialToken === undefined
123 ? []
124 : [[input.initialToken.environmentId, input.initialToken]],
125 ),
126 );
127 const bootstrapCalls = yield* Ref.make(0);
128 const relayInputs = yield* Ref.make<
129 ReadonlyArray<Parameters<ManagedRelay.ManagedRelayClient["Service"]["connectEnvironment"]>[0]>
130 >([]);
131 const session = yield* Ref.make<Option.Option<ClientCapabilities.CloudSessionIdentity>>(
132 Option.some({ accountId: "account-1" }),
133 );
134 const thumbprint = yield* Ref.make("thumbprint-1");
135 const tokenReads = yield* Queue.unbounded<EnvironmentId>();
136 const proofInputs = yield* Ref.make<
137 ReadonlyArray<{
138 readonly method: string;
139 readonly url: string;
140 readonly accessToken?: string;
141 }>
142 >([]);
143 const fetch = recordedFetch(input.responses);
144
145 const tokenStore = TokenStore.RemoteDpopAccessTokenStore.of({
146 get: (environmentId) =>
147 Ref.get(tokens).pipe(
148 Effect.map((current) => Option.fromUndefinedOr(current.get(environmentId))),
149 Effect.tap(() => Queue.offer(tokenReads, environmentId)),
150 ),
151 put: (token) =>
152 (input.beforePut ?? Effect.void).pipe(
153 Effect.andThen(
154 Ref.update(tokens, (current) => {
155 const next = new Map(current);
156 next.set(token.environmentId, token);
157 return next;
158 }),
159 ),
160 ),
161 remove: (environmentId) =>
162 Ref.update(tokens, (current) => {
163 const next = new Map(current);
164 next.delete(environmentId);
165 return next;
166 }),
167 });
168 const signer = ManagedRelay.ManagedRelayDpopSigner.of({
169 thumbprint: Ref.get(thumbprint),
170 createProof: (proofInput) =>
171 Ref.update(proofInputs, (current) => [...current, proofInput]).pipe(
172 Effect.as(`proof:${proofInput.url}`),
173 ),
174 });
175 const unexpected = () => Effect.die("Unexpected relay request");
176 const relay = ManagedRelay.ManagedRelayClient.of({
177 relayUrl: "https://relay.example.test",
178 listEnvironments: unexpected,
179 listDevices: unexpected,
180 createEnvironmentLinkChallenge: unexpected,
181 linkEnvironment: unexpected,
182 unlinkEnvironment: unexpected,
183 getEnvironmentStatus: unexpected,
184 connectEnvironment: (request) =>
185 Effect.gen(function* () {
186 yield* Ref.update(bootstrapCalls, (count) => count + 1);
187 yield* Ref.update(relayInputs, (current) => [...current, request]);
188 yield* input.beforeBootstrap ?? Effect.void;
189 return input.bootstrap ?? BOOTSTRAP;
190 }),
191 registerDevice: unexpected,
192 unregisterDevice: unexpected,
193 registerLiveActivity: unexpected,
194 getAgentActivitySnapshot: unexpected,
195 resetTokenCache: Effect.void,
196 });
197 const layer = RemoteEnvironmentAuthorization.layer.pipe(
198 Layer.provide(
199 Layer.mergeAll(
200 RpcHttp.layerRemoteHttpClient(fetch.fetchFn),
201 Layer.succeed(ManagedRelay.ManagedRelayDpopSigner, signer),
202 Layer.succeed(ManagedRelay.ManagedRelayClient, relay),
203 Layer.succeed(ClientCapabilities.CloudSession, {
204 identity: Ref.get(session),
205 clerkToken: input.clerkToken ?? Effect.succeed("clerk-session"),
206 }),
207 Layer.succeed(ClientCapabilities.RelayDeviceIdentity, {
208 deviceId: Effect.succeedSome("device-1"),
209 }),
210 Layer.succeed(TokenStore.RemoteDpopAccessTokenStore, tokenStore),
211 Layer.succeed(
212 ClientCapabilities.ClientPresentation,
213 ClientCapabilities.ClientPresentation.of({
214 metadata: {
215 label: "T3 Code Test",
216 deviceType: "mobile",
217 os: "test",
218 },
219 }),
220 ),
221 ),
222 ),
223 );
224 return {
225 layer,
226 tokens,
227 bootstrapCalls,
228 proofInputs,
229 fetch,
230 relayInputs,
231 session,
232 thumbprint,
233 tokenReads,
234 };
235});
236
237describe("RemoteEnvironmentAuthorization", () => {
238 it.effect("reuses a validated bearer descriptor while issuing fresh websocket tickets", () =>
239 Effect.gen(function* () {
240 const harness = yield* makeHarness({
241 responses: [
242 Response.json(DESCRIPTOR),
243 websocketTicket("first-ticket"),
244 websocketTicket("second-ticket"),
245 ],
246 });
247
248 const [first, second] = yield* Effect.gen(function* () {
249 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
250 const authorize = () =>
251 remote.authorizeBearer({
252 expectedEnvironmentId: ENVIRONMENT_ID,
253 httpBaseUrl: ENDPOINT.httpBaseUrl,
254 wsBaseUrl: ENDPOINT.wsBaseUrl,
255 bearerToken: "bearer-token",
256 connectionMethod: "direct",
257 });
258 return [yield* authorize(), yield* authorize()] as const;
259 }).pipe(Effect.provide(harness.layer));
260
261 expect(first.socketUrl).toContain("wsTicket=first-ticket");
262 expect(second.socketUrl).toContain("wsTicket=second-ticket");
263 expect(
264 harness.fetch.calls.filter(([url]) => String(url).endsWith("/.well-known/t3/environment")),
265 ).toHaveLength(1);
266 expect(
267 harness.fetch.calls.filter(([url]) => String(url).endsWith("/api/auth/websocket-ticket")),
268 ).toHaveLength(2);
269 }),
270 );
271
272 it.effect("revalidates a bearer descriptor after the cache expires", () =>
273 Effect.gen(function* () {
274 const reassignedEnvironmentId = EnvironmentId.make("environment-2");
275 const harness = yield* makeHarness({
276 responses: [
277 Response.json(DESCRIPTOR),
278 websocketTicket("first-ticket"),
279 Response.json({
280 ...DESCRIPTOR,
281 environmentId: reassignedEnvironmentId,
282 }),
283 ],
284 });
285
286 const failure = yield* Effect.gen(function* () {
287 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
288 const authorize = () =>
289 remote.authorizeBearer({
290 expectedEnvironmentId: ENVIRONMENT_ID,
291 httpBaseUrl: ENDPOINT.httpBaseUrl,
292 wsBaseUrl: ENDPOINT.wsBaseUrl,
293 bearerToken: "bearer-token",
294 connectionMethod: "direct",
295 });
296
297 yield* authorize();
298 yield* TestClock.adjust("10 seconds");
299 return yield* authorize().pipe(Effect.flip);
300 }).pipe(Effect.provide(Layer.merge(harness.layer, TestClock.layer())));
301
302 expect(failure).toEqual(
303 expect.objectContaining({
304 _tag: "ConnectionBlockedError",
305 reason: "configuration",
306 detail: `Connected environment ${reassignedEnvironmentId} does not match ${ENVIRONMENT_ID}.`,
307 }),
308 );
309 expect(
310 harness.fetch.calls.filter(([url]) => String(url).endsWith("/.well-known/t3/environment")),
311 ).toHaveLength(2);
312 }),
313 );
314
315 it.effect("reuses a same-account persisted environment token without contacting the relay", () =>
316 Effect.gen(function* () {
317 const cached = new TokenStore.RemoteDpopAccessToken({
318 environmentId: ENVIRONMENT_ID,
319 accountId: "account-1",
320 label: DESCRIPTOR.label,
321 endpoint: ENDPOINT,
322 accessToken: "cached-access-token",
323 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
324 dpopThumbprint: "thumbprint-1",
325 });
326 const harness = yield* makeHarness({
327 initialToken: cached,
328 responses: [websocketTicket("cached-ticket")],
329 });
330
331 const authorized = yield* Effect.gen(function* () {
332 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
333 return yield* remote.authorizeDpop({
334 expectedEnvironmentId: ENVIRONMENT_ID,
335 });
336 }).pipe(Effect.provide(harness.layer));
337
338 expect(authorized.socketUrl).toContain("wsTicket=cached-ticket");
339 expect(authorized.socketUrl).toContain("connectionMethod=relay");
340 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(0);
341 expect(harness.fetch.calls).toHaveLength(1);
342 expect(String(harness.fetch.calls[0]?.[0])).toBe(
343 "https://environment.example.test/api/auth/websocket-ticket",
344 );
345 }),
346 );
347
348 it.effect("uses the T3 Connect token on a learned direct address without the relay", () =>
349 Effect.gen(function* () {
350 const cached = new TokenStore.RemoteDpopAccessToken({
351 environmentId: ENVIRONMENT_ID,
352 accountId: "account-1",
353 label: DESCRIPTOR.label,
354 endpoint: ENDPOINT,
355 accessToken: "cached-access-token",
356 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
357 dpopThumbprint: "thumbprint-1",
358 });
359 const harness = yield* makeHarness({
360 initialToken: cached,
361 responses: [Response.json(DESCRIPTOR), websocketTicket("lan-ticket")],
362 });
363
364 const authorized = yield* Effect.gen(function* () {
365 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
366 return yield* remote.authorizeDpop({
367 expectedEnvironmentId: ENVIRONMENT_ID,
368 directEndpoint: {
369 httpBaseUrl: "http://192.168.1.10:3773/",
370 wsBaseUrl: "ws://192.168.1.10:3773/",
371 },
372 });
373 }).pipe(Effect.provide(harness.layer));
374
375 expect(authorized.httpBaseUrl).toBe("http://192.168.1.10:3773/");
376 expect(authorized.socketUrl).toMatch(/^ws:\/\/192\.168\.1\.10:3773\/ws\?/);
377 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(0);
378 expect(harness.fetch.calls.map(([url]) => String(url))).toEqual([
379 "http://192.168.1.10:3773/.well-known/t3/environment",
380 "http://192.168.1.10:3773/api/auth/websocket-ticket",
381 ]);
382 }),
383 );
384
385 it.effect("sends no token to a direct address that answers as another machine", () =>
386 Effect.gen(function* () {
387 const harness = yield* makeHarness({
388 initialToken: persistedToken({ accessToken: "cached-access-token" }),
389 responses: [
390 Response.json({ ...DESCRIPTOR, environmentId: EnvironmentId.make("someone-else") }),
391 ],
392 });
393
394 const error = yield* Effect.gen(function* () {
395 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
396 return yield* remote
397 .authorizeDpop({
398 expectedEnvironmentId: ENVIRONMENT_ID,
399 directEndpoint: {
400 httpBaseUrl: "http://192.168.1.10:3773/",
401 wsBaseUrl: "ws://192.168.1.10:3773/",
402 },
403 })
404 .pipe(Effect.flip);
405 }).pipe(Effect.provide(harness.layer));
406
407 expect(error).toMatchObject({ _tag: "ConnectionBlockedError", reason: "configuration" });
408 expect(harness.fetch.calls.map(([url]) => String(url))).toEqual([
409 "http://192.168.1.10:3773/.well-known/t3/environment",
410 ]);
411 // The token stays saved for the T3 Connect route.
412 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)?.accessToken).toBe(
413 "cached-access-token",
414 );
415 }),
416 );
417
418 it.effect("replaces a token a learned direct address rejects", () =>
419 Effect.gen(function* () {
420 const cached = new TokenStore.RemoteDpopAccessToken({
421 environmentId: ENVIRONMENT_ID,
422 accountId: "account-1",
423 label: DESCRIPTOR.label,
424 endpoint: ENDPOINT,
425 accessToken: "revoked-access-token",
426 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
427 dpopThumbprint: "thumbprint-1",
428 });
429 const harness = yield* makeHarness({
430 initialToken: cached,
431 responses: [
432 Response.json(DESCRIPTOR),
433 authInvalid(),
434 Response.json(DESCRIPTOR),
435 accessToken("replacement-access-token"),
436 websocketTicket("lan-ticket"),
437 ],
438 });
439
440 const authorized = yield* Effect.gen(function* () {
441 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
442 return yield* remote.authorizeDpop({
443 expectedEnvironmentId: ENVIRONMENT_ID,
444 directEndpoint: {
445 httpBaseUrl: "http://192.168.1.10:3773/",
446 wsBaseUrl: "ws://192.168.1.10:3773/",
447 },
448 });
449 }).pipe(Effect.provide(harness.layer));
450
451 expect(authorized.socketUrl).toContain("wsTicket=lan-ticket");
452 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
453 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)?.accessToken).toBe(
454 "replacement-access-token",
455 );
456 }),
457 );
458
459 it.effect("refreshes and persists an expired environment token", () =>
460 Effect.gen(function* () {
461 const expired = new TokenStore.RemoteDpopAccessToken({
462 environmentId: ENVIRONMENT_ID,
463 accountId: "account-1",
464 label: DESCRIPTOR.label,
465 endpoint: ENDPOINT,
466 accessToken: "expired-access-token",
467 expiresAtEpochMs: 0,
468 dpopThumbprint: "thumbprint-1",
469 });
470 const harness = yield* makeHarness({
471 initialToken: expired,
472 responses: [
473 Response.json(DESCRIPTOR),
474 accessToken("fresh-access-token"),
475 websocketTicket("fresh-ticket"),
476 ],
477 });
478
479 const authorized = yield* Effect.gen(function* () {
480 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
481 return yield* remote.authorizeDpop({
482 expectedEnvironmentId: ENVIRONMENT_ID,
483 });
484 }).pipe(Effect.provide(harness.layer));
485
486 expect(authorized.socketUrl).toContain("wsTicket=fresh-ticket");
487 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
488 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)).toEqual(
489 expect.objectContaining({
490 accessToken: "fresh-access-token",
491 dpopThumbprint: "thumbprint-1",
492 }),
493 );
494 expect(harness.fetch.calls).toHaveLength(3);
495 }),
496 );
497
498 it.effect.each([
499 { name: "read-only", grantScopes: ["orchestration:read"] },
500 { name: "administrative", grantScopes: AuthAdministrativeScopes },
501 ] as const)(
502 "inherits $name pairing grant scopes for websocket authorization and HTTP renewal",
503 ({ grantScopes }) =>
504 Effect.gen(function* () {
505 const grantedScope = grantScopes.join(" ");
506 let exchangeCount = 0;
507 const tokenFields = (init: RequestInit) =>
508 new URLSearchParams(
509 init.body instanceof Uint8Array
510 ? new TextDecoder().decode(init.body)
511 : String(init.body),
512 );
513 const exchangeGrant = (init: RequestInit) => {
514 const fields = tokenFields(init);
515 expect(fields.get("scope")).toBeNull();
516 const scope = fields.get("scope") ?? grantedScope;
517 const scopes = scope.split(" ");
518 if (scopes.some((requested) => !grantScopes.some((grant) => grant === requested))) {
519 return authInvalid();
520 }
521 return accessToken(`access-token:${++exchangeCount}:${scopes.join(",")}`, scope);
522 };
523 const harness = yield* makeHarness({
524 responses: [
525 Response.json(DESCRIPTOR),
526 exchangeGrant,
527 websocketTicket("granted-ticket"),
528 Response.json(DESCRIPTOR),
529 exchangeGrant,
530 ],
531 });
532
533 const [authorized, refreshed] = yield* Effect.gen(function* () {
534 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
535 const first = yield* remote.authorizeDpop({
536 expectedEnvironmentId: ENVIRONMENT_ID,
537 });
538 yield* TestClock.adjust("1 hour");
539 const refreshed = yield* remote.authorizeDpopHttp({
540 expectedEnvironmentId: ENVIRONMENT_ID,
541 });
542 return [first, refreshed] as const;
543 }).pipe(Effect.provide(Layer.merge(harness.layer, TestClock.layer())));
544
545 expect(authorized.socketUrl).toContain("wsTicket=granted-ticket");
546 expect(authorized.httpAuthorization).toMatchObject({
547 _tag: "Dpop",
548 accessToken: `access-token:1:${grantScopes.join(",")}`,
549 });
550 expect(refreshed.httpAuthorization).toMatchObject({
551 _tag: "Dpop",
552 accessToken: `access-token:2:${grantScopes.join(",")}`,
553 });
554 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)).toMatchObject({
555 accessToken: `access-token:2:${grantScopes.join(",")}`,
556 dpopThumbprint: "thumbprint-1",
557 });
558 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(2);
559 const exchanges = harness.fetch.calls.filter(([url]) =>
560 String(url).endsWith("/oauth/token"),
561 );
562 expect(exchanges).toHaveLength(2);
563 for (const [, init] of exchanges) {
564 expect(Object.fromEntries(tokenFields(init))).toMatchObject({
565 subject_token: BOOTSTRAP.credential,
566 client_label: "T3 Code Test",
567 client_device_type: "mobile",
568 client_os: "test",
569 });
570 }
571 }),
572 );
573
574 it.effect("evicts an auth-invalid cached token and obtains a fresh bootstrap", () =>
575 Effect.gen(function* () {
576 const cached = new TokenStore.RemoteDpopAccessToken({
577 environmentId: ENVIRONMENT_ID,
578 accountId: "account-1",
579 label: DESCRIPTOR.label,
580 endpoint: ENDPOINT,
581 accessToken: "invalid-access-token",
582 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
583 dpopThumbprint: "thumbprint-1",
584 });
585 const harness = yield* makeHarness({
586 initialToken: cached,
587 responses: [
588 authInvalid(),
589 Response.json(DESCRIPTOR),
590 accessToken("replacement-access-token"),
591 websocketTicket("replacement-ticket"),
592 ],
593 });
594
595 const authorized = yield* Effect.gen(function* () {
596 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
597 return yield* remote.authorizeDpop({
598 expectedEnvironmentId: ENVIRONMENT_ID,
599 });
600 }).pipe(Effect.provide(harness.layer));
601
602 expect(authorized.socketUrl).toContain("wsTicket=replacement-ticket");
603 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
604 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)).toEqual(
605 expect.objectContaining({
606 accessToken: "replacement-access-token",
607 }),
608 );
609 expect(harness.fetch.calls).toHaveLength(4);
610 }),
611 );
612
613 it.effect("presents clock skew as one possible cause for a generic DPoP rejection", () =>
614 Effect.gen(function* () {
615 const harness = yield* makeHarness({
616 responses: [Response.json(DESCRIPTOR), authInvalid()],
617 });
618
619 const failure = yield* Effect.gen(function* () {
620 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
621 return yield* remote.authorizeDpop({
622 expectedEnvironmentId: ENVIRONMENT_ID,
623 });
624 }).pipe(Effect.provide(harness.layer), Effect.flip);
625
626 expect(failure).toMatchObject({
627 _tag: "ConnectionBlockedError",
628 reason: "authentication",
629 detail: `The environment credential is invalid. ${DPOP_UNKNOWN_HINT}`,
630 traceId: "trace-auth-invalid",
631 });
632 }),
633 );
634
635 it.effect("refreshes a cached endpoint after its first transient failure", () =>
636 Effect.gen(function* () {
637 const cached = new TokenStore.RemoteDpopAccessToken({
638 environmentId: ENVIRONMENT_ID,
639 accountId: "account-1",
640 label: DESCRIPTOR.label,
641 endpoint: ENDPOINT,
642 accessToken: "cached-access-token",
643 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
644 dpopThumbprint: "thumbprint-1",
645 });
646 const harness = yield* makeHarness({
647 initialToken: cached,
648 responses: [
649 new Response("endpoint unavailable", { status: 503 }),
650 Response.json(DESCRIPTOR),
651 accessToken("replacement-access-token"),
652 websocketTicket("replacement-ticket"),
653 ],
654 });
655
656 const authorized = yield* Effect.gen(function* () {
657 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
658 return yield* remote.authorizeDpop({
659 expectedEnvironmentId: ENVIRONMENT_ID,
660 });
661 }).pipe(Effect.provide(harness.layer));
662
663 expect(authorized.socketUrl).toContain("wsTicket=replacement-ticket");
664 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
665 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)).toEqual(
666 expect.objectContaining({
667 accessToken: "replacement-access-token",
668 }),
669 );
670 expect(harness.fetch.calls).toHaveLength(4);
671 }),
672 );
673
674 it.effect("removes a newly refreshed token when websocket ticket authorization fails", () =>
675 Effect.gen(function* () {
676 const harness = yield* makeHarness({
677 responses: [
678 Response.json(DESCRIPTOR),
679 accessToken("unusable-access-token"),
680 new Response("endpoint unavailable", { status: 503 }),
681 ],
682 });
683
684 yield* Effect.gen(function* () {
685 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
686 return yield* remote.authorizeDpop({
687 expectedEnvironmentId: ENVIRONMENT_ID,
688 });
689 }).pipe(Effect.provide(harness.layer), Effect.flip);
690
691 expect((yield* Ref.get(harness.tokens)).has(ENVIRONMENT_ID)).toBe(false);
692 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
693 expect(harness.fetch.calls).toHaveLength(3);
694 }),
695 );
696
697 it.effect("uses the current cached HTTP token without a relay request or websocket ticket", () =>
698 Effect.gen(function* () {
699 const harness = yield* makeHarness({ initialToken: persistedToken(), responses: [] });
700 const authorized = yield* Effect.gen(function* () {
701 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
702 return yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
703 }).pipe(Effect.provide(harness.layer));
704
705 expect(authorized).toEqual({
706 environmentId: ENVIRONMENT_ID,
707 label: DESCRIPTOR.label,
708 httpBaseUrl: ENDPOINT.httpBaseUrl,
709 httpAuthorization: {
710 _tag: "Dpop",
711 accessToken: "cached-access-token",
712 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
713 },
714 });
715 expect(harness.fetch.calls).toHaveLength(0);
716 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(0);
717 expect(yield* Ref.get(harness.proofInputs)).toEqual([]);
718 }),
719 );
720
721 it.effect(
722 "renews HTTP credentials near expiry through the current cloud account and device",
723 () =>
724 Effect.gen(function* () {
725 const now = yield* Clock.currentTimeMillis;
726 const harness = yield* makeHarness({
727 initialToken: persistedToken({ expiresAtEpochMs: now + 61_000 }),
728 responses: [Response.json(DESCRIPTOR), accessToken("fresh-access-token")],
729 });
730 yield* Effect.gen(function* () {
731 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
732 const before = yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
733 expect(before.httpAuthorization.accessToken).toBe("cached-access-token");
734 yield* TestClock.adjust("1 second");
735 const after = yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
736 expect(after.httpAuthorization.accessToken).toBe("fresh-access-token");
737 expect(after.httpAuthorization.expiresAtEpochMs).toBe(now + 3_601_000);
738 }).pipe(Effect.provide(harness.layer));
739
740 expect(yield* Ref.get(harness.relayInputs)).toEqual([
741 {
742 environmentId: ENVIRONMENT_ID,
743 clerkToken: "clerk-session",
744 scopes: [RelayEnvironmentConnectScope],
745 deviceId: "device-1",
746 },
747 ]);
748 expect(harness.fetch.calls.map(([url]) => String(url))).toEqual([
749 `${ENDPOINT.httpBaseUrl}/.well-known/t3/environment`,
750 `${ENDPOINT.httpBaseUrl}/oauth/token`,
751 ]);
752 expect(yield* Ref.get(harness.proofInputs)).toEqual([
753 {
754 method: "POST",
755 url: `${ENDPOINT.httpBaseUrl}/oauth/token`,
756 },
757 ]);
758 }),
759 );
760
761 it.effect("does not evict a replacement token when a late request rejects the old one", () =>
762 Effect.gen(function* () {
763 const harness = yield* makeHarness({
764 initialToken: persistedToken(),
765 responses: [Response.json(DESCRIPTOR), accessToken("replacement-token")],
766 });
767 yield* Effect.gen(function* () {
768 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
769 const initial = yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
770 const retry = {
771 expectedEnvironmentId: ENVIRONMENT_ID,
772 rejectedAccessToken: initial.httpAuthorization.accessToken,
773 };
774 const first = yield* remote.authorizeDpopHttp(retry);
775 const late = yield* remote.authorizeDpopHttp(retry);
776 expect(first.httpAuthorization.accessToken).toBe("replacement-token");
777 expect(late).toEqual(first);
778 }).pipe(Effect.provide(harness.layer));
779
780 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
781 expect(harness.fetch.calls).toHaveLength(2);
782 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)?.accessToken).toBe(
783 "replacement-token",
784 );
785 }),
786 );
787
788 it.effect("shares one in-flight renewal between concurrent HTTP requests", () =>
789 Effect.gen(function* () {
790 const started = yield* Deferred.make<void>();
791 const release = yield* Deferred.make<void>();
792 const harness = yield* makeHarness({
793 initialToken: persistedToken({ expiresAtEpochMs: 0 }),
794 beforeBootstrap: Deferred.succeed(started, undefined).pipe(
795 Effect.andThen(Deferred.await(release)),
796 ),
797 responses: [Response.json(DESCRIPTOR), accessToken("shared-token")],
798 });
799 yield* Effect.gen(function* () {
800 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
801 const requests = yield* Effect.all(
802 Array.from({ length: 6 }, () =>
803 remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID }),
804 ),
805 { concurrency: "unbounded" },
806 ).pipe(Effect.forkChild);
807 yield* Queue.takeN(harness.tokenReads, 6);
808 yield* Deferred.await(started);
809 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
810 yield* Deferred.succeed(release, undefined);
811 const results = yield* Fiber.join(requests);
812 expect(results.map((result) => result.httpAuthorization.accessToken)).toEqual(
813 Array(6).fill("shared-token"),
814 );
815 }).pipe(Effect.provide(harness.layer));
816
817 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
818 expect(harness.fetch.calls).toHaveLength(2);
819 }),
820 );
821
822 it.effect(
823 "shares a failed renewal and allows a later attempt instead of caching its failure",
824 () =>
825 Effect.gen(function* () {
826 const release = yield* Deferred.make<void, ManagedRelay.ManagedRelayClientError>();
827 const failure = new ManagedRelay.ManagedRelayRequestTimeoutError({
828 activity: "Relay environment connection",
829 timeoutMs: ManagedRelay.MANAGED_RELAY_REQUEST_TIMEOUT_MS,
830 traceId: null,
831 });
832 const harness = yield* makeHarness({
833 responses: [],
834 beforeBootstrap: Deferred.await(release),
835 });
836 yield* Effect.gen(function* () {
837 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
838 const requests = yield* Effect.all(
839 Array.from({ length: 6 }, () =>
840 remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID }).pipe(Effect.flip),
841 ),
842 { concurrency: "unbounded" },
843 ).pipe(Effect.forkChild);
844 yield* Queue.takeN(harness.tokenReads, 6);
845 yield* Deferred.fail(release, failure);
846 const failures = yield* Fiber.join(requests);
847 expect(failures).toEqual(
848 Array(6).fill(
849 expect.objectContaining({
850 _tag: "ConnectionTransientError",
851 reason: "timeout",
852 }),
853 ),
854 );
855 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
856 yield* remote
857 .authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID })
858 .pipe(Effect.flip);
859 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(2);
860 }).pipe(Effect.provide(harness.layer));
861 expect(harness.fetch.calls).toHaveLength(0);
862 }),
863 );
864
865 it.effect("does not block another environment behind a pending renewal", () =>
866 Effect.gen(function* () {
867 const started = yield* Deferred.make<void>();
868 const release = yield* Deferred.make<void>();
869 const otherEnvironmentId = EnvironmentId.make("environment-2");
870 const harness = yield* makeHarness({
871 initialToken: persistedToken({
872 environmentId: otherEnvironmentId,
873 accessToken: "other-token",
874 }),
875 beforeBootstrap: Deferred.succeed(started, undefined).pipe(
876 Effect.andThen(Deferred.await(release)),
877 ),
878 responses: [Response.json(DESCRIPTOR), accessToken("fresh-token")],
879 });
880 yield* Effect.gen(function* () {
881 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
882 const pending = yield* remote
883 .authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID })
884 .pipe(Effect.forkChild);
885 yield* Deferred.await(started);
886 const other = yield* remote.authorizeDpopHttp({
887 expectedEnvironmentId: otherEnvironmentId,
888 });
889 expect(other.httpAuthorization.accessToken).toBe("other-token");
890 yield* Deferred.succeed(release, undefined);
891 yield* Fiber.join(pending);
892 }).pipe(Effect.provide(harness.layer));
893 }),
894 );
895
896 it.effect("does not return or persist credentials after logout during renewal", () =>
897 Effect.gen(function* () {
898 const started = yield* Deferred.make<void>();
899 const release = yield* Deferred.make<void>();
900 const harness = yield* makeHarness({
901 beforeBootstrap: Deferred.succeed(started, undefined).pipe(
902 Effect.andThen(Deferred.await(release)),
903 ),
904 responses: [],
905 });
906 yield* Effect.gen(function* () {
907 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
908 const pending = yield* remote
909 .authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID })
910 .pipe(Effect.flip, Effect.forkChild);
911 yield* Deferred.await(started);
912 yield* Ref.set(harness.session, Option.none());
913 yield* Deferred.succeed(release, undefined);
914 expect(yield* Fiber.join(pending)).toMatchObject({
915 _tag: "ConnectionBlockedError",
916 reason: "authentication",
917 });
918 }).pipe(Effect.provide(harness.layer));
919 expect((yield* Ref.get(harness.tokens)).size).toBe(0);
920 expect(harness.fetch.calls).toHaveLength(0);
921 }),
922 );
923
924 it.effect("removes an obsolete credential if logout overlaps the persistent write", () =>
925 Effect.gen(function* () {
926 const started = yield* Deferred.make<void>();
927 const release = yield* Deferred.make<void>();
928 const harness = yield* makeHarness({
929 beforePut: Deferred.succeed(started, undefined).pipe(
930 Effect.andThen(Deferred.await(release)),
931 ),
932 responses: [Response.json(DESCRIPTOR), accessToken("obsolete-token")],
933 });
934 yield* Effect.gen(function* () {
935 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
936 const pending = yield* remote
937 .authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID })
938 .pipe(Effect.flip, Effect.forkChild);
939 yield* Deferred.await(started);
940 yield* Ref.set(harness.session, Option.none());
941 yield* Deferred.succeed(release, undefined);
942 expect(yield* Fiber.join(pending)).toMatchObject({
943 _tag: "ConnectionBlockedError",
944 reason: "authentication",
945 });
946 }).pipe(Effect.provide(harness.layer));
947 expect((yield* Ref.get(harness.tokens)).size).toBe(0);
948 }),
949 );
950
951 it.effect(
952 "renews another account's persisted token after the authorization service restarts",
953 () =>
954 Effect.gen(function* () {
955 const harness = yield* makeHarness({
956 initialToken: persistedToken(),
957 responses: [Response.json(DESCRIPTOR), accessToken("account-2-token")],
958 });
959 const authorize = Effect.gen(function* () {
960 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
961 return yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
962 }).pipe(Effect.provide(harness.layer));
963
964 expect((yield* authorize).httpAuthorization.accessToken).toBe("cached-access-token");
965 yield* Ref.set(harness.session, Option.some({ accountId: "account-2" }));
966 expect((yield* authorize).httpAuthorization.accessToken).toBe("account-2-token");
967 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)?.accountId).toBe("account-2");
968
969 yield* Ref.set(harness.session, Option.some({ accountId: "account-2" }));
970 expect((yield* authorize).httpAuthorization.accessToken).toBe("account-2-token");
971 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
972 expect(harness.fetch.calls).toHaveLength(2);
973 }),
974 );
975
976 it.effect("renews a legacy unowned token once and reuses its account-bound replacement", () =>
977 Effect.gen(function* () {
978 const legacy = new TokenStore.RemoteDpopAccessToken({
979 environmentId: ENVIRONMENT_ID,
980 label: DESCRIPTOR.label,
981 endpoint: ENDPOINT,
982 accessToken: "legacy-access-token",
983 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
984 dpopThumbprint: "thumbprint-1",
985 });
986 const harness = yield* makeHarness({
987 initialToken: legacy,
988 responses: [Response.json(DESCRIPTOR), accessToken("account-bound-token")],
989 });
990 const authorize = Effect.gen(function* () {
991 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
992 return yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
993 }).pipe(Effect.provide(harness.layer));
994
995 expect((yield* authorize).httpAuthorization.accessToken).toBe("account-bound-token");
996 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)?.accountId).toBe("account-1");
997 expect((yield* authorize).httpAuthorization.accessToken).toBe("account-bound-token");
998 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
999 expect(harness.fetch.calls).toHaveLength(2);
1000 }),
1001 );
1002
1003 it.effect("requires fresh credentials after signing back into the same account", () =>
1004 Effect.gen(function* () {
1005 const harness = yield* makeHarness({
1006 initialToken: persistedToken(),
1007 responses: [Response.json(DESCRIPTOR), accessToken("new-session-token")],
1008 });
1009 yield* Effect.gen(function* () {
1010 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
1011 yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
1012 yield* Ref.set(harness.session, Option.none());
1013 const signedOut = yield* remote
1014 .authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID })
1015 .pipe(Effect.flip);
1016 expect(signedOut).toMatchObject({
1017 _tag: "ConnectionBlockedError",
1018 reason: "authentication",
1019 });
1020 yield* Ref.set(harness.session, Option.some({ accountId: "account-1" }));
1021 const signedIn = yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
1022 expect(signedIn.httpAuthorization.accessToken).toBe("new-session-token");
1023 }).pipe(Effect.provide(harness.layer));
1024 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
1025 }),
1026 );
1027
1028 it.effect("renews a cached credential bound to a different signing key", () =>
1029 Effect.gen(function* () {
1030 const harness = yield* makeHarness({
1031 initialToken: persistedToken(),
1032 responses: [Response.json(DESCRIPTOR), accessToken("new-key-token")],
1033 });
1034 yield* Ref.set(harness.thumbprint, "thumbprint-2");
1035 yield* Effect.gen(function* () {
1036 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
1037 const authorized = yield* remote.authorizeDpopHttp({
1038 expectedEnvironmentId: ENVIRONMENT_ID,
1039 });
1040 expect(authorized.httpAuthorization.accessToken).toBe("new-key-token");
1041 }).pipe(Effect.provide(harness.layer));
1042 expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)?.dpopThumbprint).toBe(
1043 "thumbprint-2",
1044 );
1045 }),
1046 );
1047
1048 it.effect("rejects a relay bootstrap for a different environment before contacting it", () =>
1049 Effect.gen(function* () {
1050 const harness = yield* makeHarness({
1051 responses: [],
1052 bootstrap: { ...BOOTSTRAP, environmentId: EnvironmentId.make("wrong-environment") },
1053 });
1054 const failure = yield* Effect.gen(function* () {
1055 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
1056 return yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
1057 }).pipe(Effect.provide(harness.layer), Effect.flip);
1058 expect(failure).toMatchObject({ _tag: "ConnectionBlockedError", reason: "configuration" });
1059 expect(harness.fetch.calls).toHaveLength(0);
1060 }),
1061 );
1062
1063 it.effect("keeps a shared renewal alive when one waiting request is interrupted", () =>
1064 Effect.gen(function* () {
1065 const started = yield* Deferred.make<void>();
1066 const release = yield* Deferred.make<void>();
1067 const harness = yield* makeHarness({
1068 beforeBootstrap: Deferred.succeed(started, undefined).pipe(
1069 Effect.andThen(Deferred.await(release)),
1070 ),
1071 responses: [Response.json(DESCRIPTOR), accessToken("fresh-token")],
1072 });
1073 yield* Effect.gen(function* () {
1074 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
1075 const pending = yield* remote
1076 .authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID })
1077 .pipe(Effect.forkChild);
1078 yield* Deferred.await(started);
1079 const other = yield* remote
1080 .authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID })
1081 .pipe(Effect.forkChild);
1082 yield* Queue.takeN(harness.tokenReads, 2);
1083 yield* Fiber.interrupt(pending);
1084 yield* Deferred.succeed(release, undefined);
1085 const authorized = yield* Fiber.join(other);
1086 expect(authorized.httpAuthorization.accessToken).toBe("fresh-token");
1087 const cached = yield* remote.authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID });
1088 expect(cached).toEqual(authorized);
1089 }).pipe(Effect.provide(harness.layer));
1090 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
1091 }),
1092 );
1093
1094 it.effect("times out a stalled cloud token read so later requests can renew again", () =>
1095 Effect.gen(function* () {
1096 const started = yield* Deferred.make<void>();
1097 const reads = yield* Ref.make(0);
1098 const harness = yield* makeHarness({
1099 clerkToken: Effect.gen(function* () {
1100 const read = yield* Ref.updateAndGet(reads, (value) => value + 1);
1101 if (read === 1) {
1102 yield* Deferred.succeed(started, undefined);
1103 return yield* Effect.never;
1104 }
1105 return "clerk-session";
1106 }),
1107 responses: [Response.json(DESCRIPTOR), accessToken("fresh-token")],
1108 });
1109 yield* Effect.gen(function* () {
1110 const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization;
1111 const pending = yield* remote
1112 .authorizeDpopHttp({ expectedEnvironmentId: ENVIRONMENT_ID })
1113 .pipe(Effect.flip, Effect.forkChild);
1114 yield* Deferred.await(started);
1115 yield* TestClock.adjust("30 seconds");
1116 expect(yield* Fiber.join(pending)).toMatchObject({
1117 _tag: "ConnectionTransientError",
1118 reason: "timeout",
1119 });
1120 const authorized = yield* remote.authorizeDpopHttp({
1121 expectedEnvironmentId: ENVIRONMENT_ID,
1122 });
1123 expect(authorized.httpAuthorization.accessToken).toBe("fresh-token");
1124 }).pipe(Effect.provide(harness.layer));
1125 expect(yield* Ref.get(reads)).toBe(2);
1126 expect(yield* Ref.get(harness.bootstrapCalls)).toBe(1);
1127 }),
1128 );
1129});