1import { EnvironmentId } from "@t3tools/contracts";
2import {
3 RelayEnvironmentConnectScope,
4 RelayEnvironmentStatusScope,
5} from "@t3tools/contracts/relay";
6import { describe, expect, it } from "@effect/vitest";
7import * as Duration from "effect/Duration";
8import * as Effect from "effect/Effect";
9import * as Fiber from "effect/Fiber";
10import * as Layer from "effect/Layer";
11import * as Tracer from "effect/Tracer";
12import * as Schema from "effect/Schema";
13import * as TestClock from "effect/testing/TestClock";
14
15import { RELAY_GATEWAY_TIMEOUT_MESSAGE, relayRequestFailureDetail } from "./errorPresentation.ts";
16import * as ManagedRelay from "./managedRelay.ts";
17import * as RpcHttp from "../rpc/http.ts";
18import { NETWORK_BLOCKING_HINT } from "../errors/network.ts";
19
20const encodeRelayError = Schema.encodeEffect(ManagedRelay.ManagedRelayClientError);
21const decodeRelayError = Schema.decodeUnknownEffect(ManagedRelay.ManagedRelayClientError);
22
23function layerManagedRelayTest(
24 fetchFn: typeof globalThis.fetch,
25 relayUrl = "https://relay.example.test",
26 accessTokenStore?: ManagedRelay.ManagedRelayAccessTokenStore,
27) {
28 const layerHttpClient = RpcHttp.layerRemoteHttpClient(fetchFn);
29 const layerSigner = Layer.succeed(
30 ManagedRelay.ManagedRelayDpopSigner,
31 ManagedRelay.ManagedRelayDpopSigner.of({
32 thumbprint: Effect.succeed("client-thumbprint"),
33 createProof: (input: ManagedRelay.ManagedRelayDpopProofInput) =>
34 Effect.succeed(`proof:${input.url}`),
35 }),
36 );
37 return ManagedRelay.layer({
38 relayUrl,
39 clientId: "t3-mobile",
40 ...(accessTokenStore ? { accessTokenStore } : {}),
41 }).pipe(Layer.provide(layerSigner), Layer.provide(layerHttpClient));
42}
43
44function clerkToken(subject: string, nonce: string): string {
45 const encode = (value: unknown) =>
46 btoa(JSON.stringify(value)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
47 return `${encode({ alg: "none" })}.${encode({ sub: subject, nonce })}.signature`;
48}
49
50describe("ManagedRelayClient", () => {
51 it.effect("owns tracing at service and implementation boundaries", () => {
52 const spanNames: Array<string> = [];
53 const tracer = Tracer.make({
54 span: (options) => {
55 const span = new Tracer.NativeSpan(options);
56 spanNames.push(span.name);
57 return span;
58 },
59 });
60 const fetchFn = ((input) => {
61 const url = String(input);
62 if (url.endsWith("/v1/client/dpop-token")) {
63 return Promise.resolve(
64 Response.json({
65 access_token: "relay-token",
66 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
67 token_type: "DPoP",
68 expires_in: 1_800,
69 scope: RelayEnvironmentStatusScope,
70 }),
71 );
72 }
73 return Promise.resolve(
74 Response.json({
75 environmentId: "env-1",
76 endpoint: {
77 httpBaseUrl: "https://desktop.example.test/",
78 wsBaseUrl: "wss://desktop.example.test/ws",
79 providerKind: "cloudflare_tunnel",
80 },
81 status: "online",
82 checkedAt: "2026-06-05T20:00:00.000Z",
83 descriptor: {
84 environmentId: "env-1",
85 label: "Desktop",
86 platform: { os: "darwin", arch: "arm64" },
87 serverVersion: "0.0.0-test",
88 capabilities: { repositoryIdentity: true },
89 },
90 }),
91 );
92 }) satisfies typeof globalThis.fetch;
93
94 return Effect.gen(function* () {
95 const relayClient = yield* ManagedRelay.ManagedRelayClient;
96 yield* relayClient.getEnvironmentStatus({
97 clerkToken: clerkToken("user-1", "session-1"),
98 scopes: [RelayEnvironmentStatusScope],
99 environmentId: EnvironmentId.make("env-1"),
100 });
101
102 expect(spanNames).toEqual(
103 expect.arrayContaining([
104 "clientRuntime.managedRelay.getEnvironmentStatus",
105 "clientRuntime.managedRelay.authorize",
106 "clientRuntime.managedRelay.obtainAccessToken",
107 "clientRuntime.managedRelay.tokenCacheCriticalSection",
108 "clientRuntime.managedRelay.exchangeAccessToken",
109 ]),
110 );
111 expect(spanNames).not.toEqual(
112 expect.arrayContaining([
113 "clientRuntime.managedRelay.createTokenExchangeProof",
114 "clientRuntime.managedRelay.exchangeAccessTokenRequest",
115 "clientRuntime.managedRelay.createRequestProof",
116 ]),
117 );
118 }).pipe(Effect.withTracer(tracer), Effect.provide(layerManagedRelayTest(fetchFn)));
119 });
120
121 it.effect("rejects unsafe relay URLs before sending credentials", () => {
122 let requestCount = 0;
123 const fetchFn = (() => {
124 requestCount += 1;
125 return Promise.resolve(Response.json({}));
126 }) satisfies typeof globalThis.fetch;
127
128 return Effect.gen(function* () {
129 const relayClient = yield* ManagedRelay.ManagedRelayClient;
130 const error = yield* relayClient
131 .listEnvironments({ clerkToken: "clerk-token" })
132 .pipe(Effect.flip);
133
134 expect(error).toMatchObject({
135 _tag: "ManagedRelayUrlInvalidError",
136 relayUrl: "http://relay.example.test",
137 message: "Relay URL must be a secure absolute HTTPS origin.",
138 });
139 expect(requestCount).toBe(0);
140 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, "http://relay.example.test")));
141 });
142
143 it.effect("reuses usable DPoP tokens and refreshes cleared or expiring cache entries", () => {
144 let tokenExchangeCount = 0;
145 const fetchFn = ((input) => {
146 const url = String(input);
147 if (url.endsWith("/v1/client/dpop-token")) {
148 tokenExchangeCount += 1;
149 return Promise.resolve(
150 Response.json({
151 access_token: `relay-token-${tokenExchangeCount}`,
152 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
153 token_type: "DPoP",
154 expires_in: 10,
155 scope: RelayEnvironmentStatusScope,
156 }),
157 );
158 }
159 return Promise.resolve(
160 Response.json({
161 environmentId: "env-1",
162 endpoint: {
163 httpBaseUrl: "https://desktop.example.test/",
164 wsBaseUrl: "wss://desktop.example.test/ws",
165 providerKind: "cloudflare_tunnel",
166 },
167 status: "online",
168 checkedAt: "2026-05-25T00:01:00.000Z",
169 descriptor: {
170 environmentId: "env-1",
171 label: "Desktop",
172 platform: { os: "darwin", arch: "arm64" },
173 serverVersion: "0.0.0-test",
174 capabilities: { repositoryIdentity: true },
175 },
176 }),
177 );
178 }) satisfies typeof globalThis.fetch;
179
180 return Effect.gen(function* () {
181 const relayClient = yield* ManagedRelay.ManagedRelayClient;
182 const statusInput = {
183 clerkToken: clerkToken("user-1", "session-1"),
184 scopes: [RelayEnvironmentStatusScope],
185 environmentId: EnvironmentId.make("env-1"),
186 } as const;
187
188 yield* relayClient.getEnvironmentStatus(statusInput);
189 yield* relayClient.getEnvironmentStatus(statusInput);
190 expect(tokenExchangeCount).toBe(1);
191
192 yield* TestClock.adjust(Duration.seconds(6));
193 yield* relayClient.getEnvironmentStatus(statusInput);
194 expect(tokenExchangeCount).toBe(2);
195
196 yield* relayClient.resetTokenCache;
197 yield* relayClient.getEnvironmentStatus(statusInput);
198 expect(tokenExchangeCount).toBe(3);
199 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
200 });
201
202 it.effect("uses a cached token while another scope waits for an exchange", () => {
203 const exchangeStarted = Promise.withResolvers<void>();
204 const releaseExchange = Promise.withResolvers<void>();
205 let tokenExchangeCount = 0;
206 const statusTokens: Array<string | null> = [];
207 let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = [
208 {
209 accountId: "user-1",
210 clientId: "t3-mobile",
211 relayUrl: "https://relay.example.test",
212 thumbprint: "client-thumbprint",
213 scopes: [RelayEnvironmentStatusScope],
214 accessToken: "cached-status-token",
215 expiresAtMillis: Number.MAX_SAFE_INTEGER,
216 },
217 ];
218 const accessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = {
219 load: Effect.sync(() => persistedTokens),
220 save: (entries) =>
221 Effect.sync(() => {
222 persistedTokens = entries;
223 }),
224 clear: Effect.sync(() => {
225 persistedTokens = [];
226 }),
227 };
228 const fetchFn = ((input, init) => {
229 if (String(input).endsWith("/v1/client/dpop-token")) {
230 tokenExchangeCount += 1;
231 exchangeStarted.resolve();
232 return releaseExchange.promise.then(() =>
233 Response.json({
234 access_token: "expanded-scope-token",
235 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
236 token_type: "DPoP",
237 expires_in: 1_800,
238 scope: `${RelayEnvironmentStatusScope} ${RelayEnvironmentConnectScope}`,
239 }),
240 );
241 }
242 statusTokens.push(new Headers(init?.headers).get("authorization"));
243 return Promise.resolve(
244 Response.json({
245 environmentId: "env-1",
246 endpoint: {
247 httpBaseUrl: "https://desktop.example.test/",
248 wsBaseUrl: "wss://desktop.example.test/ws",
249 providerKind: "cloudflare_tunnel",
250 },
251 status: "online",
252 checkedAt: "2026-09-04T00:00:00.000Z",
253 }),
254 );
255 }) satisfies typeof globalThis.fetch;
256
257 return Effect.gen(function* () {
258 const relayClient = yield* ManagedRelay.ManagedRelayClient;
259 const statusInput = {
260 clerkToken: clerkToken("user-1", "session-1"),
261 scopes: [RelayEnvironmentStatusScope],
262 environmentId: EnvironmentId.make("env-1"),
263 } as const;
264 const expandedScopeInput = {
265 ...statusInput,
266 scopes: [RelayEnvironmentStatusScope, RelayEnvironmentConnectScope],
267 } as const;
268 const firstMiss = yield* relayClient
269 .getEnvironmentStatus(expandedScopeInput)
270 .pipe(Effect.forkChild);
271 yield* Effect.promise(() => exchangeStarted.promise);
272 const secondMiss = yield* relayClient
273 .getEnvironmentStatus(expandedScopeInput)
274 .pipe(Effect.forkChild);
275
276 const status = yield* relayClient.getEnvironmentStatus(statusInput);
277 expect(status.status).toBe("online");
278 expect(statusTokens).toEqual(["DPoP cached-status-token"]);
279
280 releaseExchange.resolve();
281 yield* Fiber.join(firstMiss);
282 yield* Fiber.join(secondMiss);
283 expect(tokenExchangeCount).toBe(1);
284 expect(persistedTokens.map((token) => token.accessToken)).toEqual([
285 "cached-status-token",
286 "expanded-scope-token",
287 ]);
288 yield* relayClient.resetTokenCache;
289 expect(persistedTokens).toEqual([]);
290 yield* relayClient.getEnvironmentStatus(expandedScopeInput);
291 expect(tokenExchangeCount).toBe(2);
292 }).pipe(
293 Effect.ensuring(Effect.sync(() => releaseExchange.resolve())),
294 Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)),
295 );
296 });
297
298 it.effect("reuses a persisted token across runtimes and Clerk session token rotation", () => {
299 let tokenExchangeCount = 0;
300 let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = [];
301 const accessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = {
302 load: Effect.sync(() => persistedTokens),
303 save: (entries) =>
304 Effect.sync(() => {
305 persistedTokens = entries;
306 }),
307 clear: Effect.sync(() => {
308 persistedTokens = [];
309 }),
310 };
311 const fetchFn = ((input) => {
312 const url = String(input);
313 if (url.endsWith("/v1/client/dpop-token")) {
314 tokenExchangeCount += 1;
315 return Promise.resolve(
316 Response.json({
317 access_token: "persisted-relay-token",
318 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
319 token_type: "DPoP",
320 expires_in: 1_800,
321 scope: RelayEnvironmentStatusScope,
322 }),
323 );
324 }
325 return Promise.resolve(
326 Response.json({
327 environmentId: "env-1",
328 endpoint: {
329 httpBaseUrl: "https://desktop.example.test/",
330 wsBaseUrl: "wss://desktop.example.test/ws",
331 providerKind: "cloudflare_tunnel",
332 },
333 status: "online",
334 checkedAt: "2026-06-05T20:00:00.000Z",
335 descriptor: {
336 environmentId: "env-1",
337 label: "Desktop",
338 platform: { os: "darwin", arch: "arm64" },
339 serverVersion: "0.0.0-test",
340 capabilities: { repositoryIdentity: true },
341 },
342 }),
343 );
344 }) satisfies typeof globalThis.fetch;
345 const statusInput = (token: string) =>
346 ({
347 clerkToken: token,
348 scopes: [RelayEnvironmentStatusScope],
349 environmentId: EnvironmentId.make("env-1"),
350 }) as const;
351
352 return Effect.gen(function* () {
353 yield* Effect.gen(function* () {
354 const relayClient = yield* ManagedRelay.ManagedRelayClient;
355 yield* relayClient.getEnvironmentStatus(statusInput(clerkToken("user-1", "session-1")));
356 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)));
357
358 expect(tokenExchangeCount).toBe(1);
359 expect(persistedTokens).toHaveLength(1);
360
361 yield* Effect.gen(function* () {
362 const relayClient = yield* ManagedRelay.ManagedRelayClient;
363 yield* relayClient.getEnvironmentStatus(statusInput(clerkToken("user-1", "session-2")));
364 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)));
365
366 expect(tokenExchangeCount).toBe(1);
367 });
368 });
369
370 it.effect("refreshes a persisted DPoP token once when the relay rejects it", () => {
371 let tokenExchangeCount = 0;
372 const statusTokens: Array<string | null> = [];
373 let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = [
374 {
375 accountId: "user-1",
376 clientId: "t3-mobile",
377 relayUrl: "https://relay.example.test",
378 thumbprint: "client-thumbprint",
379 scopes: [RelayEnvironmentStatusScope],
380 accessToken: "stale-relay-token",
381 expiresAtMillis: Number.MAX_SAFE_INTEGER,
382 },
383 ];
384 const accessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = {
385 load: Effect.sync(() => persistedTokens),
386 save: (entries) =>
387 Effect.sync(() => {
388 persistedTokens = entries;
389 }),
390 clear: Effect.sync(() => {
391 persistedTokens = [];
392 }),
393 };
394 const fetchFn = ((input, init) => {
395 const url = String(input);
396 if (url.endsWith("/v1/client/dpop-token")) {
397 tokenExchangeCount += 1;
398 return Promise.resolve(
399 Response.json({
400 access_token: "fresh-relay-token",
401 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
402 token_type: "DPoP",
403 expires_in: 1_800,
404 scope: RelayEnvironmentStatusScope,
405 }),
406 );
407 }
408
409 const authorization = new Headers(init?.headers).get("authorization");
410 statusTokens.push(authorization);
411 if (authorization === "DPoP stale-relay-token") {
412 return Promise.resolve(
413 Response.json(
414 {
415 _tag: "RelayAuthInvalidError",
416 code: "auth_invalid",
417 reason: "invalid_bearer",
418 traceId: "trace-stale-token",
419 },
420 { status: 401 },
421 ),
422 );
423 }
424 return Promise.resolve(
425 Response.json({
426 environmentId: "env-1",
427 endpoint: {
428 httpBaseUrl: "https://desktop.example.test/",
429 wsBaseUrl: "wss://desktop.example.test/ws",
430 providerKind: "cloudflare_tunnel",
431 },
432 status: "online",
433 checkedAt: "2026-06-05T20:00:00.000Z",
434 descriptor: {
435 environmentId: "env-1",
436 label: "Desktop",
437 platform: { os: "darwin", arch: "arm64" },
438 serverVersion: "0.0.0-test",
439 capabilities: { repositoryIdentity: true },
440 },
441 }),
442 );
443 }) satisfies typeof globalThis.fetch;
444
445 return Effect.gen(function* () {
446 const relayClient = yield* ManagedRelay.ManagedRelayClient;
447 const result = yield* relayClient.getEnvironmentStatus({
448 clerkToken: clerkToken("user-1", "session-1"),
449 scopes: [RelayEnvironmentStatusScope],
450 environmentId: EnvironmentId.make("env-1"),
451 });
452
453 expect(result.status).toBe("online");
454 expect(statusTokens).toEqual(["DPoP stale-relay-token", "DPoP fresh-relay-token"]);
455 expect(tokenExchangeCount).toBe(1);
456 expect(persistedTokens).toMatchObject([
457 {
458 accessToken: "fresh-relay-token",
459 },
460 ]);
461 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)));
462 });
463
464 it.effect("does not persist tokens when the Clerk subject cannot be decoded", () => {
465 let persistedTokens: ReadonlyArray<ManagedRelay.ManagedRelayAccessTokenCacheEntry> = [];
466 const accessTokenStore: ManagedRelay.ManagedRelayAccessTokenStore = {
467 load: Effect.succeed([]),
468 save: (entries) =>
469 Effect.sync(() => {
470 persistedTokens = entries;
471 }),
472 clear: Effect.void,
473 };
474 const fetchFn = ((input) => {
475 const url = String(input);
476 if (url.endsWith("/v1/client/dpop-token")) {
477 return Promise.resolve(
478 Response.json({
479 access_token: "relay-token",
480 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
481 token_type: "DPoP",
482 expires_in: 1_800,
483 scope: RelayEnvironmentStatusScope,
484 }),
485 );
486 }
487 return Promise.resolve(
488 Response.json({
489 environmentId: "env-1",
490 endpoint: {
491 httpBaseUrl: "https://desktop.example.test/",
492 wsBaseUrl: "wss://desktop.example.test/ws",
493 providerKind: "cloudflare_tunnel",
494 },
495 status: "online",
496 checkedAt: "2026-06-05T20:00:00.000Z",
497 descriptor: {
498 environmentId: "env-1",
499 label: "Desktop",
500 platform: { os: "darwin", arch: "arm64" },
501 serverVersion: "0.0.0-test",
502 capabilities: { repositoryIdentity: true },
503 },
504 }),
505 );
506 }) satisfies typeof globalThis.fetch;
507
508 return Effect.gen(function* () {
509 const relayClient = yield* ManagedRelay.ManagedRelayClient;
510 yield* relayClient.getEnvironmentStatus({
511 clerkToken: "not-a-jwt",
512 scopes: [RelayEnvironmentStatusScope],
513 environmentId: EnvironmentId.make("env-1"),
514 });
515
516 expect(persistedTokens).toEqual([]);
517 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn, undefined, accessTokenStore)));
518 });
519
520 it.effect("times out stalled relay environment listing requests", () => {
521 const fetchFn = (() =>
522 new Promise<Response>(() => undefined)) satisfies typeof globalThis.fetch;
523
524 return Effect.gen(function* () {
525 const relayClient = yield* ManagedRelay.ManagedRelayClient;
526 const errorFiber = yield* relayClient
527 .listEnvironments({ clerkToken: "clerk-token" })
528 .pipe(Effect.flip, Effect.forkScoped);
529
530 yield* Effect.yieldNow;
531 yield* TestClock.adjust(Duration.millis(ManagedRelay.MANAGED_RELAY_REQUEST_TIMEOUT_MS));
532 const error = yield* Fiber.join(errorFiber);
533
534 expect(error).toMatchObject({
535 _tag: "ManagedRelayRequestTimeoutError",
536 activity: "Relay environment listing",
537 timeoutMs: ManagedRelay.MANAGED_RELAY_REQUEST_TIMEOUT_MS,
538 message: `Relay environment listing timed out. ${NETWORK_BLOCKING_HINT}`,
539 });
540 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), layerManagedRelayTest(fetchFn))));
541 });
542
543 it.effect("suggests checking network filtering when fetch fails without a response", () => {
544 const fetchFn = (() =>
545 Promise.reject(new TypeError("Failed to fetch"))) satisfies typeof globalThis.fetch;
546 return Effect.gen(function* () {
547 const relayClient = yield* ManagedRelay.ManagedRelayClient;
548 const error = yield* relayClient
549 .listEnvironments({ clerkToken: "clerk-token" })
550 .pipe(Effect.flip);
551 expect(error).toMatchObject({
552 _tag: "ManagedRelayRequestFailedError",
553 transportFailed: true,
554 message: `Could not list relay-managed environments. ${NETWORK_BLOCKING_HINT}`,
555 });
556 const encoded = yield* encodeRelayError(error);
557 const decoded = yield* decodeRelayError(encoded);
558 expect(decoded.message).toBe(error.message);
559 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
560 });
561
562 it.effect("does not suggest network filtering for an HTTP server error", () => {
563 const fetchFn = (() =>
564 Promise.resolve(
565 new Response("Unavailable", { status: 503 }),
566 )) satisfies typeof globalThis.fetch;
567 return Effect.gen(function* () {
568 const relayClient = yield* ManagedRelay.ManagedRelayClient;
569 const error = yield* relayClient
570 .listEnvironments({ clerkToken: "clerk-token" })
571 .pipe(Effect.flip);
572 expect(error.message).toBe("Could not list relay-managed environments.");
573 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
574 });
575
576 it.effect("preserves typed relay trace IDs on client errors", () => {
577 const fetchFn = (() =>
578 Promise.resolve(
579 Response.json(
580 {
581 _tag: "RelayAuthInvalidError",
582 code: "auth_invalid",
583 reason: "invalid_bearer",
584 traceId: "trace-managed-relay",
585 },
586 { status: 401 },
587 ),
588 )) satisfies typeof globalThis.fetch;
589
590 return Effect.gen(function* () {
591 const relayClient = yield* ManagedRelay.ManagedRelayClient;
592 const error = yield* relayClient
593 .listEnvironments({ clerkToken: "clerk-token" })
594 .pipe(Effect.flip);
595
596 expect(error).toMatchObject({
597 _tag: "ManagedRelayRequestFailedError",
598 traceId: "trace-managed-relay",
599 });
600 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
601 });
602
603 it.effect("explains a relay deadline 504 on link and keeps its trace ID", () => {
604 const fetchFn = (() =>
605 Promise.resolve(
606 Response.json(
607 { error: "relay_request_deadline_exceeded" },
608 {
609 status: 504,
610 headers: {
611 traceparent: "00-4684d7d140f34f0393d42744d240e207-0baf648c44f4bce6-01",
612 },
613 },
614 ),
615 )) satisfies typeof globalThis.fetch;
616
617 return Effect.gen(function* () {
618 const relayClient = yield* ManagedRelay.ManagedRelayClient;
619 const error = yield* relayClient
620 .linkEnvironment({
621 clerkToken: "clerk-token",
622 payload: {
623 proof: "link-proof",
624 notificationsEnabled: true,
625 liveActivitiesEnabled: true,
626 managedTunnelsEnabled: true,
627 },
628 })
629 .pipe(Effect.flip);
630
631 expect(error).toMatchObject({
632 _tag: "ManagedRelayRequestFailedError",
633 status: 504,
634 traceId: "4684d7d140f34f0393d42744d240e207",
635 });
636 expect(
637 error._tag === "ManagedRelayRequestFailedError" ? relayRequestFailureDetail(error) : null,
638 ).toBe(RELAY_GATEWAY_TIMEOUT_MESSAGE);
639 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
640 });
641
642 it.effect("accepts generic DPoP errors from relays without the optional reason", () => {
643 const fetchFn = (() =>
644 Promise.resolve(
645 Response.json(
646 {
647 _tag: "RelayAuthInvalidError",
648 code: "auth_invalid",
649 reason: "invalid_dpop",
650 traceId: "trace-old-relay",
651 },
652 { status: 401 },
653 ),
654 )) satisfies typeof globalThis.fetch;
655
656 return Effect.gen(function* () {
657 const relayClient = yield* ManagedRelay.ManagedRelayClient;
658 const error = yield* relayClient
659 .listEnvironments({ clerkToken: "clerk-token" })
660 .pipe(Effect.flip);
661
662 expect(error).toMatchObject({
663 _tag: "ManagedRelayRequestFailedError",
664 traceId: "trace-old-relay",
665 relayError: {
666 _tag: "RelayAuthInvalidError",
667 reason: "invalid_dpop",
668 },
669 });
670 if (
671 error._tag === "ManagedRelayRequestFailedError" &&
672 error.relayError?._tag === "RelayAuthInvalidError"
673 ) {
674 expect(error.relayError.dpopFailureReason).toBeUndefined();
675 }
676 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
677 });
678
679 it.effect("lists account devices through the v2 Clerk bearer client endpoint", () => {
680 const fetchFn = ((input, init) => {
681 expect(String(input)).toBe("https://relay.example.test/v2/client/devices");
682 expect(init?.headers).toMatchObject({
683 authorization: "Bearer clerk-token",
684 });
685 return Promise.resolve(
686 Response.json({
687 devices: [
688 {
689 deviceId: "device-1",
690 label: "Julius's iPhone",
691 platform: "ios",
692 iosMajorVersion: 18,
693 appVersion: "1.0.0",
694 notifications: {
695 enabled: false,
696 notifyOnApproval: true,
697 notifyOnInput: true,
698 notifyOnCompletion: true,
699 notifyOnFailure: true,
700 },
701 liveActivities: {
702 enabled: true,
703 },
704 updatedAt: "2026-06-01T00:00:00.000Z",
705 },
706 {
707 deviceId: "device-2",
708 label: "Android phone",
709 platform: "android",
710 iosMajorVersion: null,
711 androidApiLevel: 36,
712 appVersion: "1.0.0",
713 notifications: {
714 enabled: true,
715 notifyOnApproval: true,
716 notifyOnInput: true,
717 notifyOnCompletion: true,
718 notifyOnFailure: true,
719 },
720 liveActivities: { enabled: true },
721 updatedAt: "2026-06-01T00:00:00.000Z",
722 },
723 ],
724 }),
725 );
726 }) satisfies typeof globalThis.fetch;
727
728 return Effect.gen(function* () {
729 const relayClient = yield* ManagedRelay.ManagedRelayClient;
730 const devices = yield* relayClient.listDevices({ clerkToken: "clerk-token" });
731 expect(devices).toMatchObject([
732 {
733 deviceId: "device-1",
734 label: "Julius's iPhone",
735 notifications: {
736 enabled: false,
737 },
738 },
739 {
740 deviceId: "device-2",
741 platform: "android",
742 iosMajorVersion: null,
743 androidApiLevel: 36,
744 },
745 ]);
746 }).pipe(Effect.provide(layerManagedRelayTest(fetchFn)));
747 });
748});