1// @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync or sign.
2import * as NodeCrypto from "node:crypto";
3import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto";
4
5import {
6 RelayCloudEnvironmentHealthRequest,
7 RelayCloudMintCredentialRequest,
8 RelayCloudEnvironmentHealthProofPayload,
9 RelayCloudMintCredentialProofPayload,
10 RelayEnvironmentHealthResponse,
11 RelayEnvironmentHealthResponseProofPayload,
12 RelayEnvironmentMintResponse,
13 RelayEnvironmentMintResponseProofPayload,
14} from "@t3tools/contracts/relay";
15import { describe, expect, it } from "@effect/vitest";
16import * as DateTime from "effect/DateTime";
17import { RELAY_HEALTH_RESPONSE_TYP, RELAY_MINT_RESPONSE_TYP } from "@t3tools/shared/relayJwt";
18import * as Deferred from "effect/Deferred";
19import * as Duration from "effect/Duration";
20import * as Effect from "effect/Effect";
21import * as Fiber from "effect/Fiber";
22import * as Layer from "effect/Layer";
23import * as Redacted from "effect/Redacted";
24import * as Ref from "effect/Ref";
25import * as Result from "effect/Result";
26import * as Schema from "effect/Schema";
27import * as TestClock from "effect/testing/TestClock";
28import * as Tracer from "effect/Tracer";
29import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
30
31import * as EnvironmentLinks from "./EnvironmentLinks.ts";
32import * as RelayConfiguration from "../Config.ts";
33import * as EnvironmentConnector from "./EnvironmentConnector.ts";
34import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
35
36const cloudKeyPair = NodeCrypto.generateKeyPairSync("ed25519", {
37 privateKeyEncoding: { format: "pem", type: "pkcs8" },
38 publicKeyEncoding: { format: "pem", type: "spki" },
39});
40
41const environmentKeyPair = NodeCrypto.generateKeyPairSync("ed25519", {
42 privateKeyEncoding: { format: "pem", type: "pkcs8" },
43 publicKeyEncoding: { format: "pem", type: "spki" },
44});
45
46const otherEnvironmentKeyPair = NodeCrypto.generateKeyPairSync("ed25519", {
47 privateKeyEncoding: { format: "pem", type: "pkcs8" },
48 publicKeyEncoding: { format: "pem", type: "spki" },
49});
50
51const decodeHealthRequestBody = Schema.decodeUnknownSync(
52 Schema.fromJsonString(RelayCloudEnvironmentHealthRequest),
53);
54const decodeMintRequestBody = Schema.decodeUnknownSync(
55 Schema.fromJsonString(RelayCloudMintCredentialRequest),
56);
57const isEnvironmentConnectNotAuthorized = Schema.is(
58 EnvironmentConnector.EnvironmentConnectNotAuthorized,
59);
60
61function requestBodyText(request: HttpClientRequest.HttpClientRequest): string {
62 return request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : "{}";
63}
64
65const settings = RelayConfiguration.RelayConfiguration.of({
66 relayIssuer: "https://relay.example.test",
67 apns: {
68 environment: "sandbox",
69 teamId: "team-id",
70 keyId: "key-id",
71 privateKey: Redacted.make("private-key"),
72 bundleId: "com.t3tools.t3code.dev",
73 },
74 apnsDeliveryJobSigningSecret: Redacted.make("job-secret"),
75 clerkSecretKey: Redacted.make("clerk-secret"),
76 clerkPublishableKey: "pk_test_test",
77 clerkJwtAudience: "t3-code-relay",
78 cloudMintPrivateKey: Redacted.make(cloudKeyPair.privateKey),
79 cloudMintPublicKey: cloudKeyPair.publicKey,
80 managedEndpointBaseDomain: "example.test",
81 managedEndpointNamespace: undefined,
82});
83
84function signTestJwt(payload: object, typ: string, privateKey: string): string {
85 const header = Buffer.from(JSON.stringify({ alg: "EdDSA", typ })).toString("base64url");
86 const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url");
87 const input = `${header}.${encodedPayload}`;
88 return `${input}.${NodeCrypto.sign(null, Buffer.from(input), privateKey).toString("base64url")}`;
89}
90
91function decodeRequestProof<T>(proof: string): T {
92 const payload = proof.split(".")[1];
93 if (!payload) throw new Error("Missing JWT payload.");
94 return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as T;
95}
96
97function signMintResponse(
98 request: RelayCloudMintCredentialRequest,
99 overrides: Partial<RelayEnvironmentMintResponseProofPayload> = {},
100 privateKey = environmentKeyPair.privateKey,
101): RelayEnvironmentMintResponse {
102 const requestProof = decodeRequestProof<RelayCloudMintCredentialProofPayload>(request.proof);
103 const payload = {
104 iss: `t3-env:${requestProof.environmentId}`,
105 aud: "https://relay.example.test",
106 sub: requestProof.environmentId,
107 jti: "mint-response-jti",
108 iat: requestProof.iat,
109 exp: requestProof.exp,
110 environmentId: requestProof.environmentId,
111 clientProofKeyThumbprint: requestProof.clientProofKeyThumbprint,
112 requestNonce: requestProof.nonce,
113 credential: "pairing_credential",
114 ...overrides,
115 } satisfies RelayEnvironmentMintResponseProofPayload;
116 return {
117 credential: payload.credential,
118 expiresAt: DateTime.formatIso(DateTime.makeUnsafe(payload.exp * 1_000)),
119 proof: signTestJwt(payload, RELAY_MINT_RESPONSE_TYP, privateKey),
120 };
121}
122
123function signHealthResponse(
124 request: RelayCloudEnvironmentHealthRequest,
125 privateKey = environmentKeyPair.privateKey,
126 overrides: Partial<RelayEnvironmentHealthResponse> = {},
127 payloadOverrides: Partial<RelayEnvironmentHealthResponseProofPayload> = {},
128): RelayEnvironmentHealthResponse {
129 const requestProof = decodeRequestProof<RelayCloudEnvironmentHealthProofPayload>(request.proof);
130 const payload = {
131 iss: `t3-env:${requestProof.environmentId}`,
132 aud: "https://relay.example.test",
133 sub: requestProof.environmentId,
134 jti: "health-response-jti",
135 iat: requestProof.iat,
136 exp: requestProof.exp,
137 environmentId: requestProof.environmentId,
138 requestNonce: requestProof.nonce,
139 status: "online",
140 descriptor: {
141 environmentId: requestProof.environmentId,
142 label: "Connector Test Environment",
143 platform: { os: "darwin", arch: "arm64" },
144 serverVersion: "0.0.0-test",
145 capabilities: { repositoryIdentity: true },
146 },
147 checkedAt: DateTime.formatIso(DateTime.makeUnsafe(requestProof.iat * 1_000)),
148 ...payloadOverrides,
149 } satisfies RelayEnvironmentHealthResponseProofPayload;
150 return {
151 environmentId: payload.environmentId,
152 status: "online",
153 descriptor: payload.descriptor,
154 checkedAt: payload.checkedAt,
155 proof: signTestJwt(payload, RELAY_HEALTH_RESPONSE_TYP, privateKey),
156 ...overrides,
157 };
158}
159
160function layerConnectorTest(
161 execute: (
162 request: HttpClientRequest.HttpClientRequest,
163 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,
164 options?: {
165 readonly links?: EnvironmentLinks.EnvironmentLinks["Service"];
166 readonly allocations?: ManagedEndpointAllocations.ManagedEndpointAllocations["Service"];
167 },
168) {
169 return EnvironmentConnector.layer.pipe(
170 Layer.provide(NodeCryptoLayer.layer),
171 Layer.provide(Layer.succeed(EnvironmentLinks.EnvironmentLinks, options?.links ?? makeLinks())),
172 Layer.provide(
173 Layer.succeed(
174 ManagedEndpointAllocations.ManagedEndpointAllocations,
175 options?.allocations ?? makeAllocations(),
176 ),
177 ),
178 Layer.provide(RelayConfiguration.layer(settings)),
179 Layer.provide(Layer.succeed(HttpClient.HttpClient, HttpClient.make(execute))),
180 );
181}
182
183function makeAllocations(
184 allocation: ManagedEndpointAllocations.ManagedEndpointAllocation | null = {
185 userId: "user_123",
186 environmentId: "env-connector-test",
187 hostname: "env.example.test",
188 tunnelId: "tunnel-id",
189 tunnelName: "tunnel-name",
190 dnsRecordId: "dns-record-id",
191 readyAt: "2026-05-25T00:00:00.000Z",
192 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
193 updatedAt: "2026-05-25T00:00:00.000Z",
194 generation: 1,
195 tunnelReleasedAt: null,
196 },
197): ManagedEndpointAllocations.ManagedEndpointAllocations["Service"] {
198 return {
199 getByTunnelName: () => Effect.die("unused getByTunnelName"),
200 get: () => Effect.succeed(allocation),
201 reserve: () => Effect.die("unused"),
202 recordTunnel: () => Effect.die("unused"),
203 recordDns: () => Effect.die("unused"),
204 markReady: () => Effect.die("unused"),
205 enableRecovery: () => Effect.die("unused"),
206 listByTunnelNames: () => Effect.die("unused"),
207 claimRelease: () => Effect.die("unused"),
208 withClaimedTunnel: () => Effect.die("unused"),
209 claimDeprovision: () => Effect.die("unused"),
210 remove: () => Effect.die("unused"),
211 removeClaimed: () => Effect.die("unused"),
212 };
213}
214
215function makeLinks(
216 overrides: Partial<EnvironmentLinks.RelayLinkedEnvironmentRecord> = {},
217): EnvironmentLinks.EnvironmentLinks["Service"] {
218 return {
219 upsert: () => Effect.void,
220 listDeliveryUsersForEnvironment: () => Effect.succeed([]),
221 listForUser: () => Effect.succeed([]),
222 getForUser: () =>
223 Effect.succeed({
224 environmentId: "env-connector-test" as never,
225 label: "Connector Test Environment",
226 endpoint: {
227 httpBaseUrl: "https://env.example.test/",
228 wsBaseUrl: "wss://env.example.test/ws",
229 providerKind: "cloudflare_tunnel",
230 },
231 linkedAt: "2026-05-25T00:00:00.000Z",
232 environmentPublicKey: environmentKeyPair.publicKey,
233 ...overrides,
234 }),
235 findActiveManagedForEnvironment: () => Effect.succeed([]),
236 setHoldWebhooksWhileOffline: () => Effect.void,
237 revokeForUser: () => Effect.succeed(false),
238 };
239}
240
241describe("EnvironmentConnector", () => {
242 it.effect("loads the environment link and managed allocation concurrently", () =>
243 Effect.gen(function* () {
244 const started = yield* Ref.make(0);
245 const bothStarted = yield* Deferred.make<void>();
246 const release = yield* Deferred.make<void>();
247 const waitForPeer = Effect.gen(function* () {
248 const count = yield* Ref.updateAndGet(started, (value) => value + 1);
249 if (count === 2) {
250 yield* Deferred.succeed(bothStarted, undefined);
251 }
252 yield* Deferred.await(release);
253 });
254 const links = makeLinks();
255 const allocations = makeAllocations();
256 const execute = (request: HttpClientRequest.HttpClientRequest) =>
257 Effect.sync(() => {
258 const healthRequest = decodeHealthRequestBody(requestBodyText(request));
259 return HttpClientResponse.fromWeb(
260 request,
261 Response.json(signHealthResponse(healthRequest), { status: 200 }),
262 );
263 });
264 const status = Effect.gen(function* () {
265 const connector = yield* EnvironmentConnector.EnvironmentConnector;
266 return yield* connector.status({
267 userId: "user_123",
268 environmentId: "env-connector-test" as never,
269 });
270 }).pipe(
271 Effect.provide(
272 layerConnectorTest(execute, {
273 links: {
274 ...links,
275 getForUser: (input) => waitForPeer.pipe(Effect.andThen(links.getForUser(input))),
276 },
277 allocations: {
278 ...allocations,
279 get: (input) => waitForPeer.pipe(Effect.andThen(allocations.get(input))),
280 },
281 }),
282 ),
283 );
284
285 const fiber = yield* Effect.forkChild(status);
286 yield* Deferred.await(bothStarted);
287 yield* Deferred.succeed(release, undefined);
288 yield* Fiber.join(fiber);
289
290 expect(yield* Ref.get(started)).toBe(2);
291 }),
292 );
293
294 it.effect("checks linked environment health through the managed endpoint", () => {
295 const seenUrls: Array<string> = [];
296 const seenProofs: Array<RelayCloudEnvironmentHealthProofPayload> = [];
297 const execute = (request: HttpClientRequest.HttpClientRequest) =>
298 Effect.sync(() => {
299 const healthRequest = decodeHealthRequestBody(requestBodyText(request));
300 seenUrls.push(request.url);
301 seenProofs.push(decodeRequestProof(healthRequest.proof));
302 return HttpClientResponse.fromWeb(
303 request,
304 Response.json(signHealthResponse(healthRequest), { status: 200 }),
305 );
306 });
307
308 return Effect.gen(function* () {
309 const connector = yield* EnvironmentConnector.EnvironmentConnector;
310 const result = yield* connector.status({
311 userId: "user_123",
312 environmentId: "env-connector-test",
313 });
314
315 expect(seenUrls).toEqual(["https://env.example.test/api/t3-connect/health"]);
316 expect(seenProofs[0]).toMatchObject({
317 iss: "https://relay.example.test",
318 aud: "t3-env:env-connector-test",
319 sub: "user_123",
320 environmentId: "env-connector-test",
321 scope: ["environment:status"],
322 });
323 expect(result).toMatchObject({
324 environmentId: "env-connector-test",
325 status: "online",
326 descriptor: {
327 environmentId: "env-connector-test",
328 label: "Connector Test Environment",
329 },
330 });
331 }).pipe(Effect.provide(layerConnectorTest(execute)));
332 });
333
334 it.effect("rejects manual endpoints before sending a health request", () => {
335 let requestCount = 0;
336 const execute = () =>
337 Effect.sync(() => {
338 requestCount += 1;
339 throw new Error("unexpected request");
340 });
341
342 return Effect.gen(function* () {
343 const connector = yield* EnvironmentConnector.EnvironmentConnector;
344 const result = yield* Effect.result(
345 connector.status({
346 userId: "user_123",
347 environmentId: "env-connector-test",
348 }),
349 );
350
351 expect(Result.isFailure(result)).toBe(true);
352 if (Result.isFailure(result)) {
353 expect(isEnvironmentConnectNotAuthorized(result.failure)).toBe(true);
354 if (isEnvironmentConnectNotAuthorized(result.failure)) {
355 expect(result.failure).toMatchObject({
356 operation: "status",
357 reason: "endpoint_provider_not_managed",
358 });
359 }
360 }
361 expect(requestCount).toBe(0);
362 }).pipe(
363 Effect.provide(
364 layerConnectorTest(execute, {
365 links: makeLinks({
366 endpoint: {
367 httpBaseUrl: "https://127.0.0.1/",
368 wsBaseUrl: "wss://127.0.0.1/ws",
369 providerKind: "manual",
370 },
371 }),
372 }),
373 ),
374 );
375 });
376
377 it.effect("rejects stale managed endpoints before sending a mint request", () => {
378 let requestCount = 0;
379 const spans: Array<Tracer.NativeSpan> = [];
380 const tracer = Tracer.make({
381 span: (options) => {
382 const span = new Tracer.NativeSpan(options);
383 spans.push(span);
384 return span;
385 },
386 });
387 const execute = () =>
388 Effect.sync(() => {
389 requestCount += 1;
390 throw new Error("unexpected request");
391 });
392
393 return Effect.gen(function* () {
394 const connector = yield* EnvironmentConnector.EnvironmentConnector;
395 const result = yield* Effect.result(
396 connector.connect({
397 userId: "user_123",
398 environmentId: "env-connector-test",
399 clientProofKeyThumbprint: "client-proof-key-thumbprint",
400 }),
401 );
402
403 expect(Result.isFailure(result)).toBe(true);
404 if (Result.isFailure(result)) {
405 expect(isEnvironmentConnectNotAuthorized(result.failure)).toBe(true);
406 if (isEnvironmentConnectNotAuthorized(result.failure)) {
407 expect(result.failure).toMatchObject({
408 operation: "connect",
409 reason: "managed_endpoint_mismatch",
410 });
411 }
412 }
413 const resolutionSpan = spans.find(
414 (span) => span.name === "relay.environment_connector.resolve_managed_endpoint",
415 );
416 expect(Object.fromEntries(resolutionSpan?.attributes ?? [])).toMatchObject({
417 "relay.authorization.allocation_hostname": "env.example.test",
418 "relay.authorization.allocation_has_ready_at": true,
419 "relay.authorization.allocation_has_tunnel_id": true,
420 "relay.authorization.allocation_has_dns_record_id": true,
421 "relay.authorization.linked_http_base_url": "https://attacker.example.test/",
422 "relay.authorization.linked_ws_base_url": "wss://attacker.example.test/ws",
423 "relay.authorization.resolved_http_base_url": "https://env.example.test/",
424 "relay.authorization.resolved_ws_base_url": "wss://env.example.test/ws",
425 });
426 expect(requestCount).toBe(0);
427 }).pipe(
428 Effect.provide(
429 layerConnectorTest(execute, {
430 links: makeLinks({
431 endpoint: {
432 httpBaseUrl: "https://attacker.example.test/",
433 wsBaseUrl: "wss://attacker.example.test/ws",
434 providerKind: "cloudflare_tunnel",
435 },
436 }),
437 }),
438 ),
439 Effect.provideService(Tracer.Tracer, tracer),
440 );
441 });
442
443 it.effect("rejects unready managed endpoint allocations before sending a request", () => {
444 let requestCount = 0;
445 const execute = () =>
446 Effect.sync(() => {
447 requestCount += 1;
448 throw new Error("unexpected request");
449 });
450
451 return Effect.gen(function* () {
452 const connector = yield* EnvironmentConnector.EnvironmentConnector;
453 const result = yield* Effect.result(
454 connector.status({
455 userId: "user_123",
456 environmentId: "env-connector-test",
457 }),
458 );
459
460 expect(Result.isFailure(result)).toBe(true);
461 if (Result.isFailure(result)) {
462 expect(isEnvironmentConnectNotAuthorized(result.failure)).toBe(true);
463 if (isEnvironmentConnectNotAuthorized(result.failure)) {
464 expect(result.failure).toMatchObject({
465 operation: "status",
466 reason: "managed_endpoint_allocation_not_ready",
467 });
468 }
469 }
470 expect(requestCount).toBe(0);
471 }).pipe(
472 Effect.provide(
473 layerConnectorTest(execute, {
474 allocations: makeAllocations({
475 userId: "user_123",
476 environmentId: "env-connector-test",
477 hostname: "env.example.test",
478 tunnelId: "tunnel-id",
479 tunnelName: "tunnel-name",
480 dnsRecordId: "dns-record-id",
481 readyAt: null,
482 origin: null,
483 updatedAt: "2026-05-25T00:00:00.000Z",
484 generation: 1,
485 tunnelReleasedAt: null,
486 }),
487 }),
488 ),
489 );
490 });
491
492 it.effect("rejects signed health responses with stale checkedAt timestamps", () => {
493 const execute = (request: HttpClientRequest.HttpClientRequest) =>
494 Effect.sync(() => {
495 const healthRequest = decodeHealthRequestBody(requestBodyText(request));
496 return HttpClientResponse.fromWeb(
497 request,
498 Response.json(
499 signHealthResponse(
500 healthRequest,
501 environmentKeyPair.privateKey,
502 {},
503 {
504 checkedAt: "2026-05-24T00:00:00.000Z",
505 },
506 ),
507 { status: 200 },
508 ),
509 );
510 });
511
512 return Effect.gen(function* () {
513 const connector = yield* EnvironmentConnector.EnvironmentConnector;
514 const result = yield* Effect.exit(
515 connector.status({
516 userId: "user_123",
517 environmentId: "env-connector-test",
518 }),
519 );
520
521 expect(result._tag).toBe("Failure");
522 if (result._tag === "Failure") {
523 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
524 }
525 }).pipe(Effect.provide(layerConnectorTest(execute)));
526 });
527
528 it.effect("reports offline status when the managed endpoint health request fails", () => {
529 const execute = (request: HttpClientRequest.HttpClientRequest) =>
530 Effect.succeed(
531 HttpClientResponse.fromWeb(
532 request,
533 Response.json(
534 {
535 _tag: "EnvironmentHttpInternalServerError",
536 message: "Environment is unavailable.",
537 },
538 { status: 500 },
539 ),
540 ),
541 );
542
543 return Effect.gen(function* () {
544 const connector = yield* EnvironmentConnector.EnvironmentConnector;
545 const result = yield* connector.status({
546 userId: "user_123",
547 environmentId: "env-connector-test",
548 });
549
550 expect(result).toMatchObject({
551 environmentId: "env-connector-test",
552 status: "offline",
553 error: "Managed endpoint health request failed: Environment is unavailable.",
554 traceId: expect.any(String),
555 });
556 expect(result).not.toHaveProperty("offlineReason");
557 }).pipe(Effect.provide(layerConnectorTest(execute)));
558 });
559
560 it.effect("reports a released tunnel as the reason an environment is offline", () => {
561 const execute = (request: HttpClientRequest.HttpClientRequest) =>
562 Effect.succeed(
563 HttpClientResponse.fromWeb(request, new Response("tunnel not found", { status: 530 })),
564 );
565 const allocations = makeAllocations({
566 userId: "user_123",
567 environmentId: "env-connector-test",
568 hostname: "env.example.test",
569 tunnelId: "tunnel-id",
570 tunnelName: "tunnel-name",
571 dnsRecordId: "dns-record-id",
572 readyAt: "2026-05-25T00:00:00.000Z",
573 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
574 updatedAt: "2026-05-25T00:00:00.000Z",
575 generation: 2,
576 tunnelReleasedAt: "2026-05-26T00:00:00.000Z",
577 });
578
579 return Effect.gen(function* () {
580 const connector = yield* EnvironmentConnector.EnvironmentConnector;
581 const result = yield* connector.status({
582 userId: "user_123",
583 environmentId: "env-connector-test",
584 });
585
586 expect(result).toMatchObject({ status: "offline", offlineReason: "tunnel_released" });
587 }).pipe(Effect.provide(layerConnectorTest(execute, { allocations })));
588 });
589
590 it.effect("rejects health responses with a mismatched top-level environment id", () => {
591 const execute = (request: HttpClientRequest.HttpClientRequest) =>
592 Effect.sync(() => {
593 const healthRequest = decodeHealthRequestBody(requestBodyText(request));
594 return HttpClientResponse.fromWeb(
595 request,
596 Response.json(
597 signHealthResponse(healthRequest, environmentKeyPair.privateKey, {
598 environmentId: "other-env" as RelayEnvironmentHealthResponse["environmentId"],
599 }),
600 { status: 200 },
601 ),
602 );
603 });
604
605 return Effect.gen(function* () {
606 const connector = yield* EnvironmentConnector.EnvironmentConnector;
607 const result = yield* Effect.exit(
608 connector.status({
609 userId: "user_123",
610 environmentId: "env-connector-test",
611 }),
612 );
613
614 expect(result._tag).toBe("Failure");
615 if (result._tag === "Failure") {
616 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
617 }
618 }).pipe(Effect.provide(layerConnectorTest(execute)));
619 });
620
621 it.effect("rejects health responses with an unsigned top-level descriptor mutation", () => {
622 const execute = (request: HttpClientRequest.HttpClientRequest) =>
623 Effect.sync(() => {
624 const healthRequest = decodeHealthRequestBody(requestBodyText(request));
625 const response = signHealthResponse(healthRequest);
626 return HttpClientResponse.fromWeb(
627 request,
628 Response.json(
629 {
630 ...response,
631 descriptor: {
632 ...response.descriptor,
633 label: "Tampered Environment Label",
634 },
635 } satisfies RelayEnvironmentHealthResponse,
636 { status: 200 },
637 ),
638 );
639 });
640
641 return Effect.gen(function* () {
642 const connector = yield* EnvironmentConnector.EnvironmentConnector;
643 const result = yield* Effect.exit(
644 connector.status({
645 userId: "user_123",
646 environmentId: "env-connector-test",
647 }),
648 );
649
650 expect(result._tag).toBe("Failure");
651 if (result._tag === "Failure") {
652 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
653 }
654 }).pipe(Effect.provide(layerConnectorTest(execute)));
655 });
656
657 it.effect("rejects health responses when the linked environment public key is malformed", () => {
658 const execute = (request: HttpClientRequest.HttpClientRequest) =>
659 Effect.sync(() => {
660 const healthRequest = decodeHealthRequestBody(requestBodyText(request));
661 return HttpClientResponse.fromWeb(
662 request,
663 Response.json(signHealthResponse(healthRequest), { status: 200 }),
664 );
665 });
666
667 return Effect.gen(function* () {
668 const connector = yield* EnvironmentConnector.EnvironmentConnector;
669 const result = yield* Effect.exit(
670 connector.status({
671 userId: "user_123",
672 environmentId: "env-connector-test",
673 }),
674 );
675
676 expect(result._tag).toBe("Failure");
677 if (result._tag === "Failure") {
678 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
679 }
680 }).pipe(
681 Effect.provide(
682 layerConnectorTest(execute, {
683 links: makeLinks({
684 environmentPublicKey: "not a pem public key",
685 }),
686 }),
687 ),
688 );
689 });
690
691 it.effect("mints a one-time environment credential through the linked endpoint", () => {
692 const seenUrls: Array<string> = [];
693 const seenProofs: Array<RelayCloudMintCredentialProofPayload> = [];
694 const execute = (request: HttpClientRequest.HttpClientRequest) =>
695 Effect.sync(() => {
696 const mintRequest = decodeMintRequestBody(requestBodyText(request));
697 seenUrls.push(request.url);
698 seenProofs.push(decodeRequestProof(mintRequest.proof));
699 return HttpClientResponse.fromWeb(
700 request,
701 Response.json(signMintResponse(mintRequest), { status: 200 }),
702 );
703 });
704
705 return Effect.gen(function* () {
706 const connector = yield* EnvironmentConnector.EnvironmentConnector;
707 const result = yield* connector.connect({
708 userId: "user_123",
709 environmentId: "env-connector-test",
710 clientProofKeyThumbprint: "client-proof-key-thumbprint",
711 deviceId: "device-123",
712 });
713
714 expect(seenUrls).toEqual(["https://env.example.test/api/t3-connect/mint-credential"]);
715 expect(seenProofs[0]).toMatchObject({
716 iss: "https://relay.example.test",
717 aud: "t3-env:env-connector-test",
718 sub: "user_123",
719 environmentId: "env-connector-test",
720 clientProofKeyThumbprint: "client-proof-key-thumbprint",
721 cnf: { jkt: "client-proof-key-thumbprint" },
722 deviceId: "device-123",
723 scope: ["environment:connect"],
724 });
725 expect(result).toMatchObject({
726 environmentId: "env-connector-test",
727 credential: "pairing_credential",
728 endpoint: {
729 httpBaseUrl: "https://env.example.test/",
730 wsBaseUrl: "wss://env.example.test/ws",
731 },
732 });
733 }).pipe(Effect.provide(layerConnectorTest(execute)));
734 });
735
736 it.effect("only accepts mint responses signed by the user's linked environment key", () => {
737 const execute = (request: HttpClientRequest.HttpClientRequest) =>
738 Effect.sync(() => {
739 const mintRequest = decodeMintRequestBody(requestBodyText(request));
740 return HttpClientResponse.fromWeb(
741 request,
742 Response.json(signMintResponse(mintRequest, {}, otherEnvironmentKeyPair.privateKey), {
743 status: 200,
744 }),
745 );
746 });
747
748 return Effect.gen(function* () {
749 const connector = yield* EnvironmentConnector.EnvironmentConnector;
750 const result = yield* Effect.exit(
751 connector.connect({
752 userId: "user_123",
753 environmentId: "env-connector-test",
754 clientProofKeyThumbprint: "client-proof-key-thumbprint",
755 }),
756 );
757
758 expect(result._tag).toBe("Failure");
759 if (result._tag === "Failure") {
760 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
761 }
762 }).pipe(Effect.provide(layerConnectorTest(execute)));
763 });
764
765 it.effect("rejects mint responses when the linked environment public key is malformed", () => {
766 const execute = (request: HttpClientRequest.HttpClientRequest) =>
767 Effect.sync(() => {
768 const mintRequest = decodeMintRequestBody(requestBodyText(request));
769 return HttpClientResponse.fromWeb(
770 request,
771 Response.json(signMintResponse(mintRequest), { status: 200 }),
772 );
773 });
774
775 return Effect.gen(function* () {
776 const connector = yield* EnvironmentConnector.EnvironmentConnector;
777 const result = yield* Effect.exit(
778 connector.connect({
779 userId: "user_123",
780 environmentId: "env-connector-test",
781 clientProofKeyThumbprint: "client-proof-key-thumbprint",
782 }),
783 );
784
785 expect(result._tag).toBe("Failure");
786 if (result._tag === "Failure") {
787 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
788 }
789 }).pipe(
790 Effect.provide(
791 layerConnectorTest(execute, {
792 links: makeLinks({
793 environmentPublicKey: "not a pem public key",
794 }),
795 }),
796 ),
797 );
798 });
799
800 it.effect("rejects environment mint responses with an overlong credential window", () => {
801 const execute = (request: HttpClientRequest.HttpClientRequest) =>
802 Effect.sync(() => {
803 const mintRequest = decodeMintRequestBody(requestBodyText(request));
804 return HttpClientResponse.fromWeb(
805 request,
806 Response.json(
807 { ...signMintResponse(mintRequest), expiresAt: "2999-01-01T00:00:00.000Z" },
808 { status: 200 },
809 ),
810 );
811 });
812
813 return Effect.gen(function* () {
814 const connector = yield* EnvironmentConnector.EnvironmentConnector;
815 const result = yield* Effect.exit(
816 connector.connect({
817 userId: "user_123",
818 environmentId: "env-connector-test",
819 clientProofKeyThumbprint: "client-proof-key-thumbprint",
820 }),
821 );
822
823 expect(result._tag).toBe("Failure");
824 if (result._tag === "Failure") {
825 expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
826 }
827 }).pipe(Effect.provide(layerConnectorTest(execute)));
828 });
829
830 it.effect("times out hung managed endpoint mint requests", () => {
831 let resolveRequestStarted: (() => void) | undefined;
832 const requestStarted = new Promise<void>((resolve) => {
833 resolveRequestStarted = () => resolve();
834 });
835 const execute = () =>
836 Effect.sync(() => {
837 resolveRequestStarted?.();
838 }).pipe(Effect.andThen(Effect.never as Effect.Effect<HttpClientResponse.HttpClientResponse>));
839
840 return Effect.gen(function* () {
841 const connector = yield* EnvironmentConnector.EnvironmentConnector;
842 const resultFiber = yield* connector
843 .connect({
844 userId: "user_123",
845 environmentId: "env-connector-test",
846 clientProofKeyThumbprint: "client-proof-key-thumbprint",
847 })
848 .pipe(Effect.result, Effect.forkScoped);
849
850 yield* Effect.promise(() => requestStarted);
851 yield* TestClock.adjust(
852 Duration.millis(EnvironmentConnector.ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS),
853 );
854 const result = yield* Fiber.join(resultFiber);
855
856 expect(Result.isFailure(result)).toBe(true);
857 if (Result.isFailure(result)) {
858 expect(result.failure._tag).toBe("EnvironmentMintRequestTimedOut");
859 expect(result.failure).toMatchObject({
860 environmentId: "env-connector-test",
861 timeoutMs: EnvironmentConnector.ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS,
862 });
863 }
864 }).pipe(Effect.provide(Layer.merge(TestClock.layer(), layerConnectorTest(execute))));
865 });
866});