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