1import * as NodeServices from "@effect/platform-node/NodeServices";
2
3import { describe, expect, it } from "@effect/vitest";
4import * as Alchemy from "alchemy";
5import * as Cloudflare from "alchemy/Cloudflare";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Redacted from "effect/Redacted";
9
10import * as RelayConfiguration from "../Config.ts";
11import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
12import * as ManagedEndpointProvider from "./ManagedEndpointProvider.ts";
13import * as ManagedTunnelLimits from "./ManagedTunnelLimits.ts";
14
15const config = RelayConfiguration.RelayConfiguration.of({
16 relayIssuer: "https://relay.example.test",
17 apns: {
18 environment: "sandbox",
19 teamId: "team-id",
20 keyId: "key-id",
21 privateKey: Redacted.make("private-key"),
22 bundleId: "com.t3tools.t3code.dev",
23 },
24 apnsDeliveryJobSigningSecret: Redacted.make("job-secret"),
25 clerkSecretKey: Redacted.make("clerk-secret"),
26 clerkPublishableKey: "pk_test_test",
27 clerkJwtAudience: "t3-code-relay",
28 cloudMintPrivateKey: Redacted.make("cloud-private-key"),
29 cloudMintPublicKey: "cloud-public-key",
30 managedEndpointBaseDomain: "t3code.test",
31 managedEndpointNamespace: "dev_julius",
32});
33
34interface TunnelCall {
35 readonly operation: "get" | "list" | "create" | "putConfiguration" | "getToken" | "delete";
36 readonly input: unknown;
37}
38
39interface DnsCall {
40 readonly operation: "listRecords" | "createRecord" | "updateRecord" | "deleteRecord";
41 readonly input: unknown;
42}
43
44interface AllocationCall {
45 readonly operation:
46 | "get"
47 | "reserve"
48 | "recordTunnel"
49 | "recordDns"
50 | "markReady"
51 | "claimRelease"
52 | "claimDeprovision"
53 | "remove"
54 | "removeClaimed";
55 readonly input: unknown;
56}
57
58function allocationKey(input: { readonly userId: string; readonly environmentId: string }) {
59 return `${input.userId}:${input.environmentId}`;
60}
61
62function makeTunnelClient(calls: TunnelCall[] = []) {
63 return ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
64 get: (tunnelId) =>
65 Effect.sync(() => {
66 calls.push({ operation: "get", input: tunnelId });
67 return {
68 id: tunnelId,
69 name: "managed-tunnel",
70 status: "down",
71 connsInactiveAt: "2026-06-01T00:00:00.000Z",
72 };
73 }),
74 list: (request) =>
75 Effect.sync(() => {
76 calls.push({ operation: "list", input: request });
77 return { result: [] };
78 }),
79 create: (request) =>
80 Effect.sync(() => {
81 calls.push({ operation: "create", input: request });
82 return { id: "tunnel-id", name: request.name };
83 }),
84 putConfiguration: (tunnelId, tunnelConfig) =>
85 Effect.sync(() => {
86 calls.push({ operation: "putConfiguration", input: { tunnelId, tunnelConfig } });
87 }),
88 getToken: (tunnelId) =>
89 Effect.sync(() => {
90 calls.push({ operation: "getToken", input: tunnelId });
91 return "connector-token";
92 }),
93 delete: (tunnelId) =>
94 Effect.sync(() => {
95 calls.push({ operation: "delete", input: tunnelId });
96 }),
97 });
98}
99
100function makePersistentTunnelClient(calls: TunnelCall[] = []) {
101 let tunnel: { readonly id: string; readonly name: string } | null = null;
102 return ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
103 get: (tunnelId) =>
104 Effect.suspend(() => {
105 calls.push({ operation: "get", input: tunnelId });
106 return tunnel === null
107 ? Effect.fail(
108 new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
109 operation: "get",
110 tunnelId,
111 cause: { _tag: "NotFound" },
112 }),
113 )
114 : Effect.succeed({
115 ...tunnel,
116 status: "down",
117 connsInactiveAt: "2026-06-01T00:00:00.000Z",
118 });
119 }),
120 list: (request) =>
121 Effect.sync(() => {
122 calls.push({ operation: "list", input: request });
123 return { result: tunnel === null ? [] : [tunnel] };
124 }),
125 create: (request) =>
126 Effect.sync(() => {
127 calls.push({ operation: "create", input: request });
128 tunnel = { id: "tunnel-id", name: request.name };
129 return tunnel;
130 }),
131 putConfiguration: (tunnelId, tunnelConfig) =>
132 Effect.sync(() => {
133 calls.push({ operation: "putConfiguration", input: { tunnelId, tunnelConfig } });
134 }),
135 getToken: (tunnelId) =>
136 Effect.sync(() => {
137 calls.push({ operation: "getToken", input: tunnelId });
138 return "connector-token";
139 }),
140 delete: (tunnelId) =>
141 Effect.sync(() => {
142 calls.push({ operation: "delete", input: tunnelId });
143 tunnel = null;
144 }),
145 });
146}
147
148function makeDnsClient(
149 calls: DnsCall[] = [],
150 records: ReadonlyArray<{ readonly id: string }> = [],
151) {
152 let currentRecords = [...records];
153 return ManagedEndpointProvider.ManagedEndpointDnsClient.of({
154 listRecords: (hostname) =>
155 Effect.sync(() => {
156 calls.push({ operation: "listRecords", input: hostname });
157 return currentRecords;
158 }),
159 createRecord: (request) =>
160 Effect.sync(() => {
161 calls.push({ operation: "createRecord", input: request });
162 const record = { id: "created-record-id" };
163 currentRecords = [record];
164 return record;
165 }),
166 updateRecord: (dnsRecordId, request) =>
167 Effect.gen(function* () {
168 calls.push({ operation: "updateRecord", input: { dnsRecordId, request } });
169 if (!currentRecords.some((record) => record.id === dnsRecordId)) {
170 return yield* new ManagedEndpointProvider.ManagedEndpointDnsClientError({
171 operation: "update-record",
172 hostname: request.name,
173 dnsRecordId,
174 cause: { _tag: "NotFound", dnsRecordId },
175 });
176 }
177 }),
178 deleteRecord: (dnsRecordId) =>
179 Effect.sync(() => {
180 calls.push({ operation: "deleteRecord", input: dnsRecordId });
181 currentRecords = currentRecords.filter((record) => record.id !== dnsRecordId);
182 }),
183 });
184}
185
186function makeAllocations(calls: AllocationCall[] = []) {
187 const allocations = new Map<string, ManagedEndpointAllocations.ManagedEndpointAllocation>();
188 const recoveryEnabled = new Set<string>();
189 let generation = 0;
190 const mutate = (
191 key: string,
192 change: (
193 allocation: ManagedEndpointAllocations.ManagedEndpointAllocation,
194 ) => ManagedEndpointAllocations.ManagedEndpointAllocation,
195 ) => {
196 const allocation = allocations.get(key);
197 if (allocation !== undefined) {
198 allocations.set(key, {
199 ...change(allocation),
200 generation: allocation.generation + 1,
201 updatedAt: `generation-${++generation}`,
202 });
203 }
204 };
205 return ManagedEndpointAllocations.ManagedEndpointAllocations.of({
206 getByTunnelName: () => Effect.die("unused getByTunnelName"),
207 get: (input) =>
208 Effect.sync(() => {
209 calls.push({ operation: "get", input });
210 return allocations.get(allocationKey(input)) ?? null;
211 }),
212 reserve: (input) =>
213 Effect.sync(() => {
214 calls.push({ operation: "reserve", input });
215 const allocation = allocations.get(allocationKey(input)) ?? {
216 ...input,
217 tunnelId: null,
218 dnsRecordId: null,
219 readyAt: null,
220 origin: null,
221 updatedAt: `generation-${++generation}`,
222 generation: 0,
223 tunnelReleasedAt: null,
224 };
225 allocations.set(allocationKey(input), allocation);
226 return allocation;
227 }),
228 recordTunnel: (input) =>
229 Effect.sync(() => {
230 calls.push({ operation: "recordTunnel", input });
231 const current = allocations.get(allocationKey(input));
232 if (current?.generation !== input.generation) {
233 return null;
234 }
235 mutate(allocationKey(input), (allocation) => ({
236 ...allocation,
237 tunnelId: input.tunnelId,
238 readyAt: allocation.tunnelId === input.tunnelId ? allocation.readyAt : null,
239 }));
240 return allocations.get(allocationKey(input))?.generation ?? null;
241 }),
242 recordDns: (input) =>
243 Effect.sync(() => {
244 calls.push({ operation: "recordDns", input });
245 const current = allocations.get(allocationKey(input));
246 if (current?.generation !== input.generation || current.tunnelId !== input.tunnelId) {
247 return null;
248 }
249 mutate(allocationKey(input), (allocation) => ({
250 ...allocation,
251 dnsRecordId: input.dnsRecordId,
252 }));
253 return allocations.get(allocationKey(input))?.generation ?? null;
254 }),
255 markReady: (input) =>
256 Effect.sync(() => {
257 calls.push({ operation: "markReady", input });
258 const current = allocations.get(allocationKey(input));
259 if (current?.generation !== input.generation || current.tunnelId !== input.tunnelId) {
260 return false;
261 }
262 mutate(allocationKey(input), (allocation) => ({
263 ...allocation,
264 readyAt: "2026-06-02T00:00:00.000Z",
265 origin: input.origin,
266 }));
267 return true;
268 }),
269 enableRecovery: (input) =>
270 Effect.sync(() => {
271 const allocation = allocations.get(allocationKey(input));
272 if (allocation?.tunnelId !== input.tunnelId) {
273 return false;
274 }
275 recoveryEnabled.add(allocationKey(input));
276 return true;
277 }),
278 listByTunnelNames: (tunnelNames) =>
279 Effect.sync(() =>
280 [...allocations.values()]
281 .filter((allocation) => tunnelNames.includes(allocation.tunnelName))
282 .map((allocation) => ({
283 ...allocation,
284 recoveryEnabled: recoveryEnabled.has(allocationKey(allocation)),
285 })),
286 ),
287 claimRelease: (input) =>
288 Effect.sync(() => {
289 calls.push({ operation: "claimRelease", input });
290 const allocation = allocations.get(allocationKey(input));
291 if (
292 allocation === undefined ||
293 allocation.tunnelId !== input.tunnelId ||
294 allocation.generation !== input.generation
295 ) {
296 return null;
297 }
298 mutate(allocationKey(input), (current) => current);
299 return allocations.get(allocationKey(input))?.generation ?? null;
300 }),
301 withClaimedTunnel: (input, effect) =>
302 Effect.suspend(() => {
303 const current = allocations.get(allocationKey(input));
304 return current?.tunnelId === input.tunnelId && current.generation === input.generation
305 ? Effect.asSome(effect)
306 : Effect.succeedNone;
307 }),
308 claimDeprovision: (input) =>
309 Effect.sync(() => {
310 calls.push({ operation: "claimDeprovision", input });
311 const allocation = allocations.get(allocationKey(input));
312 if (allocation === undefined || allocation.generation !== input.generation) {
313 return null;
314 }
315 mutate(allocationKey(input), (current) => current);
316 return allocations.get(allocationKey(input))?.generation ?? null;
317 }),
318 remove: (input) =>
319 Effect.sync(() => {
320 calls.push({ operation: "remove", input });
321 allocations.delete(allocationKey(input));
322 }),
323 removeClaimed: (input) =>
324 Effect.sync(() => {
325 calls.push({ operation: "removeClaimed", input });
326 const allocation = allocations.get(allocationKey(input));
327 if (allocation === undefined || allocation.generation !== input.generation) {
328 return false;
329 }
330 allocations.delete(allocationKey(input));
331 return true;
332 }),
333 });
334}
335
336function makeTunnelLimits(
337 calls: Array<{ readonly userId: string; readonly environmentId: string }> = [],
338 result: ManagedTunnelLimits.ManagedTunnelLimitExceeded | null = null,
339) {
340 return ManagedTunnelLimits.ManagedTunnelLimits.of({
341 ensureCapacity: (input) =>
342 Effect.suspend(() => {
343 calls.push(input);
344 return result === null ? Effect.void : Effect.fail(result);
345 }),
346 });
347}
348
349function layerProvider(
350 tunnelClient = makeTunnelClient(),
351 dnsClient = makeDnsClient(),
352 allocations = makeAllocations(),
353 tunnelLimits = makeTunnelLimits(),
354) {
355 return ManagedEndpointProvider.layer.pipe(
356 Layer.provideMerge(NodeServices.layer),
357 Layer.provide(RelayConfiguration.layer(config)),
358 Layer.provide(ManagedEndpointProvider.layerTunnelClient(tunnelClient)),
359 Layer.provide(ManagedEndpointProvider.layerDnsClient(dnsClient)),
360 Layer.provide(
361 Layer.succeed(ManagedEndpointAllocations.ManagedEndpointAllocations, allocations),
362 ),
363 Layer.provide(Layer.succeed(ManagedTunnelLimits.ManagedTunnelLimits, tunnelLimits)),
364 );
365}
366
367// First 16 hex chars of SHA-256(`dev_julius:${userId}:${environmentId}`), pinned so a
368// change to the endpoint naming scheme fails here instead of silently matching.
369const MANAGED_ENDPOINT_HASHES: Record<string, string> = {
370 "user_ABC:env_ABC": "d101ac68108a423e",
371 "user_ABC:env_shared": "d7e6356aaf8863aa",
372 "user_DEF:env_shared": "a2fc84ac1b8c1c35",
373};
374
375function expectedManagedEndpointHash(environmentId: string, userId: string): string {
376 const hash = MANAGED_ENDPOINT_HASHES[`${userId}:${environmentId}`];
377 if (hash === undefined) throw new Error(`No pinned hash for ${userId}:${environmentId}`);
378 return hash;
379}
380
381function expectedManagedHostname(environmentId: string, userId = "user_ABC"): string {
382 return `dev-julius-${expectedManagedEndpointHash(environmentId, userId)}.t3code.test`;
383}
384
385function expectedManagedTunnelName(environmentId: string, userId = "user_ABC"): string {
386 return `t3coderelay-managedendpoint-dev-julius-${expectedManagedEndpointHash(environmentId, userId)}`;
387}
388
389describe("ManagedEndpointProvider", () => {
390 it.effect("does not require the deployment RuntimeContext when building the Worker layer", () => {
391 const tunnelClient = {
392 get: () => Effect.succeed({ id: "tunnel-id", name: "managed-tunnel" }),
393 list: () => Effect.succeed({ result: [] }),
394 create: (request: { readonly name: string }) =>
395 Effect.succeed({ id: "tunnel-id", name: request.name }),
396 putConfiguration: () => Effect.void,
397 getToken: () => Effect.succeed("connector-token"),
398 delete: () => Effect.void,
399 } as unknown as Cloudflare.Tunnel.ReadWriteTunnelClient;
400 const dnsClient = {
401 listDnsRecords: () => Effect.succeed({ result: [] }),
402 createDnsRecord: () => Effect.succeed({ id: "dns-record-id" }),
403 updateDnsRecord: () => Effect.void,
404 deleteDnsRecord: () => Effect.void,
405 } as unknown as Cloudflare.DNS.ReadWriteDnsClient;
406 const runtimeContext = {} as Alchemy.BaseRuntimeContext;
407 const layer = ManagedEndpointProvider.layerCloudflareBindings(
408 tunnelClient,
409 dnsClient,
410 runtimeContext,
411 ).pipe(
412 Layer.provideMerge(NodeServices.layer),
413 Layer.provide(RelayConfiguration.layer(config)),
414 Layer.provide(
415 Layer.succeed(ManagedEndpointAllocations.ManagedEndpointAllocations, makeAllocations()),
416 ),
417 Layer.provide(Layer.succeed(ManagedTunnelLimits.ManagedTunnelLimits, makeTunnelLimits())),
418 );
419
420 return Effect.gen(function* () {
421 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
422 const result = yield* provider.provision({
423 userId: "user_ABC",
424 environmentId: "env_ABC",
425 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
426 });
427
428 expect(result.runtime.connectorToken).toBe("connector-token");
429 }).pipe(Effect.provide(layer));
430 });
431
432 it.effect("provisions a Cloudflare tunnel endpoint and connector token", () => {
433 const tunnelCalls: TunnelCall[] = [];
434 const dnsCalls: DnsCall[] = [];
435 const allocationCalls: AllocationCall[] = [];
436
437 return Effect.gen(function* () {
438 const hostname = expectedManagedHostname("env_ABC");
439 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
440 const result = yield* provider.provision({
441 userId: "user_ABC",
442 environmentId: "env_ABC",
443 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
444 });
445
446 expect(result).toEqual({
447 endpoint: {
448 httpBaseUrl: `https://${hostname}/`,
449 wsBaseUrl: `wss://${hostname}/ws`,
450 providerKind: "cloudflare_tunnel",
451 },
452 runtime: {
453 providerKind: "cloudflare_tunnel",
454 connectorToken: "connector-token",
455 tunnelId: "tunnel-id",
456 tunnelName: expectedManagedTunnelName("env_ABC"),
457 },
458 });
459 expect(dnsCalls).toEqual([
460 { operation: "listRecords", input: hostname },
461 {
462 operation: "createRecord",
463 input: {
464 type: "CNAME",
465 name: hostname,
466 content: "tunnel-id.cfargotunnel.com",
467 ttl: 1,
468 proxied: true,
469 },
470 },
471 ]);
472 expect(tunnelCalls.map((call) => call.operation)).toEqual([
473 "list",
474 "create",
475 "putConfiguration",
476 "getToken",
477 ]);
478 expect(tunnelCalls[2]?.input).toMatchObject({
479 tunnelConfig: {
480 ingress: [
481 {
482 hostname,
483 service: "http://127.0.0.1:3773",
484 },
485 { service: "http_status:404" },
486 ],
487 },
488 });
489 expect(tunnelCalls[0]?.input).toEqual({
490 name: expectedManagedTunnelName("env_ABC"),
491 isDeleted: false,
492 });
493 expect(allocationCalls.map((call) => call.operation)).toEqual([
494 "reserve",
495 "recordTunnel",
496 "recordDns",
497 "markReady",
498 ]);
499 }).pipe(
500 Effect.provide(
501 layerProvider(
502 makeTunnelClient(tunnelCalls),
503 makeDnsClient(dnsCalls),
504 makeAllocations(allocationCalls),
505 ),
506 ),
507 );
508 });
509
510 it.effect("checks the managed tunnel limit before reserving an allocation", () => {
511 const limitCalls: Array<{ readonly userId: string; readonly environmentId: string }> = [];
512
513 return Effect.gen(function* () {
514 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
515 yield* provider.provision({
516 userId: "user_ABC",
517 environmentId: "env_ABC",
518 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
519 });
520
521 expect(limitCalls).toEqual([{ userId: "user_ABC", environmentId: "env_ABC" }]);
522 }).pipe(
523 Effect.provide(
524 layerProvider(
525 makeTunnelClient(),
526 makeDnsClient(),
527 makeAllocations(),
528 makeTunnelLimits(limitCalls),
529 ),
530 ),
531 );
532 });
533
534 it.effect("refuses to provision past the managed tunnel limit without side effects", () => {
535 const tunnelCalls: TunnelCall[] = [];
536 const dnsCalls: DnsCall[] = [];
537 const allocationCalls: AllocationCall[] = [];
538 const exceeded = new ManagedTunnelLimits.ManagedTunnelLimitExceeded({
539 userId: "user_ABC",
540 environmentId: "env_ABC",
541 maxTunnels: 10,
542 activeTunnels: 10,
543 });
544
545 return Effect.gen(function* () {
546 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
547 const error = yield* Effect.flip(
548 provider.provision({
549 userId: "user_ABC",
550 environmentId: "env_ABC",
551 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
552 }),
553 );
554
555 expect(error).toBe(exceeded);
556 expect(tunnelCalls).toEqual([]);
557 expect(dnsCalls).toEqual([]);
558 expect(allocationCalls).toEqual([]);
559 }).pipe(
560 Effect.provide(
561 layerProvider(
562 makeTunnelClient(tunnelCalls),
563 makeDnsClient(dnsCalls),
564 makeAllocations(allocationCalls),
565 makeTunnelLimits([], exceeded),
566 ),
567 ),
568 );
569 });
570
571 it.effect("uses stage-scoped stable names without leaking unusual environment ids", () => {
572 const tunnelCalls: TunnelCall[] = [];
573
574 return Effect.gen(function* () {
575 const environmentId = "ENV With Spaces/../Symbols!" + "x".repeat(80);
576 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
577 yield* provider.provision({
578 userId: "user_ABC",
579 environmentId,
580 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
581 });
582
583 const requestedName = (
584 tunnelCalls.find((call) => call.operation === "list")?.input as
585 | { readonly name?: string }
586 | undefined
587 )?.name;
588 expect(requestedName).toMatch(/^t3coderelay-managedendpoint-dev-julius-[a-f0-9]{16}$/);
589 const configBody = (
590 tunnelCalls.find((call) => call.operation === "putConfiguration")?.input as
591 | { readonly tunnelConfig?: unknown }
592 | undefined
593 )?.tunnelConfig;
594 expect(configBody).toMatchObject({
595 ingress: [
596 {
597 hostname: expect.stringMatching(/^dev-julius-[a-f0-9]{16}\.t3code\.test$/),
598 },
599 { service: "http_status:404" },
600 ],
601 });
602 const hostname = (
603 configBody as
604 | {
605 readonly ingress?: readonly [{ readonly hostname?: unknown }, unknown];
606 }
607 | undefined
608 )?.ingress?.[0]?.hostname;
609 expect(typeof hostname === "string" ? hostname.split(".")[0]?.length : 0).toBeLessThanOrEqual(
610 63,
611 );
612 expect(tunnelCalls.find((call) => call.operation === "create")?.input).toMatchObject({
613 name: requestedName,
614 configSrc: "cloudflare",
615 });
616 }).pipe(Effect.provide(layerProvider(makeTunnelClient(tunnelCalls))));
617 });
618
619 it.effect("formats IPv6 loopback origins as valid Cloudflare ingress service URLs", () => {
620 const tunnelCalls: TunnelCall[] = [];
621
622 return Effect.gen(function* () {
623 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
624 yield* provider.provision({
625 userId: "user_ABC",
626 environmentId: "env-ipv6",
627 origin: { localHttpHost: "::1", localHttpPort: 3773 },
628 });
629
630 expect(
631 tunnelCalls.find((call) => call.operation === "putConfiguration")?.input,
632 ).toMatchObject({
633 tunnelConfig: {
634 ingress: [
635 {
636 service: "http://[::1]:3773",
637 },
638 { service: "http_status:404" },
639 ],
640 },
641 });
642 }).pipe(Effect.provide(layerProvider(makeTunnelClient(tunnelCalls))));
643 });
644
645 it.effect("rejects non-loopback managed endpoint origins before calling Cloudflare", () => {
646 const dnsCalls: DnsCall[] = [];
647
648 return Effect.gen(function* () {
649 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
650 const result = yield* Effect.result(
651 provider.provision({
652 userId: "user_ABC",
653 environmentId: "env_ABC",
654 origin: { localHttpHost: "192.168.1.10", localHttpPort: 3773 },
655 }),
656 );
657
658 expect(dnsCalls).toHaveLength(0);
659 expect(result._tag).toBe("Failure");
660 if (result._tag === "Failure") {
661 expect(result.failure).toMatchObject({
662 _tag: "ManagedEndpointOriginNotAllowed",
663 userId: "user_ABC",
664 environmentId: "env_ABC",
665 host: "192.168.1.10",
666 port: 3773,
667 });
668 }
669 }).pipe(Effect.provide(layerProvider(makeTunnelClient(), makeDnsClient(dnsCalls))));
670 });
671
672 it.effect("rejects invalid managed endpoint origin ports before calling Cloudflare", () => {
673 const dnsCalls: DnsCall[] = [];
674
675 return Effect.gen(function* () {
676 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
677 const result = yield* Effect.result(
678 provider.provision({
679 userId: "user_ABC",
680 environmentId: "env_ABC",
681 origin: { localHttpHost: "127.0.0.1", localHttpPort: 65_536 },
682 }),
683 );
684
685 expect(dnsCalls).toHaveLength(0);
686 expect(result._tag).toBe("Failure");
687 if (result._tag === "Failure") {
688 expect(result.failure._tag).toBe("ManagedEndpointOriginNotAllowed");
689 }
690 }).pipe(Effect.provide(layerProvider(makeTunnelClient(), makeDnsClient(dnsCalls))));
691 });
692
693 it.effect("reconciles an existing same-host DNS record through the DNS client", () => {
694 const dnsCalls: DnsCall[] = [];
695 return Effect.gen(function* () {
696 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
697 yield* provider.provision({
698 userId: "user_ABC",
699 environmentId: "env_ABC",
700 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
701 });
702
703 expect(dnsCalls.map((call) => call.operation)).toEqual(["listRecords", "updateRecord"]);
704 expect(dnsCalls[1]?.input).toMatchObject({ dnsRecordId: "existing-record-id" });
705 }).pipe(
706 Effect.provide(
707 layerProvider(makeTunnelClient(), makeDnsClient(dnsCalls, [{ id: "existing-record-id" }])),
708 ),
709 );
710 });
711
712 it.effect("reuses checkpointed resources when provisioning is retried", () => {
713 const tunnelCalls: TunnelCall[] = [];
714 const dnsCalls: DnsCall[] = [];
715 const allocationCalls: AllocationCall[] = [];
716 const layer = layerProvider(
717 makePersistentTunnelClient(tunnelCalls),
718 makeDnsClient(dnsCalls),
719 makeAllocations(allocationCalls),
720 );
721
722 return Effect.gen(function* () {
723 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
724 const request = {
725 userId: "user_ABC",
726 environmentId: "env_ABC",
727 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
728 } as const;
729 yield* provider.provision(request);
730 yield* provider.provision(request);
731
732 expect(tunnelCalls.map((call) => call.operation)).toEqual([
733 "list",
734 "create",
735 "putConfiguration",
736 "getToken",
737 "list",
738 "putConfiguration",
739 "getToken",
740 ]);
741 expect(dnsCalls.map((call) => call.operation)).toEqual([
742 "listRecords",
743 "createRecord",
744 "updateRecord",
745 ]);
746 expect(allocationCalls.map((call) => call.operation)).toEqual([
747 "reserve",
748 "recordTunnel",
749 "recordDns",
750 "markReady",
751 "reserve",
752 "recordTunnel",
753 "recordDns",
754 "markReady",
755 ]);
756 }).pipe(Effect.provide(layer));
757 });
758
759 it.effect("recreates a checkpointed DNS record when it was removed externally", () => {
760 const dnsCalls: DnsCall[] = [];
761 const allocationCalls: AllocationCall[] = [];
762 const dnsClient = makeDnsClient(dnsCalls);
763 const layer = layerProvider(
764 makePersistentTunnelClient(),
765 dnsClient,
766 makeAllocations(allocationCalls),
767 );
768
769 return Effect.gen(function* () {
770 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
771 const request = {
772 userId: "user_ABC",
773 environmentId: "env_ABC",
774 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
775 } as const;
776 yield* provider.provision(request);
777 yield* dnsClient.deleteRecord("created-record-id");
778 yield* provider.provision(request);
779
780 expect(dnsCalls.map((call) => call.operation)).toEqual([
781 "listRecords",
782 "createRecord",
783 "deleteRecord",
784 "updateRecord",
785 "listRecords",
786 "createRecord",
787 ]);
788 }).pipe(Effect.provide(layer));
789 });
790
791 it.effect("does not hide non-not-found checkpoint update failures", () => {
792 const dnsCalls: DnsCall[] = [];
793 const failure = new ManagedEndpointProvider.ManagedEndpointDnsClientError({
794 operation: "update-record",
795 dnsRecordId: "created-record-id",
796 cause: new Error("Cloudflare DNS unavailable"),
797 });
798 let records: ReadonlyArray<{ readonly id: string }> = [];
799 const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({
800 listRecords: (hostname) =>
801 Effect.sync(() => {
802 dnsCalls.push({ operation: "listRecords", input: hostname });
803 return records;
804 }),
805 createRecord: (request) =>
806 Effect.sync(() => {
807 dnsCalls.push({ operation: "createRecord", input: request });
808 const record = { id: "created-record-id" };
809 records = [record];
810 return record;
811 }),
812 updateRecord: (dnsRecordId, request) =>
813 Effect.sync(() => {
814 dnsCalls.push({ operation: "updateRecord", input: { dnsRecordId, request } });
815 }).pipe(Effect.andThen(Effect.fail(failure))),
816 deleteRecord: () => Effect.void,
817 });
818 const allocations = makeAllocations();
819 const layer = layerProvider(makePersistentTunnelClient(), dnsClient, allocations);
820
821 return Effect.gen(function* () {
822 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
823 const request = {
824 userId: "user_ABC",
825 environmentId: "env_ABC",
826 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
827 } as const;
828 yield* provider.provision(request);
829 const error = yield* Effect.flip(provider.provision(request));
830
831 expect(error).toMatchObject({
832 _tag: "ManagedEndpointProvisioningFailed",
833 stage: "ensure-dns-record",
834 userId: "user_ABC",
835 environmentId: "env_ABC",
836 });
837 expect(dnsCalls.map((call) => call.operation)).toEqual([
838 "listRecords",
839 "createRecord",
840 "updateRecord",
841 ]);
842 expect(yield* allocations.get(request)).toMatchObject({
843 tunnelId: "tunnel-id",
844 readyAt: "2026-06-02T00:00:00.000Z",
845 });
846 }).pipe(Effect.provide(layer));
847 });
848
849 it.effect(
850 "deprovisions checkpointed DNS and tunnel resources before removing the allocation",
851 () => {
852 const tunnelCalls: TunnelCall[] = [];
853 const dnsCalls: DnsCall[] = [];
854 const allocationCalls: AllocationCall[] = [];
855 const layer = layerProvider(
856 makePersistentTunnelClient(tunnelCalls),
857 makeDnsClient(dnsCalls),
858 makeAllocations(allocationCalls),
859 );
860
861 return Effect.gen(function* () {
862 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
863 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
864 yield* provider.provision({
865 ...key,
866 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
867 });
868 yield* provider.deprovision(key);
869
870 expect(dnsCalls.map((call) => call.operation)).toEqual([
871 "listRecords",
872 "createRecord",
873 "deleteRecord",
874 ]);
875 expect(tunnelCalls.map((call) => call.operation)).toEqual([
876 "list",
877 "create",
878 "putConfiguration",
879 "getToken",
880 "delete",
881 ]);
882 expect(allocationCalls.map((call) => call.operation)).toEqual([
883 "reserve",
884 "recordTunnel",
885 "recordDns",
886 "markReady",
887 "get",
888 "claimDeprovision",
889 "removeClaimed",
890 ]);
891 }).pipe(Effect.provide(layer));
892 },
893 );
894
895 it.effect("does not deprovision an allocation superseded by a concurrent relink", () => {
896 const tunnelCalls: TunnelCall[] = [];
897 const dnsCalls: DnsCall[] = [];
898 const allocationCalls: AllocationCall[] = [];
899 const layer = layerProvider(
900 makePersistentTunnelClient(tunnelCalls),
901 makeDnsClient(dnsCalls),
902 makeAllocations(allocationCalls),
903 );
904
905 return Effect.gen(function* () {
906 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
907 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
908 const request = {
909 ...key,
910 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
911 } as const;
912 yield* provider.provision(request);
913 const unlinkTarget = yield* provider.prepareDeprovision(key);
914 expect(unlinkTarget).not.toBeNull();
915 if (unlinkTarget === null) {
916 return;
917 }
918
919 // A relink refreshes the allocation generation after unlink captured its
920 // target but before unlink begins external teardown.
921 yield* provider.provision(request);
922 const tunnelCallCount = tunnelCalls.length;
923 const dnsCallCount = dnsCalls.length;
924 const allocationCallCount = allocationCalls.length;
925
926 yield* provider.deprovision({ ...key, target: unlinkTarget });
927
928 expect(tunnelCalls).toHaveLength(tunnelCallCount);
929 expect(dnsCalls).toHaveLength(dnsCallCount);
930 expect(allocationCalls.slice(allocationCallCount).map((call) => call.operation)).toEqual([
931 "claimDeprovision",
932 ]);
933 }).pipe(Effect.provide(layer));
934 });
935
936 it.effect("releases the tunnel while keeping the allocation, DNS record, and hostname", () => {
937 const tunnelCalls: TunnelCall[] = [];
938 const dnsCalls: DnsCall[] = [];
939 const allocationCalls: AllocationCall[] = [];
940 const layer = layerProvider(
941 makePersistentTunnelClient(tunnelCalls),
942 makeDnsClient(dnsCalls),
943 makeAllocations(allocationCalls),
944 );
945
946 return Effect.gen(function* () {
947 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
948 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
949 const origin = { localHttpHost: "127.0.0.1", localHttpPort: 3773 } as const;
950 const first = yield* provider.provision({ ...key, origin });
951 const released = yield* provider.release(key);
952 const second = yield* provider.provision({ ...key, origin });
953
954 expect(released).toBe(true);
955 expect(second.endpoint).toEqual(first.endpoint);
956 expect(tunnelCalls.map((call) => call.operation)).toEqual([
957 // first provision
958 "list",
959 "create",
960 "putConfiguration",
961 "getToken",
962 // release deletes only the tunnel...
963 "delete",
964 // ...and the next provision recreates it under the same name
965 "list",
966 "create",
967 "putConfiguration",
968 "getToken",
969 ]);
970 // The DNS record survives the release and is repointed, never deleted.
971 expect(dnsCalls.map((call) => call.operation)).toEqual([
972 "listRecords",
973 "createRecord",
974 "updateRecord",
975 ]);
976 expect(allocationCalls.map((call) => call.operation)).not.toContain("remove");
977 // An ordinary release, such as a host shutting down, must not tell the
978 // user to update: that host gets a new tunnel when it starts again.
979 const claims = allocationCalls.filter((call) => call.operation === "claimRelease");
980 expect(claims.map((call) => (call.input as { markReleased?: boolean }).markReleased)).toEqual(
981 [undefined, undefined],
982 );
983 }).pipe(Effect.provide(layer));
984 });
985
986 it.effect("marks the release on the claim that deletes the tunnel when asked", () => {
987 const allocationCalls: AllocationCall[] = [];
988 const layer = layerProvider(
989 makePersistentTunnelClient(),
990 makeDnsClient(),
991 makeAllocations(allocationCalls),
992 );
993
994 return Effect.gen(function* () {
995 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
996 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
997 yield* provider.provision({
998 ...key,
999 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1000 });
1001 expect(yield* provider.release({ ...key, markReleased: true })).toBe(true);
1002
1003 // The first claim only reserves the release; the second, taken with the
1004 // row locked right before the delete, is the one that records it.
1005 const claims = allocationCalls.filter((call) => call.operation === "claimRelease");
1006 expect(claims.map((call) => (call.input as { markReleased?: boolean }).markReleased)).toEqual(
1007 [undefined, true],
1008 );
1009 }).pipe(Effect.provide(layer));
1010 });
1011
1012 it.effect("treats an environment without a recorded tunnel as already released", () => {
1013 const tunnelCalls: TunnelCall[] = [];
1014 const dnsCalls: DnsCall[] = [];
1015 const layer = layerProvider(
1016 makePersistentTunnelClient(tunnelCalls),
1017 makeDnsClient(dnsCalls),
1018 makeAllocations(),
1019 );
1020
1021 return Effect.gen(function* () {
1022 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1023 const released = yield* provider.release({ userId: "user_ABC", environmentId: "env_ABC" });
1024
1025 expect(released).toBe(true);
1026 expect(tunnelCalls).toEqual([]);
1027 expect(dnsCalls).toEqual([]);
1028 }).pipe(Effect.provide(layer));
1029 });
1030
1031 it.effect("keeps the tunnel alive when a concurrent provision outdates the release claim", () => {
1032 const tunnelCalls: TunnelCall[] = [];
1033 const allocations = makeAllocations();
1034 // Simulates a provision racing the release: the allocation generation no
1035 // longer matches what the release loaded, so the claim fails.
1036 const outdated = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1037 ...allocations,
1038 claimRelease: () => Effect.succeed(null),
1039 });
1040 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), outdated);
1041
1042 return Effect.gen(function* () {
1043 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1044 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1045 yield* provider.provision({
1046 ...key,
1047 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1048 });
1049 const released = yield* provider.release(key);
1050
1051 // false tells the caller its connector token is still live, so it must
1052 // keep its runtime config.
1053 expect(released).toBe(false);
1054 expect(tunnelCalls.map((call) => call.operation)).toEqual([
1055 "list",
1056 "create",
1057 "putConfiguration",
1058 "getToken",
1059 ]);
1060 }).pipe(Effect.provide(layer));
1061 });
1062
1063 it.effect("does not release a tunnel when the requested tunnel id is outdated", () => {
1064 const tunnelCalls: TunnelCall[] = [];
1065 const layer = layerProvider(
1066 makePersistentTunnelClient(tunnelCalls),
1067 makeDnsClient(),
1068 makeAllocations(),
1069 );
1070
1071 return Effect.gen(function* () {
1072 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1073 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1074 yield* provider.provision({
1075 ...key,
1076 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1077 });
1078
1079 expect(yield* provider.release({ ...key, expectedTunnelId: "old-tunnel-id" })).toBe(false);
1080 expect(tunnelCalls.map((call) => call.operation)).not.toContain("delete");
1081 }).pipe(Effect.provide(layer));
1082 });
1083
1084 it.effect("keeps a tunnel when a provision replaces an ordinary release generation", () => {
1085 const tunnelCalls: TunnelCall[] = [];
1086 const allocations = makeAllocations();
1087 let replaceAfterClaim = false;
1088 const replaced = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1089 ...allocations,
1090 claimRelease: (input) =>
1091 allocations.claimRelease(input).pipe(
1092 Effect.tap((claimedGeneration) => {
1093 if (claimedGeneration === null || replaceAfterClaim) return Effect.void;
1094 replaceAfterClaim = true;
1095 return allocations
1096 .recordTunnel({
1097 userId: input.userId,
1098 environmentId: input.environmentId,
1099 tunnelId: "replacement-tunnel",
1100 generation: claimedGeneration,
1101 })
1102 .pipe(Effect.asVoid);
1103 }),
1104 ),
1105 });
1106 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), replaced);
1107
1108 return Effect.gen(function* () {
1109 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1110 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1111 yield* provider.provision({
1112 ...key,
1113 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1114 });
1115
1116 expect(yield* provider.release(key)).toBe(false);
1117 expect(tunnelCalls.map((call) => call.operation)).not.toContain("delete");
1118 }).pipe(Effect.provide(layer));
1119 });
1120
1121 it.effect("only confirms the tunnel exists when the registered origin is unchanged", () => {
1122 const tunnelCalls: TunnelCall[] = [];
1123 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls));
1124
1125 return Effect.gen(function* () {
1126 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1127 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1128 const origin = { localHttpHost: "127.0.0.1", localHttpPort: 3773 } as const;
1129 const provisioned = yield* provider.provision({ ...key, origin });
1130 tunnelCalls.length = 0;
1131
1132 expect(
1133 yield* provider.reconcileOrigin({
1134 ...key,
1135 tunnelId: provisioned.runtime.tunnelId!,
1136 origin,
1137 endpoint: provisioned.endpoint,
1138 }),
1139 ).toBe("ready");
1140 expect(tunnelCalls).toEqual([{ operation: "get", input: "tunnel-id" }]);
1141 }).pipe(Effect.provide(layer));
1142 });
1143
1144 it.effect("asks for recovery when the recorded tunnel was deleted", () => {
1145 const tunnelCalls: TunnelCall[] = [];
1146 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls));
1147
1148 return Effect.gen(function* () {
1149 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1150 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1151 const origin = { localHttpHost: "127.0.0.1", localHttpPort: 3773 } as const;
1152 const provisioned = yield* provider.provision({ ...key, origin });
1153 // A shutdown release deletes the tunnel but keeps the recorded id; the
1154 // host was killed before it dropped its stored config.
1155 expect(yield* provider.release(key)).toBe(true);
1156
1157 expect(
1158 yield* provider.reconcileOrigin({
1159 ...key,
1160 tunnelId: provisioned.runtime.tunnelId!,
1161 origin,
1162 endpoint: provisioned.endpoint,
1163 }),
1164 ).toBe("recovery_required");
1165 }).pipe(Effect.provide(layer));
1166 });
1167
1168 it.effect("updates Cloudflare ingress once when the registered port changes", () => {
1169 const tunnelCalls: TunnelCall[] = [];
1170 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls));
1171
1172 return Effect.gen(function* () {
1173 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1174 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1175 const provisioned = yield* provider.provision({
1176 ...key,
1177 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1178 });
1179 tunnelCalls.length = 0;
1180
1181 expect(
1182 yield* provider.reconcileOrigin({
1183 ...key,
1184 tunnelId: provisioned.runtime.tunnelId!,
1185 origin: { localHttpHost: "127.0.0.1", localHttpPort: 4884 },
1186 endpoint: provisioned.endpoint,
1187 }),
1188 ).toBe("ready");
1189 expect(tunnelCalls).toEqual([
1190 { operation: "get", input: "tunnel-id" },
1191 {
1192 operation: "putConfiguration",
1193 input: {
1194 tunnelId: "tunnel-id",
1195 tunnelConfig: {
1196 ingress: [
1197 {
1198 hostname: expectedManagedHostname("env_ABC"),
1199 service: "http://127.0.0.1:4884",
1200 },
1201 { service: "http_status:404" },
1202 ],
1203 },
1204 },
1205 },
1206 ]);
1207 }).pipe(Effect.provide(layer));
1208 });
1209
1210 it.effect("requires recovery when Cloudflare reports the registered tunnel is missing", () => {
1211 const tunnelCalls: TunnelCall[] = [];
1212 const baseTunnelClient = makePersistentTunnelClient(tunnelCalls);
1213 let tunnelMissing = false;
1214 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
1215 ...baseTunnelClient,
1216 putConfiguration: (tunnelId, tunnelConfig) =>
1217 tunnelMissing
1218 ? Effect.fail(
1219 new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
1220 operation: "put-configuration",
1221 tunnelId,
1222 cause: { _tag: "TunnelNotFound" },
1223 }),
1224 )
1225 : baseTunnelClient.putConfiguration(tunnelId, tunnelConfig),
1226 });
1227 const layer = layerProvider(tunnelClient);
1228
1229 return Effect.gen(function* () {
1230 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1231 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1232 const provisioned = yield* provider.provision({
1233 ...key,
1234 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1235 });
1236 tunnelMissing = true;
1237
1238 expect(
1239 yield* provider.reconcileOrigin({
1240 ...key,
1241 tunnelId: provisioned.runtime.tunnelId!,
1242 origin: { localHttpHost: "127.0.0.1", localHttpPort: 4884 },
1243 endpoint: provisioned.endpoint,
1244 }),
1245 ).toBe("recovery_required");
1246 }).pipe(Effect.provide(layer));
1247 });
1248
1249 it.effect("fails origin sync when the allocation generation changes", () => {
1250 const allocations = makeAllocations();
1251 let loseClaim = false;
1252 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1253 ...allocations,
1254 withClaimedTunnel: (input, effect) =>
1255 loseClaim ? Effect.succeedNone : allocations.withClaimedTunnel(input, effect),
1256 });
1257 const layer = layerProvider(makePersistentTunnelClient(), makeDnsClient(), changed);
1258
1259 return Effect.gen(function* () {
1260 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1261 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1262 const provisioned = yield* provider.provision({
1263 ...key,
1264 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1265 });
1266 loseClaim = true;
1267
1268 const error = yield* Effect.flip(
1269 provider.reconcileOrigin({
1270 ...key,
1271 tunnelId: provisioned.runtime.tunnelId!,
1272 origin: { localHttpHost: "127.0.0.1", localHttpPort: 4884 },
1273 endpoint: provisioned.endpoint,
1274 }),
1275 );
1276 expect(error).toMatchObject({
1277 _tag: "ManagedEndpointProvisioningFailed",
1278 stage: "sync-origin",
1279 });
1280 }).pipe(Effect.provide(layer));
1281 });
1282
1283 it.effect("rejects an active endpoint that does not match the allocation hostname", () => {
1284 const layer = layerProvider(makePersistentTunnelClient());
1285
1286 return Effect.gen(function* () {
1287 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1288 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1289 const provisioned = yield* provider.provision({
1290 ...key,
1291 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1292 });
1293
1294 const error = yield* Effect.flip(
1295 provider.reconcileOrigin({
1296 ...key,
1297 tunnelId: provisioned.runtime.tunnelId!,
1298 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1299 endpoint: {
1300 ...provisioned.endpoint,
1301 httpBaseUrl: "https://different-host.t3code.test/",
1302 },
1303 }),
1304 );
1305 expect(error).toMatchObject({
1306 _tag: "ManagedEndpointProvisioningFailed",
1307 stage: "verify-endpoint",
1308 });
1309 }).pipe(Effect.provide(layer));
1310 });
1311
1312 it.effect(
1313 "keeps a newly created tunnel available for retry after losing its allocation claim",
1314 () => {
1315 const tunnelCalls: TunnelCall[] = [];
1316 const allocations = makeAllocations();
1317 let changeGeneration = true;
1318 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1319 ...allocations,
1320 recordTunnel: (input) =>
1321 Effect.gen(function* () {
1322 if (changeGeneration) {
1323 changeGeneration = false;
1324 yield* allocations.claimDeprovision(input);
1325 }
1326 return yield* allocations.recordTunnel(input);
1327 }),
1328 });
1329 const layer = layerProvider(
1330 makePersistentTunnelClient(tunnelCalls),
1331 makeDnsClient(),
1332 changed,
1333 );
1334
1335 return Effect.gen(function* () {
1336 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1337 const input = {
1338 userId: "user_ABC",
1339 environmentId: "env_ABC",
1340 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1341 };
1342 const error = yield* Effect.flip(provider.provision(input));
1343
1344 expect(error).toMatchObject({
1345 _tag: "ManagedEndpointProvisioningFailed",
1346 stage: "record-tunnel",
1347 reason: "claim-lost",
1348 });
1349 expect(tunnelCalls.map((call) => call.operation)).toEqual(["list", "create"]);
1350 expect((yield* provider.provision(input)).runtime.tunnelId).toBe("tunnel-id");
1351 expect(tunnelCalls.filter((call) => call.operation === "create")).toHaveLength(1);
1352 expect(tunnelCalls.filter((call) => call.operation === "delete")).toEqual([]);
1353 }).pipe(Effect.provide(layer));
1354 },
1355 );
1356
1357 it.effect("does not overwrite DNS when tunnel ownership changes during provisioning", () => {
1358 const allocations = makeAllocations();
1359 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1360 ...allocations,
1361 recordDns: () => Effect.succeed(null),
1362 });
1363 const layer = layerProvider(makePersistentTunnelClient(), makeDnsClient(), changed);
1364
1365 return Effect.gen(function* () {
1366 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1367 const error = yield* Effect.flip(
1368 provider.provision({
1369 userId: "user_ABC",
1370 environmentId: "env_ABC",
1371 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1372 }),
1373 );
1374
1375 expect(error).toMatchObject({
1376 _tag: "ManagedEndpointProvisioningFailed",
1377 stage: "record-dns",
1378 });
1379 }).pipe(Effect.provide(layer));
1380 });
1381
1382 it.effect("does not change tunnel ingress after another provision takes ownership", () => {
1383 const tunnelCalls: TunnelCall[] = [];
1384 const allocations = makeAllocations();
1385 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1386 ...allocations,
1387 withClaimedTunnel: () => Effect.succeedNone,
1388 });
1389 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), changed);
1390
1391 return Effect.gen(function* () {
1392 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1393 const error = yield* Effect.flip(
1394 provider.provision({
1395 userId: "user_ABC",
1396 environmentId: "env_ABC",
1397 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1398 }),
1399 );
1400
1401 expect(error).toMatchObject({
1402 _tag: "ManagedEndpointProvisioningFailed",
1403 stage: "configure-tunnel",
1404 reason: "claim-lost",
1405 });
1406 expect(tunnelCalls.map((call) => call.operation)).not.toContain("putConfiguration");
1407 }).pipe(Effect.provide(layer));
1408 });
1409
1410 it.effect("does not change DNS after another provision takes ownership", () => {
1411 const dnsCalls: DnsCall[] = [];
1412 const allocations = makeAllocations();
1413 let lockCount = 0;
1414 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1415 ...allocations,
1416 withClaimedTunnel: (input, effect) =>
1417 ++lockCount === 1 ? allocations.withClaimedTunnel(input, effect) : Effect.succeedNone,
1418 });
1419 const layer = layerProvider(makePersistentTunnelClient(), makeDnsClient(dnsCalls), changed);
1420
1421 return Effect.gen(function* () {
1422 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1423 const error = yield* Effect.flip(
1424 provider.provision({
1425 userId: "user_ABC",
1426 environmentId: "env_ABC",
1427 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1428 }),
1429 );
1430
1431 expect(error).toMatchObject({
1432 _tag: "ManagedEndpointProvisioningFailed",
1433 stage: "record-dns",
1434 });
1435 expect(dnsCalls).toEqual([]);
1436 }).pipe(Effect.provide(layer));
1437 });
1438
1439 it.effect("does not mark a superseded tunnel allocation as ready", () => {
1440 const allocations = makeAllocations();
1441 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1442 ...allocations,
1443 markReady: () => Effect.succeed(false),
1444 });
1445 const layer = layerProvider(makePersistentTunnelClient(), makeDnsClient(), changed);
1446
1447 return Effect.gen(function* () {
1448 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1449 const error = yield* Effect.flip(
1450 provider.provision({
1451 userId: "user_ABC",
1452 environmentId: "env_ABC",
1453 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1454 }),
1455 );
1456
1457 expect(error).toMatchObject({
1458 _tag: "ManagedEndpointProvisioningFailed",
1459 stage: "mark-allocation-ready",
1460 reason: "claim-lost",
1461 });
1462 }).pipe(Effect.provide(layer));
1463 });
1464
1465 it.effect("keeps a tunnel that reconnects before scheduled deletion", () => {
1466 const tunnelCalls: TunnelCall[] = [];
1467 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
1468 ...makePersistentTunnelClient(tunnelCalls),
1469 get: (tunnelId) =>
1470 Effect.succeed({
1471 id: tunnelId,
1472 name: expectedManagedTunnelName("env_ABC"),
1473 status: "healthy",
1474 connsInactiveAt: null,
1475 }),
1476 });
1477 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations());
1478
1479 return Effect.gen(function* () {
1480 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1481 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1482 yield* provider.provision({
1483 ...key,
1484 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1485 });
1486
1487 expect(
1488 yield* provider.release({
1489 ...key,
1490 expectedTunnelId: "tunnel-id",
1491 expectedStatus: "down",
1492 expectedInactiveBefore: "2026-06-01T00:05:00.000Z",
1493 }),
1494 ).toBe(false);
1495 expect(tunnelCalls.map((call) => call.operation)).not.toContain("delete");
1496 }).pipe(Effect.provide(layer));
1497 });
1498
1499 it.effect("keeps a tunnel when a new provision replaces the release generation", () => {
1500 const tunnelCalls: TunnelCall[] = [];
1501 const allocations = makeAllocations();
1502 const replaced = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1503 ...allocations,
1504 claimRelease: (input) =>
1505 allocations.claimRelease(input).pipe(
1506 Effect.tap((claimedGeneration) =>
1507 claimedGeneration === null
1508 ? Effect.void
1509 : allocations
1510 .recordTunnel({
1511 userId: input.userId,
1512 environmentId: input.environmentId,
1513 tunnelId: "replacement-tunnel",
1514 generation: claimedGeneration,
1515 })
1516 .pipe(Effect.asVoid),
1517 ),
1518 ),
1519 });
1520 const layer = layerProvider(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), replaced);
1521
1522 return Effect.gen(function* () {
1523 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1524 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1525 yield* provider.provision({
1526 ...key,
1527 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1528 });
1529
1530 expect(
1531 yield* provider.release({
1532 ...key,
1533 expectedTunnelId: "tunnel-id",
1534 expectedStatus: "down",
1535 expectedInactiveBefore: "2026-06-01T00:05:00.000Z",
1536 }),
1537 ).toBe(false);
1538 expect(tunnelCalls.map((call) => call.operation)).not.toContain("delete");
1539 }).pipe(Effect.provide(layer));
1540 });
1541
1542 it.effect("treats an already deleted tunnel as successfully released", () => {
1543 const notFound = { _tag: "NotFound" } as const;
1544 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
1545 ...makeTunnelClient(),
1546 delete: (tunnelId) =>
1547 Effect.fail(
1548 new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
1549 operation: "delete",
1550 tunnelId,
1551 cause: notFound,
1552 }),
1553 ),
1554 });
1555 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations());
1556
1557 return Effect.gen(function* () {
1558 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1559 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1560 yield* provider.provision({
1561 ...key,
1562 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1563 });
1564 yield* provider.release(key);
1565 }).pipe(Effect.provide(layer));
1566 });
1567
1568 it.effect.each([
1569 { name: "the tunnel is gone", tunnelRemoved: true, released: true },
1570 { name: "the tunnel still exists", tunnelRemoved: false, released: false },
1571 ] as const)(
1572 "resolves a delete whose response was lost by checking whether $name",
1573 ({ tunnelRemoved, released }) => {
1574 const persistent = makePersistentTunnelClient();
1575 const lostResponse = new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
1576 operation: "delete",
1577 tunnelId: "tunnel-id",
1578 cause: { _tag: "TimeoutError" },
1579 });
1580 // Cloudflare may or may not have applied the delete before the
1581 // response was lost; the client only sees the timeout.
1582 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
1583 ...persistent,
1584 delete: (tunnelId) =>
1585 (tunnelRemoved ? persistent.delete(tunnelId) : Effect.void).pipe(
1586 Effect.andThen(Effect.fail(lostResponse)),
1587 ),
1588 });
1589 const allocationCalls: AllocationCall[] = [];
1590 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations(allocationCalls));
1591
1592 return Effect.gen(function* () {
1593 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1594 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1595 yield* provider.provision({
1596 ...key,
1597 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1598 });
1599 const result = yield* Effect.result(provider.release({ ...key, markReleased: true }));
1600
1601 if (released) {
1602 // Succeeding commits the marked claim, so status can explain it.
1603 expect(result).toMatchObject({ _tag: "Success", success: true });
1604 } else {
1605 // Failing rolls the claim back; the tunnel is still there to retry.
1606 expect(result).toMatchObject({
1607 _tag: "Failure",
1608 failure: { stage: "delete-tunnel", cause: lostResponse },
1609 });
1610 }
1611 }).pipe(Effect.provide(layer));
1612 },
1613 );
1614
1615 it.effect("surfaces non-not-found tunnel deletion failures when releasing", () => {
1616 const failure = new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
1617 operation: "delete",
1618 tunnelId: "tunnel-id",
1619 cause: "Cloudflare tunnel deletion failed",
1620 });
1621 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
1622 ...makeTunnelClient(),
1623 delete: () => Effect.fail(failure),
1624 });
1625 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations());
1626
1627 return Effect.gen(function* () {
1628 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1629 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1630 yield* provider.provision({
1631 ...key,
1632 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1633 });
1634 const error = yield* Effect.flip(provider.release(key));
1635
1636 expect(error).toMatchObject({
1637 _tag: "ManagedEndpointDeprovisioningFailed",
1638 stage: "delete-tunnel",
1639 userId: key.userId,
1640 environmentId: key.environmentId,
1641 tunnelId: "tunnel-id",
1642 });
1643 expect(error.cause).toBe(failure);
1644 }).pipe(Effect.provide(layer));
1645 });
1646
1647 it.effect("reports a tunnel with an attached connector as not released", () => {
1648 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
1649 ...makeTunnelClient(),
1650 delete: (tunnelId) =>
1651 Effect.fail(
1652 new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
1653 operation: "delete",
1654 tunnelId,
1655 cause: {
1656 _tag: "BadRequest",
1657 code: 1022,
1658 message:
1659 "This tunnel has active connections. Please stop all cloudflared replicas, or wait a few minutes for connections to close, then try again.",
1660 },
1661 }),
1662 ),
1663 });
1664 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations());
1665
1666 return Effect.gen(function* () {
1667 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1668 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1669 yield* provider.provision({
1670 ...key,
1671 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1672 });
1673 expect(yield* provider.release(key)).toBe(false);
1674 }).pipe(Effect.provide(layer));
1675 });
1676
1677 it.effect("treats an absent allocation as already deprovisioned", () => {
1678 const tunnelCalls: TunnelCall[] = [];
1679 const dnsCalls: DnsCall[] = [];
1680 const allocationCalls: AllocationCall[] = [];
1681 const layer = layerProvider(
1682 makePersistentTunnelClient(tunnelCalls),
1683 makeDnsClient(dnsCalls),
1684 makeAllocations(allocationCalls),
1685 );
1686
1687 return Effect.gen(function* () {
1688 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1689 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1690 yield* provider.deprovision(key);
1691
1692 expect(tunnelCalls).toEqual([]);
1693 expect(dnsCalls).toEqual([]);
1694 expect(allocationCalls).toEqual([{ operation: "get", input: key }]);
1695 }).pipe(Effect.provide(layer));
1696 });
1697
1698 it.effect("keeps the allocation when tunnel cleanup fails so unlink can retry", () => {
1699 const allocationCalls: AllocationCall[] = [];
1700 const tunnelCalls: TunnelCall[] = [];
1701 let deleteAttempts = 0;
1702 const failure = new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
1703 operation: "delete",
1704 tunnelId: "tunnel-id",
1705 cause: "Cloudflare tunnel deletion failed",
1706 });
1707 const tunnels = makePersistentTunnelClient(tunnelCalls);
1708 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
1709 ...tunnels,
1710 delete: (tunnelId) =>
1711 Effect.gen(function* () {
1712 tunnelCalls.push({ operation: "delete", input: tunnelId });
1713 deleteAttempts++;
1714 if (deleteAttempts === 1) {
1715 return yield* failure;
1716 }
1717 }),
1718 });
1719 const layer = layerProvider(tunnelClient, makeDnsClient(), makeAllocations(allocationCalls));
1720
1721 return Effect.gen(function* () {
1722 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1723 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1724 yield* provider.provision({
1725 ...key,
1726 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1727 });
1728 const first = yield* Effect.result(provider.deprovision(key));
1729 expect(first._tag).toBe("Failure");
1730 if (first._tag === "Failure") {
1731 expect(first.failure).toMatchObject({
1732 _tag: "ManagedEndpointDeprovisioningFailed",
1733 stage: "delete-tunnel",
1734 userId: key.userId,
1735 environmentId: key.environmentId,
1736 tunnelId: "tunnel-id",
1737 });
1738 expect(first.failure.cause).toBe(failure);
1739 }
1740 yield* provider.deprovision(key);
1741
1742 expect(allocationCalls.map((call) => call.operation)).toEqual([
1743 "reserve",
1744 "recordTunnel",
1745 "recordDns",
1746 "markReady",
1747 "get",
1748 "claimDeprovision",
1749 "get",
1750 "claimDeprovision",
1751 "removeClaimed",
1752 ]);
1753 }).pipe(Effect.provide(layer));
1754 });
1755
1756 it.effect("treats already deleted remote resources as successfully deprovisioned", () => {
1757 const allocationCalls: AllocationCall[] = [];
1758 const notFound = { _tag: "NotFound" } as const;
1759 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
1760 ...makeTunnelClient(),
1761 delete: () =>
1762 Effect.fail(
1763 new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
1764 operation: "delete",
1765 tunnelId: "tunnel-id",
1766 cause: notFound,
1767 }),
1768 ),
1769 });
1770 const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({
1771 ...makeDnsClient(),
1772 deleteRecord: () =>
1773 Effect.fail(
1774 new ManagedEndpointProvider.ManagedEndpointDnsClientError({
1775 operation: "delete-record",
1776 dnsRecordId: "created-record-id",
1777 cause: notFound,
1778 }),
1779 ),
1780 });
1781 const layer = layerProvider(tunnelClient, dnsClient, makeAllocations(allocationCalls));
1782
1783 return Effect.gen(function* () {
1784 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1785 const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
1786 yield* provider.provision({
1787 ...key,
1788 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1789 });
1790 yield* provider.deprovision(key);
1791
1792 expect(allocationCalls.map((call) => call.operation)).toContain("removeClaimed");
1793 }).pipe(Effect.provide(layer));
1794 });
1795
1796 it.effect("scopes managed endpoint resources by user", () => {
1797 const tunnelCalls: TunnelCall[] = [];
1798
1799 return Effect.gen(function* () {
1800 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1801 yield* provider.provision({
1802 userId: "user_ABC",
1803 environmentId: "env_shared",
1804 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1805 });
1806 yield* provider.provision({
1807 userId: "user_DEF",
1808 environmentId: "env_shared",
1809 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1810 });
1811
1812 expect(
1813 tunnelCalls.filter((call) => call.operation === "list").map((call) => call.input),
1814 ).toEqual([
1815 { name: expectedManagedTunnelName("env_shared", "user_ABC"), isDeleted: false },
1816 { name: expectedManagedTunnelName("env_shared", "user_DEF"), isDeleted: false },
1817 ]);
1818 }).pipe(Effect.provide(layerProvider(makeTunnelClient(tunnelCalls))));
1819 });
1820
1821 it.effect("recovers when DNS creation reports failure after the record became visible", () => {
1822 const dnsCalls: DnsCall[] = [];
1823 const failure = new ManagedEndpointProvider.ManagedEndpointDnsClientError({
1824 operation: "create-record",
1825 hostname: expectedManagedHostname("env_ABC"),
1826 cause: "ambiguous Cloudflare DNS response",
1827 });
1828 let records: ReadonlyArray<{ readonly id: string }> = [];
1829 const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({
1830 listRecords: (hostname) =>
1831 Effect.sync(() => {
1832 dnsCalls.push({ operation: "listRecords", input: hostname });
1833 return records;
1834 }),
1835 createRecord: (request) =>
1836 Effect.gen(function* () {
1837 dnsCalls.push({ operation: "createRecord", input: request });
1838 records = [{ id: "created-record-id" }];
1839 return yield* failure;
1840 }),
1841 updateRecord: (dnsRecordId, request) =>
1842 Effect.sync(() => {
1843 dnsCalls.push({ operation: "updateRecord", input: { dnsRecordId, request } });
1844 }),
1845 deleteRecord: (dnsRecordId) =>
1846 Effect.sync(() => {
1847 dnsCalls.push({ operation: "deleteRecord", input: dnsRecordId });
1848 }),
1849 });
1850
1851 return Effect.gen(function* () {
1852 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1853 yield* provider.provision({
1854 userId: "user_ABC",
1855 environmentId: "env_ABC",
1856 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1857 });
1858
1859 expect(dnsCalls.map((call) => call.operation)).toEqual([
1860 "listRecords",
1861 "createRecord",
1862 "listRecords",
1863 "updateRecord",
1864 ]);
1865 }).pipe(Effect.provide(layerProvider(makeTunnelClient(), dnsClient)));
1866 });
1867
1868 it.effect("reports mismatched tunnel responses without manufacturing a cause", () => {
1869 const dnsCalls: DnsCall[] = [];
1870 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
1871 ...makeTunnelClient(),
1872 create: () => Effect.succeed({ id: "returned-tunnel-id", name: "unexpected-tunnel" }),
1873 });
1874
1875 return Effect.gen(function* () {
1876 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1877 const error = yield* Effect.flip(
1878 provider.provision({
1879 userId: "user_ABC",
1880 environmentId: "env_ABC",
1881 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1882 }),
1883 );
1884
1885 expect(error).toMatchObject({
1886 _tag: "ManagedEndpointProvisioningFailed",
1887 stage: "validate-tunnel-response",
1888 userId: "user_ABC",
1889 environmentId: "env_ABC",
1890 hostname: expectedManagedHostname("env_ABC"),
1891 tunnelName: expectedManagedTunnelName("env_ABC"),
1892 returnedTunnelId: "returned-tunnel-id",
1893 returnedTunnelName: "unexpected-tunnel",
1894 });
1895 if (error._tag === "ManagedEndpointProvisioningFailed") {
1896 expect(error.cause).toBeUndefined();
1897 }
1898 expect(dnsCalls).toHaveLength(0);
1899 }).pipe(Effect.provide(layerProvider(tunnelClient, makeDnsClient(dnsCalls))));
1900 });
1901
1902 it.effect("fails provisioning when the DNS client fails", () => {
1903 const failure = new ManagedEndpointProvider.ManagedEndpointDnsClientError({
1904 operation: "list-records",
1905 hostname: expectedManagedHostname("env_ABC"),
1906 cause: "Cloudflare DNS failure",
1907 });
1908 const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({
1909 listRecords: () => Effect.fail(failure),
1910 createRecord: () => Effect.die("unused"),
1911 updateRecord: () => Effect.die("unused"),
1912 deleteRecord: () => Effect.die("unused"),
1913 });
1914
1915 return Effect.gen(function* () {
1916 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
1917 const error = yield* Effect.flip(
1918 provider.provision({
1919 userId: "user_ABC",
1920 environmentId: "env_ABC",
1921 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
1922 }),
1923 );
1924
1925 expect(error).toMatchObject({
1926 _tag: "ManagedEndpointProvisioningFailed",
1927 stage: "ensure-dns-record",
1928 userId: "user_ABC",
1929 environmentId: "env_ABC",
1930 hostname: expectedManagedHostname("env_ABC"),
1931 tunnelName: expectedManagedTunnelName("env_ABC"),
1932 tunnelId: "tunnel-id",
1933 });
1934 if (error._tag === "ManagedEndpointProvisioningFailed") {
1935 expect(error.cause).toBe(failure);
1936 }
1937 }).pipe(Effect.provide(layerProvider(makeTunnelClient(), dnsClient)));
1938 });
1939});