authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 12:30:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 12:30:03-07:00
logd7ff7e73336482d64449f72a4bd31d9ce9f4c01e
tree9db8825bf67c97a0baf7d127f56105f7096d27d1
parentbd8e3ee008e4c78085ebfbf47bf216389a92ac75
signature Signed by PGP key B5690EEEBB952194

[codex] Audit managed endpoint error context (#3245)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 678 insertions(+), 104 deletions(-)

infra/relay/src/environments/ManagedEndpointAllocations.test.ts created+93
...@@ -0,0 +1,93 @@
1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4
5import * as RelayDb from "../db.ts";
6import { relayManagedEndpointAllocations } from "../persistence/schema.ts";
7import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
8
9const layerWithDb = (db: RelayDb.RelayDb["Service"]) =>
10 ManagedEndpointAllocations.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, db)));
11
12describe("ManagedEndpointAllocations", () => {
13 it.effect("retains database failures with allocation operation and identity", () => {
14 const cause = new Error("database unavailable");
15 const fakeDb = {
16 select: () => ({
17 from: (table: unknown) => {
18 expect(table).toBe(relayManagedEndpointAllocations);
19 return {
20 where: () => ({
21 limit: () => Effect.fail(cause),
22 }),
23 };
24 },
25 }),
26 } as unknown as RelayDb.RelayDb["Service"];
27
28 return Effect.gen(function* () {
29 const allocations = yield* ManagedEndpointAllocations.ManagedEndpointAllocations;
30 const error = yield* Effect.flip(
31 allocations.get({ userId: "user-1", environmentId: "environment-1" }),
32 );
33
34 expect(error).toMatchObject({
35 _tag: "ManagedEndpointAllocationPersistenceError",
36 operation: "get",
37 stage: "database-request",
38 userId: "user-1",
39 environmentId: "environment-1",
40 });
41 expect(error.cause).toBe(cause);
42 }).pipe(Effect.provide(layerWithDb(fakeDb)));
43 });
44
45 it.effect("reports an unresolved reservation without manufacturing a cause", () => {
46 const fakeDb = {
47 insert: (table: unknown) => {
48 expect(table).toBe(relayManagedEndpointAllocations);
49 return {
50 values: () => ({
51 onConflictDoNothing: () => ({
52 returning: () => Effect.succeed([]),
53 }),
54 }),
55 };
56 },
57 select: () => ({
58 from: (table: unknown) => {
59 expect(table).toBe(relayManagedEndpointAllocations);
60 return {
61 where: () => ({
62 limit: () => Effect.succeed([]),
63 }),
64 };
65 },
66 }),
67 } as unknown as RelayDb.RelayDb["Service"];
68
69 return Effect.gen(function* () {
70 const allocations = yield* ManagedEndpointAllocations.ManagedEndpointAllocations;
71 const error = yield* Effect.flip(
72 allocations.reserve({
73 userId: "user-1",
74 environmentId: "environment-1",
75 hostname: "environment-1.example.test",
76 tunnelName: "environment-1-tunnel",
77 }),
78 );
79
80 expect(error).toMatchObject({
81 _tag: "ManagedEndpointAllocationPersistenceError",
82 operation: "reserve",
83 stage: "resolve-reservation",
84 userId: "user-1",
85 environmentId: "environment-1",
86 hostname: "environment-1.example.test",
87 tunnelName: "environment-1-tunnel",
88 });
89 expect(error.cause).toBeUndefined();
90 expect(error.message).toContain("'resolve-reservation'");
91 }).pipe(Effect.provide(layerWithDb(fakeDb)));
92 });
93});
infra/relay/src/environments/ManagedEndpointAllocations.ts+110-23
...@@ -38,15 +38,29 @@ export function resolveReadyManagedEndpoint(input: {...@@ -38,15 +38,29 @@ export function resolveReadyManagedEndpoint(input: {
3838
39export class ManagedEndpointAllocationPersistenceError extends Schema.TaggedErrorClass<ManagedEndpointAllocationPersistenceError>()(39export class ManagedEndpointAllocationPersistenceError extends Schema.TaggedErrorClass<ManagedEndpointAllocationPersistenceError>()(
40 "ManagedEndpointAllocationPersistenceError",40 "ManagedEndpointAllocationPersistenceError",
41 { cause: Schema.Defect() },41 {
42 operation: Schema.Literals([
43 "get",
44 "reserve",
45 "record-tunnel",
46 "record-dns",
47 "mark-ready",
48 "remove",
49 ]),
50 stage: Schema.Literals(["database-request", "resolve-reservation"]),
51 userId: Schema.String,
52 environmentId: Schema.String,
53 hostname: Schema.optionalKey(Schema.String),
54 tunnelName: Schema.optionalKey(Schema.String),
55 tunnelId: Schema.optionalKey(Schema.String),
56 dnsRecordId: Schema.optionalKey(Schema.String),
57 cause: Schema.optional(Schema.Defect()),
58 },
42) {59) {
43 override get message(): string {60 override get message(): string {
44 return "Failed to persist managed endpoint allocation";61 return `Managed endpoint allocation '${this.operation}' failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`;
45 }62 }
46}63}
47const isManagedEndpointAllocationPersistenceError = Schema.is(
48 ManagedEndpointAllocationPersistenceError,
49);
5064
51interface ManagedEndpointAllocationKey {65interface ManagedEndpointAllocationKey {
52 readonly userId: string;66 readonly userId: string;
...@@ -106,11 +120,6 @@ const whereAllocation = (input: ManagedEndpointAllocationKey) =>...@@ -106,11 +120,6 @@ const whereAllocation = (input: ManagedEndpointAllocationKey) =>
106 eq(relayManagedEndpointAllocations.environmentId, input.environmentId),120 eq(relayManagedEndpointAllocations.environmentId, input.environmentId),
107 );121 );
108122
109const persistenceError = (cause: unknown) =>
110 isManagedEndpointAllocationPersistenceError(cause)
111 ? cause
112 : new ManagedEndpointAllocationPersistenceError({ cause });
113
114const make = Effect.gen(function* () {123const make = Effect.gen(function* () {
115 const db = yield* RelayDb.RelayDb;124 const db = yield* RelayDb.RelayDb;
116125
...@@ -125,7 +134,15 @@ const make = Effect.gen(function* () {...@@ -125,7 +134,15 @@ const make = Effect.gen(function* () {
125 .limit(1)134 .limit(1)
126 .pipe(135 .pipe(
127 Effect.map((rows) => rows[0] ?? null),136 Effect.map((rows) => rows[0] ?? null),
128 Effect.mapError(persistenceError),137 Effect.mapError(
138 (cause) =>
139 new ManagedEndpointAllocationPersistenceError({
140 operation: "get",
141 stage: "database-request",
142 ...input,
143 cause,
144 }),
145 ),
129 );146 );
130 }),147 }),
131 reserve: Effect.fn("relay.managed_endpoint_allocations.reserve")(function* (148 reserve: Effect.fn("relay.managed_endpoint_allocations.reserve")(function* (
...@@ -140,7 +157,18 @@ const make = Effect.gen(function* () {...@@ -140,7 +157,18 @@ const make = Effect.gen(function* () {
140 updatedAt: now,157 updatedAt: now,
141 })158 })
142 .onConflictDoNothing()159 .onConflictDoNothing()
143 .returning(allocationSelection);160 .returning(allocationSelection)
161 .pipe(
162 Effect.mapError(
163 (cause) =>
164 new ManagedEndpointAllocationPersistenceError({
165 operation: "reserve",
166 stage: "database-request",
167 ...input,
168 cause,
169 }),
170 ),
171 );
144172
145 const allocation =173 const allocation =
146 inserted[0] ??174 inserted[0] ??
...@@ -149,16 +177,29 @@ const make = Effect.gen(function* () {...@@ -149,16 +177,29 @@ const make = Effect.gen(function* () {
149 .from(relayManagedEndpointAllocations)177 .from(relayManagedEndpointAllocations)
150 .where(whereAllocation(input))178 .where(whereAllocation(input))
151 .limit(1)179 .limit(1)
152 .pipe(Effect.map((rows) => rows[0])));180 .pipe(
181 Effect.map((rows) => rows[0]),
182 Effect.mapError(
183 (cause) =>
184 new ManagedEndpointAllocationPersistenceError({
185 operation: "reserve",
186 stage: "database-request",
187 ...input,
188 cause,
189 }),
190 ),
191 ));
153192
154 if (allocation === undefined) {193 if (allocation === undefined) {
155 return yield* new ManagedEndpointAllocationPersistenceError({194 return yield* new ManagedEndpointAllocationPersistenceError({
156 cause: new Error("Managed endpoint allocation was not persisted."),195 operation: "reserve",
196 stage: "resolve-reservation",
197 ...input,
157 });198 });
158 }199 }
159200
160 return allocation;201 return allocation;
161 }, Effect.mapError(persistenceError)),202 }),
162 recordTunnel: Effect.fn("relay.managed_endpoint_allocations.record_tunnel")(function* (203 recordTunnel: Effect.fn("relay.managed_endpoint_allocations.record_tunnel")(function* (
163 input: RecordManagedEndpointTunnelInput,204 input: RecordManagedEndpointTunnelInput,
164 ) {205 ) {
...@@ -168,8 +209,19 @@ const make = Effect.gen(function* () {...@@ -168,8 +209,19 @@ const make = Effect.gen(function* () {
168 tunnelId: input.tunnelId,209 tunnelId: input.tunnelId,
169 updatedAt: DateTime.formatIso(yield* DateTime.now),210 updatedAt: DateTime.formatIso(yield* DateTime.now),
170 })211 })
171 .where(whereAllocation(input));212 .where(whereAllocation(input))
172 }, Effect.mapError(persistenceError)),213 .pipe(
214 Effect.mapError(
215 (cause) =>
216 new ManagedEndpointAllocationPersistenceError({
217 operation: "record-tunnel",
218 stage: "database-request",
219 ...input,
220 cause,
221 }),
222 ),
223 );
224 }),
173 recordDns: Effect.fn("relay.managed_endpoint_allocations.record_dns")(function* (225 recordDns: Effect.fn("relay.managed_endpoint_allocations.record_dns")(function* (
174 input: RecordManagedEndpointDnsInput,226 input: RecordManagedEndpointDnsInput,
175 ) {227 ) {
...@@ -179,8 +231,19 @@ const make = Effect.gen(function* () {...@@ -179,8 +231,19 @@ const make = Effect.gen(function* () {
179 dnsRecordId: input.dnsRecordId,231 dnsRecordId: input.dnsRecordId,
180 updatedAt: DateTime.formatIso(yield* DateTime.now),232 updatedAt: DateTime.formatIso(yield* DateTime.now),
181 })233 })
182 .where(whereAllocation(input));234 .where(whereAllocation(input))
183 }, Effect.mapError(persistenceError)),235 .pipe(
236 Effect.mapError(
237 (cause) =>
238 new ManagedEndpointAllocationPersistenceError({
239 operation: "record-dns",
240 stage: "database-request",
241 ...input,
242 cause,
243 }),
244 ),
245 );
246 }),
184 markReady: Effect.fn("relay.managed_endpoint_allocations.mark_ready")(function* (247 markReady: Effect.fn("relay.managed_endpoint_allocations.mark_ready")(function* (
185 input: ManagedEndpointAllocationKey,248 input: ManagedEndpointAllocationKey,
186 ) {249 ) {
...@@ -191,13 +254,37 @@ const make = Effect.gen(function* () {...@@ -191,13 +254,37 @@ const make = Effect.gen(function* () {
191 readyAt: now,254 readyAt: now,
192 updatedAt: now,255 updatedAt: now,
193 })256 })
194 .where(whereAllocation(input));257 .where(whereAllocation(input))
195 }, Effect.mapError(persistenceError)),258 .pipe(
259 Effect.mapError(
260 (cause) =>
261 new ManagedEndpointAllocationPersistenceError({
262 operation: "mark-ready",
263 stage: "database-request",
264 ...input,
265 cause,
266 }),
267 ),
268 );
269 }),
196 remove: Effect.fn("relay.managed_endpoint_allocations.remove")(function* (270 remove: Effect.fn("relay.managed_endpoint_allocations.remove")(function* (
197 input: ManagedEndpointAllocationKey,271 input: ManagedEndpointAllocationKey,
198 ) {272 ) {
199 yield* db.delete(relayManagedEndpointAllocations).where(whereAllocation(input));273 yield* db
200 }, Effect.mapError(persistenceError)),274 .delete(relayManagedEndpointAllocations)
275 .where(whereAllocation(input))
276 .pipe(
277 Effect.mapError(
278 (cause) =>
279 new ManagedEndpointAllocationPersistenceError({
280 operation: "remove",
281 stage: "database-request",
282 ...input,
283 cause,
284 }),
285 ),
286 );
287 }),
201 });288 });
202});289});
203290
infra/relay/src/environments/ManagedEndpointProvider.test.ts+83-5
...@@ -130,6 +130,9 @@ function makeDnsClient(...@@ -130,6 +130,9 @@ function makeDnsClient(
130 calls.push({ operation: "updateRecord", input: { dnsRecordId, request } });130 calls.push({ operation: "updateRecord", input: { dnsRecordId, request } });
131 if (!currentRecords.some((record) => record.id === dnsRecordId)) {131 if (!currentRecords.some((record) => record.id === dnsRecordId)) {
132 return yield* new ManagedEndpointProvider.ManagedEndpointDnsClientError({132 return yield* new ManagedEndpointProvider.ManagedEndpointDnsClientError({
133 operation: "update-record",
134 hostname: request.name,
135 dnsRecordId,
133 cause: `DNS record ${dnsRecordId} does not exist.`,136 cause: `DNS record ${dnsRecordId} does not exist.`,
134 });137 });
135 }138 }
...@@ -398,7 +401,13 @@ describe("ManagedEndpointProvider", () => {...@@ -398,7 +401,13 @@ describe("ManagedEndpointProvider", () => {
398 expect(dnsCalls).toHaveLength(0);401 expect(dnsCalls).toHaveLength(0);
399 expect(result._tag).toBe("Failure");402 expect(result._tag).toBe("Failure");
400 if (result._tag === "Failure") {403 if (result._tag === "Failure") {
401 expect(result.failure._tag).toBe("ManagedEndpointOriginNotAllowed");404 expect(result.failure).toMatchObject({
405 _tag: "ManagedEndpointOriginNotAllowed",
406 userId: "user_ABC",
407 environmentId: "env_ABC",
408 host: "192.168.1.10",
409 port: 3773,
410 });
402 }411 }
403 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), makeDnsClient(dnsCalls))));412 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), makeDnsClient(dnsCalls))));
404 });413 });
...@@ -593,6 +602,8 @@ describe("ManagedEndpointProvider", () => {...@@ -593,6 +602,8 @@ describe("ManagedEndpointProvider", () => {
593 const tunnelCalls: TunnelCall[] = [];602 const tunnelCalls: TunnelCall[] = [];
594 let deleteAttempts = 0;603 let deleteAttempts = 0;
595 const failure = new ManagedEndpointProvider.ManagedEndpointTunnelClientError({604 const failure = new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
605 operation: "delete",
606 tunnelId: "tunnel-id",
596 cause: "Cloudflare tunnel deletion failed",607 cause: "Cloudflare tunnel deletion failed",
597 });608 });
598 const tunnels = makePersistentTunnelClient(tunnelCalls);609 const tunnels = makePersistentTunnelClient(tunnelCalls);
...@@ -618,6 +629,16 @@ describe("ManagedEndpointProvider", () => {...@@ -618,6 +629,16 @@ describe("ManagedEndpointProvider", () => {
618 });629 });
619 const first = yield* Effect.result(provider.deprovision(key));630 const first = yield* Effect.result(provider.deprovision(key));
620 expect(first._tag).toBe("Failure");631 expect(first._tag).toBe("Failure");
632 if (first._tag === "Failure") {
633 expect(first.failure).toMatchObject({
634 _tag: "ManagedEndpointDeprovisioningFailed",
635 stage: "delete-tunnel",
636 userId: key.userId,
637 environmentId: key.environmentId,
638 tunnelId: "tunnel-id",
639 });
640 expect(first.failure.cause).toBe(failure);
641 }
621 yield* provider.deprovision(key);642 yield* provider.deprovision(key);
622643
623 expect(allocationCalls.map((call) => call.operation)).toEqual([644 expect(allocationCalls.map((call) => call.operation)).toEqual([
...@@ -639,13 +660,23 @@ describe("ManagedEndpointProvider", () => {...@@ -639,13 +660,23 @@ describe("ManagedEndpointProvider", () => {
639 ...makeTunnelClient(),660 ...makeTunnelClient(),
640 delete: () =>661 delete: () =>
641 Effect.fail(662 Effect.fail(
642 new ManagedEndpointProvider.ManagedEndpointTunnelClientError({ cause: notFound }),663 new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
664 operation: "delete",
665 tunnelId: "tunnel-id",
666 cause: notFound,
667 }),
643 ),668 ),
644 });669 });
645 const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({670 const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({
646 ...makeDnsClient(),671 ...makeDnsClient(),
647 deleteRecord: () =>672 deleteRecord: () =>
648 Effect.fail(new ManagedEndpointProvider.ManagedEndpointDnsClientError({ cause: notFound })),673 Effect.fail(
674 new ManagedEndpointProvider.ManagedEndpointDnsClientError({
675 operation: "delete-record",
676 dnsRecordId: "created-record-id",
677 cause: notFound,
678 }),
679 ),
649 });680 });
650 const layer = providerLayer(tunnelClient, dnsClient, makeAllocations(allocationCalls));681 const layer = providerLayer(tunnelClient, dnsClient, makeAllocations(allocationCalls));
651682
...@@ -690,6 +721,8 @@ describe("ManagedEndpointProvider", () => {...@@ -690,6 +721,8 @@ describe("ManagedEndpointProvider", () => {
690 it.effect("recovers when DNS creation reports failure after the record became visible", () => {721 it.effect("recovers when DNS creation reports failure after the record became visible", () => {
691 const dnsCalls: DnsCall[] = [];722 const dnsCalls: DnsCall[] = [];
692 const failure = new ManagedEndpointProvider.ManagedEndpointDnsClientError({723 const failure = new ManagedEndpointProvider.ManagedEndpointDnsClientError({
724 operation: "create-record",
725 hostname: expectedManagedHostname("env_ABC"),
693 cause: "ambiguous Cloudflare DNS response",726 cause: "ambiguous Cloudflare DNS response",
694 });727 });
695 let records: ReadonlyArray<{ readonly id: string }> = [];728 let records: ReadonlyArray<{ readonly id: string }> = [];
...@@ -732,8 +765,43 @@ describe("ManagedEndpointProvider", () => {...@@ -732,8 +765,43 @@ describe("ManagedEndpointProvider", () => {
732 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), dnsClient)));765 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), dnsClient)));
733 });766 });
734767
768 it.effect("reports malformed tunnel responses without manufacturing a cause", () => {
769 const dnsCalls: DnsCall[] = [];
770 const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
771 ...makeTunnelClient(),
772 create: () => Effect.succeed({ id: "returned-tunnel-id", name: null }),
773 });
774
775 return Effect.gen(function* () {
776 const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
777 const error = yield* Effect.flip(
778 provider.provision({
779 userId: "user_ABC",
780 environmentId: "env_ABC",
781 origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
782 }),
783 );
784
785 expect(error).toMatchObject({
786 _tag: "ManagedEndpointProvisioningFailed",
787 stage: "validate-tunnel-response",
788 userId: "user_ABC",
789 environmentId: "env_ABC",
790 hostname: expectedManagedHostname("env_ABC"),
791 tunnelName: expectedManagedTunnelName("env_ABC"),
792 returnedTunnelId: "returned-tunnel-id",
793 });
794 if (error._tag === "ManagedEndpointProvisioningFailed") {
795 expect(error.cause).toBeUndefined();
796 }
797 expect(dnsCalls).toHaveLength(0);
798 }).pipe(Effect.provide(providerLayer(tunnelClient, makeDnsClient(dnsCalls))));
799 });
800
735 it.effect("fails provisioning when the DNS client fails", () => {801 it.effect("fails provisioning when the DNS client fails", () => {
736 const failure = new ManagedEndpointProvider.ManagedEndpointDnsClientError({802 const failure = new ManagedEndpointProvider.ManagedEndpointDnsClientError({
803 operation: "list-records",
804 hostname: expectedManagedHostname("env_ABC"),
737 cause: "Cloudflare DNS failure",805 cause: "Cloudflare DNS failure",
738 });806 });
739 const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({807 const dnsClient = ManagedEndpointProvider.ManagedEndpointDnsClient.of({
...@@ -753,8 +821,18 @@ describe("ManagedEndpointProvider", () => {...@@ -753,8 +821,18 @@ describe("ManagedEndpointProvider", () => {
753 }),821 }),
754 );822 );
755823
756 expect(error._tag).toBe("ManagedEndpointProvisioningFailed");824 expect(error).toMatchObject({
757 expect(error.cause).toBe(failure);825 _tag: "ManagedEndpointProvisioningFailed",
826 stage: "ensure-dns-record",
827 userId: "user_ABC",
828 environmentId: "env_ABC",
829 hostname: expectedManagedHostname("env_ABC"),
830 tunnelName: expectedManagedTunnelName("env_ABC"),
831 tunnelId: "tunnel-id",
832 });
833 if (error._tag === "ManagedEndpointProvisioningFailed") {
834 expect(error.cause).toBe(failure);
835 }
758 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), dnsClient)));836 }).pipe(Effect.provide(providerLayer(makeTunnelClient(), dnsClient)));
759 });837 });
760});838});
infra/relay/src/environments/ManagedEndpointProvider.ts+392-76
...@@ -7,7 +7,6 @@ import * as Effect from "effect/Effect";...@@ -7,7 +7,6 @@ import * as Effect from "effect/Effect";
7import * as Encoding from "effect/Encoding";7import * as Encoding from "effect/Encoding";
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";9import * as Option from "effect/Option";
10import * as Result from "effect/Result";
11import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
1211
13import type {12import type {
...@@ -27,40 +26,86 @@ import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";...@@ -27,40 +26,86 @@ import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
2726
28export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()(27export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()(
29 "ManagedEndpointProvisioningNotConfigured",28 "ManagedEndpointProvisioningNotConfigured",
30 {},29 {
30 userId: Schema.String,
31 environmentId: Schema.String,
32 missingSettings: Schema.Array(
33 Schema.Literals(["managedEndpointBaseDomain", "managedEndpointNamespace"]),
34 ),
35 },
31) {36) {
32 override get message(): string {37 override get message(): string {
33 return "Managed endpoint provisioning is not configured";38 return `Managed endpoint provisioning is not configured for user '${this.userId}', environment '${this.environmentId}': missing ${this.missingSettings.join(", ")}`;
34 }39 }
35}40}
3641
42const ManagedEndpointProvisioningStage = Schema.Literals([
43 "derive-environment-hash",
44 "reserve-allocation",
45 "ensure-tunnel",
46 "validate-tunnel-response",
47 "record-tunnel",
48 "configure-tunnel",
49 "ensure-dns-record",
50 "record-dns",
51 "get-tunnel-token",
52 "mark-allocation-ready",
53]);
54
37export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointProvisioningFailed>()(55export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointProvisioningFailed>()(
38 "ManagedEndpointProvisioningFailed",56 "ManagedEndpointProvisioningFailed",
39 { cause: Schema.Defect() },57 {
58 stage: ManagedEndpointProvisioningStage,
59 userId: Schema.String,
60 environmentId: Schema.String,
61 hostname: Schema.optionalKey(Schema.String),
62 tunnelName: Schema.optionalKey(Schema.String),
63 tunnelId: Schema.optionalKey(Schema.String),
64 dnsRecordId: Schema.optionalKey(Schema.String),
65 returnedTunnelName: Schema.optionalKey(Schema.String),
66 returnedTunnelId: Schema.optionalKey(Schema.String),
67 cause: Schema.optional(Schema.Defect()),
68 },
40) {69) {
41 override get message(): string {70 override get message(): string {
42 return "Managed endpoint provisioning failed";71 return `Managed endpoint provisioning failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`;
43 }72 }
44}73}
4574
75const ManagedEndpointDeprovisioningStage = Schema.Literals([
76 "load-allocation",
77 "delete-dns-record",
78 "delete-tunnel",
79 "remove-allocation",
80]);
81
46export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointDeprovisioningFailed>()(82export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointDeprovisioningFailed>()(
47 "ManagedEndpointDeprovisioningFailed",83 "ManagedEndpointDeprovisioningFailed",
48 { cause: Schema.Defect() },84 {
85 stage: ManagedEndpointDeprovisioningStage,
86 userId: Schema.String,
87 environmentId: Schema.String,
88 tunnelId: Schema.optionalKey(Schema.String),
89 dnsRecordId: Schema.optionalKey(Schema.String),
90 cause: Schema.Defect(),
91 },
49) {92) {
50 override get message(): string {93 override get message(): string {
51 return "Managed endpoint deprovisioning failed";94 return `Managed endpoint deprovisioning failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`;
52 }95 }
53}96}
5497
55export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass<ManagedEndpointOriginNotAllowed>()(98export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass<ManagedEndpointOriginNotAllowed>()(
56 "ManagedEndpointOriginNotAllowed",99 "ManagedEndpointOriginNotAllowed",
57 {100 {
101 userId: Schema.String,
102 environmentId: Schema.String,
58 host: Schema.String,103 host: Schema.String,
59 port: Schema.Number,104 port: Schema.Number,
60 },105 },
61) {106) {
62 override get message(): string {107 override get message(): string {
63 return `Managed endpoint origin '${this.host}:${this.port}' is not allowed`;108 return `Managed endpoint origin '${this.host}:${this.port}' is not allowed for user '${this.userId}', environment '${this.environmentId}'`;
64 }109 }
65}110}
66111
...@@ -94,12 +139,26 @@ interface ManagedEndpointTunnel {...@@ -94,12 +139,26 @@ interface ManagedEndpointTunnel {
94 readonly name?: string | null;139 readonly name?: string | null;
95}140}
96141
142const ManagedEndpointTunnelClientOperation = Schema.Literals([
143 "list",
144 "create",
145 "put-configuration",
146 "get-token",
147 "delete",
148]);
149
97export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<ManagedEndpointTunnelClientError>()(150export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<ManagedEndpointTunnelClientError>()(
98 "ManagedEndpointTunnelClientError",151 "ManagedEndpointTunnelClientError",
99 { cause: Schema.Defect() },152 {
153 operation: ManagedEndpointTunnelClientOperation,
154 tunnelName: Schema.optionalKey(Schema.String),
155 tunnelId: Schema.optionalKey(Schema.String),
156 cause: Schema.Defect(),
157 },
100) {158) {
101 override get message(): string {159 override get message(): string {
102 return "Managed endpoint tunnel provider request failed";160 const target = this.tunnelId ?? this.tunnelName;
161 return `Managed endpoint tunnel provider '${this.operation}' request failed${target === undefined ? "" : ` for '${target}'`}`;
103 }162 }
104}163}
105164
...@@ -147,12 +206,25 @@ interface ManagedEndpointCnameRecordInput {...@@ -147,12 +206,25 @@ interface ManagedEndpointCnameRecordInput {
147 readonly proxied: true;206 readonly proxied: true;
148}207}
149208
209const ManagedEndpointDnsClientOperation = Schema.Literals([
210 "list-records",
211 "create-record",
212 "update-record",
213 "delete-record",
214]);
215
150export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<ManagedEndpointDnsClientError>()(216export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<ManagedEndpointDnsClientError>()(
151 "ManagedEndpointDnsClientError",217 "ManagedEndpointDnsClientError",
152 { cause: Schema.Defect() },218 {
219 operation: ManagedEndpointDnsClientOperation,
220 hostname: Schema.optionalKey(Schema.String),
221 dnsRecordId: Schema.optionalKey(Schema.String),
222 cause: Schema.Defect(),
223 },
153) {224) {
154 override get message(): string {225 override get message(): string {
155 return "Managed endpoint DNS provider request failed";226 const target = this.dnsRecordId ?? this.hostname;
227 return `Managed endpoint DNS provider '${this.operation}' request failed${target === undefined ? "" : ` for '${target}'`}`;
156 }228 }
157}229}
158230
...@@ -183,13 +255,26 @@ export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) =>...@@ -183,13 +255,26 @@ export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) =>
183255
184const requireCloudflareSettings = Effect.fnUntraced(function* (256const requireCloudflareSettings = Effect.fnUntraced(function* (
185 settings: RelayConfiguration.RelayConfiguration["Service"],257 settings: RelayConfiguration.RelayConfiguration["Service"],
258 input: { readonly userId: string; readonly environmentId: string },
186) {259) {
187 if (!settings.managedEndpointBaseDomain || !settings.managedEndpointNamespace) {260 const baseDomain = settings.managedEndpointBaseDomain;
188 return yield* new ManagedEndpointProvisioningNotConfigured();261 const namespace = settings.managedEndpointNamespace;
262 const missingSettings: Array<"managedEndpointBaseDomain" | "managedEndpointNamespace"> = [];
263 if (!baseDomain) {
264 missingSettings.push("managedEndpointBaseDomain");
265 }
266 if (!namespace) {
267 missingSettings.push("managedEndpointNamespace");
268 }
269 if (!baseDomain || !namespace) {
270 return yield* new ManagedEndpointProvisioningNotConfigured({
271 ...input,
272 missingSettings,
273 });
189 }274 }
190 return {275 return {
191 baseDomain: settings.managedEndpointBaseDomain,276 baseDomain,
192 namespace: settings.managedEndpointNamespace,277 namespace,
193 };278 };
194});279});
195280
...@@ -231,10 +316,19 @@ function isNotFoundCause(cause: unknown): boolean {...@@ -231,10 +316,19 @@ function isNotFoundCause(cause: unknown): boolean {
231 return "cause" in cause && isNotFoundCause(cause.cause);316 return "cause" in cause && isNotFoundCause(cause.cause);
232}317}
233318
234const ignoreNotFound = <A, E>(effect: Effect.Effect<A, E>): Effect.Effect<void, E> =>319type ManagedEndpointClientError = ManagedEndpointTunnelClientError | ManagedEndpointDnsClientError;
320
321const ignoreNotFound = <A>(
322 effect: Effect.Effect<A, ManagedEndpointClientError>,
323): Effect.Effect<void, ManagedEndpointClientError> =>
235 effect.pipe(324 effect.pipe(
236 Effect.asVoid,325 Effect.asVoid,
237 Effect.catch((cause) => (isNotFoundCause(cause) ? Effect.void : Effect.fail(cause))),326 Effect.catchTags({
327 ManagedEndpointTunnelClientError: (error) =>
328 isNotFoundCause(error.cause) ? Effect.void : Effect.fail(error),
329 ManagedEndpointDnsClientError: (error) =>
330 isNotFoundCause(error.cause) ? Effect.void : Effect.fail(error),
331 }),
238 );332 );
239333
240const make = Effect.gen(function* () {334const make = Effect.gen(function* () {
...@@ -289,25 +383,26 @@ const make = Effect.gen(function* () {...@@ -289,25 +383,26 @@ const make = Effect.gen(function* () {
289 }383 }
290 return yield* dns.createRecord(dnsRecord).pipe(384 return yield* dns.createRecord(dnsRecord).pipe(
291 Effect.map((record) => record.id),385 Effect.map((record) => record.id),
292 Effect.catch((createError) =>386 Effect.catchTags({
293 Effect.gen(function* () {387 ManagedEndpointDnsClientError: (createError) =>
294 let records = yield* dns.listRecords(hostname);388 Effect.gen(function* () {
295 for (let attempt = 0; records.length === 0 && attempt < 4; attempt++) {389 let records = yield* dns.listRecords(hostname);
296 yield* Effect.sleep("200 millis");390 for (let attempt = 0; records.length === 0 && attempt < 4; attempt++) {
297 records = yield* dns.listRecords(hostname);391 yield* Effect.sleep("200 millis");
298 }392 records = yield* dns.listRecords(hostname);
299 return records;393 }
300 }).pipe(394 return records;
301 Effect.flatMap((records) =>395 }).pipe(
302 records.length > 0396 Effect.flatMap((records) =>
303 ? updateExistingDnsRecords(records, preferredDnsRecordId, dnsRecord)397 records.length > 0
304 : Effect.fail(createError),398 ? updateExistingDnsRecords(records, preferredDnsRecordId, dnsRecord)
305 ),399 : Effect.fail(createError),
306 Effect.flatMap((dnsRecordId) =>400 ),
307 dnsRecordId === null ? Effect.fail(createError) : Effect.succeed(dnsRecordId),401 Effect.flatMap((dnsRecordId) =>
402 dnsRecordId === null ? Effect.fail(createError) : Effect.succeed(dnsRecordId),
403 ),
308 ),404 ),
309 ),405 }),
310 ),
311 );406 );
312 });407 });
313408
...@@ -317,25 +412,59 @@ const make = Effect.gen(function* () {...@@ -317,25 +412,59 @@ const make = Effect.gen(function* () {
317 "relay.user_id": input.userId,412 "relay.user_id": input.userId,
318 "relay.environment_id": input.environmentId,413 "relay.environment_id": input.environmentId,
319 });414 });
320 const allocation = yield* allocations415 const allocation = yield* allocations.get(input).pipe(
321 .get(input)416 Effect.mapError(
322 .pipe(Effect.mapError((cause) => new ManagedEndpointDeprovisioningFailed({ cause })));417 (cause) =>
418 new ManagedEndpointDeprovisioningFailed({
419 ...input,
420 stage: "load-allocation",
421 cause,
422 }),
423 ),
424 );
323 if (allocation === null) {425 if (allocation === null) {
324 return;426 return;
325 }427 }
326 if (allocation.dnsRecordId !== null) {428 const dnsRecordId = allocation.dnsRecordId;
327 yield* ignoreNotFound(dns.deleteRecord(allocation.dnsRecordId)).pipe(429 if (dnsRecordId !== null) {
328 Effect.mapError((cause) => new ManagedEndpointDeprovisioningFailed({ cause })),430 yield* ignoreNotFound(dns.deleteRecord(dnsRecordId)).pipe(
431 Effect.mapError(
432 (cause) =>
433 new ManagedEndpointDeprovisioningFailed({
434 ...input,
435 stage: "delete-dns-record",
436 dnsRecordId,
437 cause,
438 }),
439 ),
329 );440 );
330 }441 }
331 if (allocation.tunnelId !== null) {442 const tunnelId = allocation.tunnelId;
332 yield* ignoreNotFound(tunnels.delete(allocation.tunnelId)).pipe(443 if (tunnelId !== null) {
333 Effect.mapError((cause) => new ManagedEndpointDeprovisioningFailed({ cause })),444 yield* ignoreNotFound(tunnels.delete(tunnelId)).pipe(
445 Effect.mapError(
446 (cause) =>
447 new ManagedEndpointDeprovisioningFailed({
448 ...input,
449 stage: "delete-tunnel",
450 tunnelId,
451 cause,
452 }),
453 ),
334 );454 );
335 }455 }
336 yield* allocations456 yield* allocations.remove(input).pipe(
337 .remove(input)457 Effect.mapError(
338 .pipe(Effect.mapError((cause) => new ManagedEndpointDeprovisioningFailed({ cause })));458 (cause) =>
459 new ManagedEndpointDeprovisioningFailed({
460 ...input,
461 stage: "remove-allocation",
462 ...(allocation.tunnelId === null ? {} : { tunnelId: allocation.tunnelId }),
463 ...(allocation.dnsRecordId === null ? {} : { dnsRecordId: allocation.dnsRecordId }),
464 cause,
465 }),
466 ),
467 );
339 }),468 }),
340 provision: Effect.fn("relay.managed_endpoint_provider.provision")(function* (input) {469 provision: Effect.fn("relay.managed_endpoint_provider.provision")(function* (input) {
341 yield* Effect.annotateCurrentSpan({470 yield* Effect.annotateCurrentSpan({
...@@ -346,11 +475,13 @@ const make = Effect.gen(function* () {...@@ -346,11 +475,13 @@ const make = Effect.gen(function* () {
346 });475 });
347 if (!isLoopbackOrigin(input.origin)) {476 if (!isLoopbackOrigin(input.origin)) {
348 return yield* new ManagedEndpointOriginNotAllowed({477 return yield* new ManagedEndpointOriginNotAllowed({
478 userId: input.userId,
479 environmentId: input.environmentId,
349 host: input.origin.localHttpHost,480 host: input.origin.localHttpHost,
350 port: input.origin.localHttpPort,481 port: input.origin.localHttpPort,
351 });482 });
352 }483 }
353 const cf = yield* requireCloudflareSettings(config);484 const cf = yield* requireCloudflareSettings(config, input);
354 const environmentHash = yield* crypto485 const environmentHash = yield* crypto
355 .digest(486 .digest(
356 "SHA-256",487 "SHA-256",
...@@ -360,19 +491,45 @@ const make = Effect.gen(function* () {...@@ -360,19 +491,45 @@ const make = Effect.gen(function* () {
360 )491 )
361 .pipe(492 .pipe(
362 Effect.map(Encoding.encodeHex),493 Effect.map(Encoding.encodeHex),
363 Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })),494 Effect.mapError(
495 (cause) =>
496 new ManagedEndpointProvisioningFailed({
497 userId: input.userId,
498 environmentId: input.environmentId,
499 stage: "derive-environment-hash",
500 cause,
501 }),
502 ),
364 );503 );
504 const requestedHostname = managedEndpointHostname(
505 cf.namespace,
506 cf.baseDomain,
507 environmentHash,
508 );
509 const requestedTunnelName = managedEndpointTunnelName(cf.namespace, environmentHash);
365 const allocation = yield* allocations510 const allocation = yield* allocations
366 .reserve({511 .reserve({
367 userId: input.userId,512 userId: input.userId,
368 environmentId: input.environmentId,513 environmentId: input.environmentId,
369 hostname: managedEndpointHostname(cf.namespace, cf.baseDomain, environmentHash),514 hostname: requestedHostname,
370 tunnelName: managedEndpointTunnelName(cf.namespace, environmentHash),515 tunnelName: requestedTunnelName,
371 })516 })
372 .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })));517 .pipe(
518 Effect.mapError(
519 (cause) =>
520 new ManagedEndpointProvisioningFailed({
521 userId: input.userId,
522 environmentId: input.environmentId,
523 stage: "reserve-allocation",
524 hostname: requestedHostname,
525 tunnelName: requestedTunnelName,
526 cause,
527 }),
528 ),
529 );
373 const { hostname, tunnelName } = allocation;530 const { hostname, tunnelName } = allocation;
374531
375 const tunnel = yield* tunnels.list({ name: tunnelName, isDeleted: false }).pipe(532 const tunnelResponse = yield* tunnels.list({ name: tunnelName, isDeleted: false }).pipe(
376 Effect.map((tunnels) => tunnels.result),533 Effect.map((tunnels) => tunnels.result),
377 Effect.map(Arr.findFirst((tunnel) => tunnel.name === tunnelName)),534 Effect.map(Arr.findFirst((tunnel) => tunnel.name === tunnelName)),
378 Effect.flatMap(535 Effect.flatMap(
...@@ -381,20 +538,50 @@ const make = Effect.gen(function* () {...@@ -381,20 +538,50 @@ const make = Effect.gen(function* () {
381 onNone: () => tunnels.create({ name: tunnelName, configSrc: "cloudflare" }),538 onNone: () => tunnels.create({ name: tunnelName, configSrc: "cloudflare" }),
382 }),539 }),
383 ),540 ),
384 Effect.filterMapOrFail((tunnel) =>541 Effect.mapError(
385 tunnel.id && tunnel.name542 (cause) =>
386 ? Result.succeed({ id: tunnel.id, name: tunnel.name })543 new ManagedEndpointProvisioningFailed({
387 : Result.fail(new ManagedEndpointProvisioningFailed({ cause: tunnel })),544 userId: input.userId,
545 environmentId: input.environmentId,
546 stage: "ensure-tunnel",
547 hostname,
548 tunnelName,
549 cause,
550 }),
388 ),551 ),
389 Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })),
390 );552 );
553 if (!tunnelResponse.id || !tunnelResponse.name) {
554 return yield* new ManagedEndpointProvisioningFailed({
555 userId: input.userId,
556 environmentId: input.environmentId,
557 stage: "validate-tunnel-response",
558 hostname,
559 tunnelName,
560 ...(tunnelResponse.id ? { returnedTunnelId: tunnelResponse.id } : {}),
561 ...(tunnelResponse.name ? { returnedTunnelName: tunnelResponse.name } : {}),
562 });
563 }
564 const tunnel = { id: tunnelResponse.id, name: tunnelResponse.name };
391 yield* allocations565 yield* allocations
392 .recordTunnel({566 .recordTunnel({
393 userId: input.userId,567 userId: input.userId,
394 environmentId: input.environmentId,568 environmentId: input.environmentId,
395 tunnelId: tunnel.id,569 tunnelId: tunnel.id,
396 })570 })
397 .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })));571 .pipe(
572 Effect.mapError(
573 (cause) =>
574 new ManagedEndpointProvisioningFailed({
575 userId: input.userId,
576 environmentId: input.environmentId,
577 stage: "record-tunnel",
578 hostname,
579 tunnelName,
580 tunnelId: tunnel.id,
581 cause,
582 }),
583 ),
584 );
398585
399 yield* tunnels586 yield* tunnels
400 .putConfiguration(tunnel.id, {587 .putConfiguration(tunnel.id, {
...@@ -406,7 +593,20 @@ const make = Effect.gen(function* () {...@@ -406,7 +593,20 @@ const make = Effect.gen(function* () {
406 { service: "http_status:404" },593 { service: "http_status:404" },
407 ],594 ],
408 })595 })
409 .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })));596 .pipe(
597 Effect.mapError(
598 (cause) =>
599 new ManagedEndpointProvisioningFailed({
600 userId: input.userId,
601 environmentId: input.environmentId,
602 stage: "configure-tunnel",
603 hostname,
604 tunnelName,
605 tunnelId: tunnel.id,
606 cause,
607 }),
608 ),
609 );
410610
411 const dnsRecord = {611 const dnsRecord = {
412 type: "CNAME",612 type: "CNAME",
...@@ -417,7 +617,19 @@ const make = Effect.gen(function* () {...@@ -417,7 +617,19 @@ const make = Effect.gen(function* () {
417 } as const;617 } as const;
418618
419 const dnsRecordId = yield* ensureDnsRecord(hostname, allocation.dnsRecordId, dnsRecord).pipe(619 const dnsRecordId = yield* ensureDnsRecord(hostname, allocation.dnsRecordId, dnsRecord).pipe(
420 Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })),620 Effect.mapError(
621 (cause) =>
622 new ManagedEndpointProvisioningFailed({
623 userId: input.userId,
624 environmentId: input.environmentId,
625 stage: "ensure-dns-record",
626 hostname,
627 tunnelName,
628 tunnelId: tunnel.id,
629 ...(allocation.dnsRecordId === null ? {} : { dnsRecordId: allocation.dnsRecordId }),
630 cause,
631 }),
632 ),
421 );633 );
422 yield* allocations634 yield* allocations
423 .recordDns({635 .recordDns({
...@@ -425,17 +637,57 @@ const make = Effect.gen(function* () {...@@ -425,17 +637,57 @@ const make = Effect.gen(function* () {
425 environmentId: input.environmentId,637 environmentId: input.environmentId,
426 dnsRecordId,638 dnsRecordId,
427 })639 })
428 .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })));640 .pipe(
641 Effect.mapError(
642 (cause) =>
643 new ManagedEndpointProvisioningFailed({
644 userId: input.userId,
645 environmentId: input.environmentId,
646 stage: "record-dns",
647 hostname,
648 tunnelName,
649 tunnelId: tunnel.id,
650 dnsRecordId,
651 cause,
652 }),
653 ),
654 );
429655
430 const connectorToken = yield* tunnels656 const connectorToken = yield* tunnels.getToken(tunnel.id).pipe(
431 .getToken(tunnel.id)657 Effect.mapError(
432 .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })));658 (cause) =>
659 new ManagedEndpointProvisioningFailed({
660 userId: input.userId,
661 environmentId: input.environmentId,
662 stage: "get-tunnel-token",
663 hostname,
664 tunnelName,
665 tunnelId: tunnel.id,
666 dnsRecordId,
667 cause,
668 }),
669 ),
670 );
433 yield* allocations671 yield* allocations
434 .markReady({672 .markReady({
435 userId: input.userId,673 userId: input.userId,
436 environmentId: input.environmentId,674 environmentId: input.environmentId,
437 })675 })
438 .pipe(Effect.mapError((cause) => new ManagedEndpointProvisioningFailed({ cause })));676 .pipe(
677 Effect.mapError(
678 (cause) =>
679 new ManagedEndpointProvisioningFailed({
680 userId: input.userId,
681 environmentId: input.environmentId,
682 stage: "mark-allocation-ready",
683 hostname,
684 tunnelName,
685 tunnelId: tunnel.id,
686 dnsRecordId,
687 cause,
688 }),
689 ),
690 );
439691
440 return {692 return {
441 endpoint: managedEndpointForHostname(hostname),693 endpoint: managedEndpointForHostname(hostname),
...@@ -464,27 +716,62 @@ export const layerCloudflareBindings = (...@@ -464,27 +716,62 @@ export const layerCloudflareBindings = (
464 ManagedEndpointTunnelClient.of({716 ManagedEndpointTunnelClient.of({
465 list: (request) =>717 list: (request) =>
466 tunnelClient.list(request).pipe(718 tunnelClient.list(request).pipe(
467 Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })),719 Effect.mapError(
720 (cause) =>
721 new ManagedEndpointTunnelClientError({
722 operation: "list",
723 tunnelName: request.name,
724 cause,
725 }),
726 ),
468 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),727 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
469 ),728 ),
470 create: (request) =>729 create: (request) =>
471 tunnelClient.create(request).pipe(730 tunnelClient.create(request).pipe(
472 Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })),731 Effect.mapError(
732 (cause) =>
733 new ManagedEndpointTunnelClientError({
734 operation: "create",
735 tunnelName: request.name,
736 cause,
737 }),
738 ),
473 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),739 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
474 ),740 ),
475 putConfiguration: (tunnelId, config) =>741 putConfiguration: (tunnelId, config) =>
476 tunnelClient.putConfiguration(tunnelId, config).pipe(742 tunnelClient.putConfiguration(tunnelId, config).pipe(
477 Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })),743 Effect.mapError(
744 (cause) =>
745 new ManagedEndpointTunnelClientError({
746 operation: "put-configuration",
747 tunnelId,
748 cause,
749 }),
750 ),
478 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),751 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
479 ),752 ),
480 getToken: (tunnelId) =>753 getToken: (tunnelId) =>
481 tunnelClient.getToken(tunnelId).pipe(754 tunnelClient.getToken(tunnelId).pipe(
482 Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })),755 Effect.mapError(
756 (cause) =>
757 new ManagedEndpointTunnelClientError({
758 operation: "get-token",
759 tunnelId,
760 cause,
761 }),
762 ),
483 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),763 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
484 ),764 ),
485 delete: (tunnelId) =>765 delete: (tunnelId) =>
486 tunnelClient.delete(tunnelId).pipe(766 tunnelClient.delete(tunnelId).pipe(
487 Effect.mapError((cause) => new ManagedEndpointTunnelClientError({ cause })),767 Effect.mapError(
768 (cause) =>
769 new ManagedEndpointTunnelClientError({
770 operation: "delete",
771 tunnelId,
772 cause,
773 }),
774 ),
488 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),775 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
489 ),776 ),
490 }),777 }),
...@@ -500,23 +787,52 @@ export const layerCloudflareBindings = (...@@ -500,23 +787,52 @@ export const layerCloudflareBindings = (
500 normalizeHostname(record.name) === normalizeHostname(hostname),787 normalizeHostname(record.name) === normalizeHostname(hostname),
501 ),788 ),
502 ),789 ),
503 Effect.mapError((cause) => new ManagedEndpointDnsClientError({ cause })),790 Effect.mapError(
791 (cause) =>
792 new ManagedEndpointDnsClientError({
793 operation: "list-records",
794 hostname,
795 cause,
796 }),
797 ),
504 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),798 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
505 ),799 ),
506 createRecord: (request) =>800 createRecord: (request) =>
507 dnsClient.createDnsRecord(request).pipe(801 dnsClient.createDnsRecord(request).pipe(
508 Effect.map((response) => ({ id: response.id })),802 Effect.map((response) => ({ id: response.id })),
509 Effect.mapError((cause) => new ManagedEndpointDnsClientError({ cause })),803 Effect.mapError(
804 (cause) =>
805 new ManagedEndpointDnsClientError({
806 operation: "create-record",
807 hostname: request.name,
808 cause,
809 }),
810 ),
510 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),811 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
511 ),812 ),
512 updateRecord: (dnsRecordId, request) =>813 updateRecord: (dnsRecordId, request) =>
513 dnsClient.updateDnsRecord(dnsRecordId, request).pipe(814 dnsClient.updateDnsRecord(dnsRecordId, request).pipe(
514 Effect.mapError((cause) => new ManagedEndpointDnsClientError({ cause })),815 Effect.mapError(
816 (cause) =>
817 new ManagedEndpointDnsClientError({
818 operation: "update-record",
819 hostname: request.name,
820 dnsRecordId,
821 cause,
822 }),
823 ),
515 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),824 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
516 ),825 ),
517 deleteRecord: (dnsRecordId) =>826 deleteRecord: (dnsRecordId) =>
518 dnsClient.deleteDnsRecord(dnsRecordId).pipe(827 dnsClient.deleteDnsRecord(dnsRecordId).pipe(
519 Effect.mapError((cause) => new ManagedEndpointDnsClientError({ cause })),828 Effect.mapError(
829 (cause) =>
830 new ManagedEndpointDnsClientError({
831 operation: "delete-record",
832 dnsRecordId,
833 cause,
834 }),
835 ),
520 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),836 Effect.provideService(Alchemy.RuntimeContext, alchemyRuntimeContext),
521 ),837 ),
522 }),838 }),