import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Arr from "effect/Array";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Schema from "effect/Schema"; import type { RelayManagedEndpoint, RelayManagedEndpointOrigin, RelayManagedEndpointRuntimeConfig,} from "@t3tools/contracts/relay"; import * as RelayConfiguration from "../Config.ts";import { managedEndpointDigestInput, managedEndpointForHostname, managedEndpointHostname, managedEndpointTunnelName,} from "../deploymentConfig.ts";import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts"; export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()( "ManagedEndpointProvisioningNotConfigured", { userId: Schema.String, environmentId: Schema.String, missingSettings: Schema.Array( Schema.Literals(["managedEndpointBaseDomain", "managedEndpointNamespace"]), ), },) { override get message(): string { return `Managed endpoint provisioning is not configured for user '${this.userId}', environment '${this.environmentId}': missing ${this.missingSettings.join(", ")}`; }} const ManagedEndpointProvisioningStage = Schema.Literals([ "derive-environment-hash", "reserve-allocation", "ensure-tunnel", "validate-tunnel-response", "record-tunnel", "configure-tunnel", "ensure-dns-record", "record-dns", "get-tunnel-token", "mark-allocation-ready",]); export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointProvisioningFailed>()( "ManagedEndpointProvisioningFailed", { stage: ManagedEndpointProvisioningStage, userId: Schema.String, environmentId: Schema.String, hostname: Schema.optionalKey(Schema.String), tunnelName: Schema.optionalKey(Schema.String), tunnelId: Schema.optionalKey(Schema.String), dnsRecordId: Schema.optionalKey(Schema.String), returnedTunnelName: Schema.optionalKey(Schema.String), returnedTunnelId: Schema.optionalKey(Schema.String), cause: Schema.optional(Schema.Defect()), },) { override get message(): string { return `Managed endpoint provisioning failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`; }} const ManagedEndpointDeprovisioningStage = Schema.Literals([ "load-allocation", "delete-dns-record", "delete-tunnel", "remove-allocation",]); export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointDeprovisioningFailed>()( "ManagedEndpointDeprovisioningFailed", { stage: ManagedEndpointDeprovisioningStage, userId: Schema.String, environmentId: Schema.String, tunnelId: Schema.optionalKey(Schema.String), dnsRecordId: Schema.optionalKey(Schema.String), cause: Schema.Defect(), },) { override get message(): string { return `Managed endpoint deprovisioning failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`; }} export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass<ManagedEndpointOriginNotAllowed>()( "ManagedEndpointOriginNotAllowed", { userId: Schema.String, environmentId: Schema.String, host: Schema.String, port: Schema.Number, },) { override get message(): string { return `Managed endpoint origin '${this.host}:${this.port}' is not allowed for user '${this.userId}', environment '${this.environmentId}'`; }} export type ManagedEndpointProviderError = | ManagedEndpointProvisioningNotConfigured | ManagedEndpointProvisioningFailed | ManagedEndpointOriginNotAllowed; export interface ManagedEndpointProvisioningResult { readonly endpoint: RelayManagedEndpoint; readonly runtime: RelayManagedEndpointRuntimeConfig;} export class ManagedEndpointProvider extends Context.Service< ManagedEndpointProvider, { readonly provision: (input: { readonly userId: string; readonly environmentId: string; readonly origin: RelayManagedEndpointOrigin; }) => Effect.Effect<ManagedEndpointProvisioningResult, ManagedEndpointProviderError>; readonly deprovision: (input: { readonly userId: string; readonly environmentId: string; }) => Effect.Effect<void, ManagedEndpointDeprovisioningFailed>; }>()("t3code-relay/environments/ManagedEndpointProvider") {} interface ManagedEndpointTunnel { readonly id?: string | null; readonly name?: string | null;} const ManagedEndpointTunnelClientOperation = Schema.Literals([ "list", "create", "put-configuration", "get-token", "delete",]); export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<ManagedEndpointTunnelClientError>()( "ManagedEndpointTunnelClientError", { operation: ManagedEndpointTunnelClientOperation, tunnelName: Schema.optionalKey(Schema.String), tunnelId: Schema.optionalKey(Schema.String), cause: Schema.Defect(), },) { override get message(): string { const target = this.tunnelId ?? this.tunnelName; return `Managed endpoint tunnel provider '${this.operation}' request failed${target === undefined ? "" : ` for '${target}'`}`; }} export class ManagedEndpointTunnelClient extends Context.Service< ManagedEndpointTunnelClient, { readonly list: (request: { readonly name: string; readonly isDeleted: false; }) => Effect.Effect< { readonly result: ReadonlyArray<ManagedEndpointTunnel> }, ManagedEndpointTunnelClientError >; readonly create: (request: { readonly name: string; readonly configSrc: "cloudflare"; }) => Effect.Effect<ManagedEndpointTunnel, ManagedEndpointTunnelClientError>; readonly putConfiguration: ( tunnelId: string, config: { readonly ingress: Array<{ readonly hostname?: string; readonly service: string; }>; }, ) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>; readonly getToken: ( tunnelId: string, ) => Effect.Effect<string, ManagedEndpointTunnelClientError>; readonly delete: (tunnelId: string) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>; }>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointTunnelClient") {} export const layerTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) => Layer.succeed(ManagedEndpointTunnelClient, client); interface ManagedEndpointCnameRecordInput { readonly type: "CNAME"; readonly name: string; readonly content: string; readonly ttl: 1; readonly proxied: true;} const ManagedEndpointDnsClientOperation = Schema.Literals([ "list-records", "create-record", "update-record", "delete-record",]); export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<ManagedEndpointDnsClientError>()( "ManagedEndpointDnsClientError", { operation: ManagedEndpointDnsClientOperation, hostname: Schema.optionalKey(Schema.String), dnsRecordId: Schema.optionalKey(Schema.String), cause: Schema.Defect(), },) { override get message(): string { const target = this.dnsRecordId ?? this.hostname; return `Managed endpoint DNS provider '${this.operation}' request failed${target === undefined ? "" : ` for '${target}'`}`; }} export class ManagedEndpointDnsClient extends Context.Service< ManagedEndpointDnsClient, { readonly listRecords: ( hostname: string, ) => Effect.Effect<ReadonlyArray<{ readonly id: string }>, ManagedEndpointDnsClientError>; readonly createRecord: ( request: ManagedEndpointCnameRecordInput, ) => Effect.Effect<{ readonly id: string }, ManagedEndpointDnsClientError>; readonly updateRecord: ( dnsRecordId: string, request: ManagedEndpointCnameRecordInput, ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>; readonly deleteRecord: ( dnsRecordId: string, ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>; }>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointDnsClient") {} export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) => Layer.succeed(ManagedEndpointDnsClient, client); const requireCloudflareSettings = Effect.fnUntraced(function* ( settings: RelayConfiguration.RelayConfiguration["Service"], input: { readonly userId: string; readonly environmentId: string },) { const baseDomain = settings.managedEndpointBaseDomain; const namespace = settings.managedEndpointNamespace; const missingSettings: Array<"managedEndpointBaseDomain" | "managedEndpointNamespace"> = []; if (!baseDomain) { missingSettings.push("managedEndpointBaseDomain"); } if (!namespace) { missingSettings.push("managedEndpointNamespace"); } if (!baseDomain || !namespace) { return yield* new ManagedEndpointProvisioningNotConfigured({ ...input, missingSettings, }); } return { baseDomain, namespace, };}); function formatOriginService(origin: RelayManagedEndpointOrigin): string { const host = origin.localHttpHost.includes(":") ? `[${origin.localHttpHost.replace(/^\[(.*)\]$/u, "$1")}]` : origin.localHttpHost; return `http://${host}:${origin.localHttpPort}`;} function normalizeHostname(hostname: string): string { return hostname .trim() .toLowerCase() .replace(/\.$/u, "") .replace(/^\[(.*)\]$/u, "$1");} function isLoopbackOrigin(origin: RelayManagedEndpointOrigin): boolean { const hostname = normalizeHostname(origin.localHttpHost); return ( (hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost") && Number.isInteger(origin.localHttpPort) && origin.localHttpPort > 0 && origin.localHttpPort <= 65_535 );} function isNotFoundCause(cause: unknown): boolean { if (typeof cause !== "object" || cause === null) { return false; } if ("_tag" in cause && cause._tag === "NotFound") { return true; } if ("status" in cause && cause.status === 404) { return true; } return "cause" in cause && isNotFoundCause(cause.cause);} type ManagedEndpointClientError = ManagedEndpointTunnelClientError | ManagedEndpointDnsClientError; const ignoreNotFound = <A>( effect: Effect.Effect<A, ManagedEndpointClientError>,): Effect.Effect<void, ManagedEndpointClientError> => effect.pipe( Effect.asVoid, Effect.catchTags({ ManagedEndpointTunnelClientError: (error) => isNotFoundCause(error.cause) ? Effect.void : Effect.fail(error), ManagedEndpointDnsClientError: (error) => isNotFoundCause(error.cause) ? Effect.void : Effect.fail(error), }), ); export const make = Effect.gen(function* () { const config = yield* RelayConfiguration.RelayConfiguration; const crypto = yield* Crypto.Crypto; const tunnels = yield* ManagedEndpointTunnelClient; const dns = yield* ManagedEndpointDnsClient; const allocations = yield* ManagedEndpointAllocations.ManagedEndpointAllocations; const updateExistingDnsRecords = Effect.fnUntraced(function* ( records: ReadonlyArray<{ readonly id: string }>, preferredDnsRecordId: string | null, dnsRecord: ManagedEndpointCnameRecordInput, ) { const keptRecord = records.find((record) => record.id === preferredDnsRecordId) ?? records[0]; if (keptRecord === undefined) { return null; } yield* Effect.forEach( records, (record) => (record.id === keptRecord.id ? Effect.void : dns.deleteRecord(record.id)), { discard: true }, ); yield* dns.updateRecord(keptRecord.id, dnsRecord); return keptRecord.id; }); const ensureDnsRecord = Effect.fnUntraced(function* ( hostname: string, preferredDnsRecordId: string | null, dnsRecord: ManagedEndpointCnameRecordInput, ) { if (preferredDnsRecordId !== null) { const checkpointedRecordUpdated = yield* dns .updateRecord(preferredDnsRecordId, dnsRecord) .pipe( Effect.as(true), Effect.catchTags({ ManagedEndpointDnsClientError: (error) => isNotFoundCause(error.cause) ? Effect.succeed(false) : Effect.fail(error), }), ); if (checkpointedRecordUpdated) { return preferredDnsRecordId; } } const existingDnsRecords = yield* dns.listRecords(hostname); const existingDnsRecordId = yield* updateExistingDnsRecords( existingDnsRecords, preferredDnsRecordId, dnsRecord, ); if (existingDnsRecordId !== null) { return existingDnsRecordId; } return yield* dns.createRecord(dnsRecord).pipe( Effect.map((record) => record.id), Effect.catchTags({ ManagedEndpointDnsClientError: (createError) => Effect.gen(function* () { let records = yield* dns.listRecords(hostname); for (let attempt = 0; records.length === 0 && attempt < 4; attempt++) { yield* Effect.sleep("200 millis"); records = yield* dns.listRecords(hostname); } return records; }).pipe( Effect.flatMap((records) => records.length > 0 ? updateExistingDnsRecords(records, preferredDnsRecordId, dnsRecord) : Effect.fail(createError), ), Effect.flatMap((dnsRecordId) => dnsRecordId === null ? Effect.fail(createError) : Effect.succeed(dnsRecordId), ), ), }), ); }); return ManagedEndpointProvider.of({ deprovision: Effect.fn("relay.managed_endpoint_provider.deprovision")(function* (input) { yield* Effect.annotateCurrentSpan({ "relay.user_id": input.userId, "relay.environment_id": input.environmentId, }); const allocation = yield* allocations.get(input).pipe( Effect.mapError( (cause) => new ManagedEndpointDeprovisioningFailed({ ...input, stage: "load-allocation", cause, }), ), ); if (allocation === null) { return; } const dnsRecordId = allocation.dnsRecordId; if (dnsRecordId !== null) { yield* ignoreNotFound(dns.deleteRecord(dnsRecordId)).pipe( Effect.mapError( (cause) => new ManagedEndpointDeprovisioningFailed({ ...input, stage: "delete-dns-record", dnsRecordId, cause, }), ), ); } const tunnelId = allocation.tunnelId; if (tunnelId !== null) { yield* ignoreNotFound(tunnels.delete(tunnelId)).pipe( Effect.mapError( (cause) => new ManagedEndpointDeprovisioningFailed({ ...input, stage: "delete-tunnel", tunnelId, cause, }), ), ); } yield* allocations.remove(input).pipe( Effect.mapError( (cause) => new ManagedEndpointDeprovisioningFailed({ ...input, stage: "remove-allocation", ...(allocation.tunnelId === null ? {} : { tunnelId: allocation.tunnelId }), ...(allocation.dnsRecordId === null ? {} : { dnsRecordId: allocation.dnsRecordId }), cause, }), ), ); }), provision: Effect.fn("relay.managed_endpoint_provider.provision")(function* (input) { yield* Effect.annotateCurrentSpan({ "relay.user_id": input.userId, "relay.environment_id": input.environmentId, "relay.managed_endpoint.origin_host": input.origin.localHttpHost, "relay.managed_endpoint.origin_port": input.origin.localHttpPort, }); if (!isLoopbackOrigin(input.origin)) { return yield* new ManagedEndpointOriginNotAllowed({ userId: input.userId, environmentId: input.environmentId, host: input.origin.localHttpHost, port: input.origin.localHttpPort, }); } const cf = yield* requireCloudflareSettings(config, input); const environmentHash = yield* crypto .digest( "SHA-256", new TextEncoder().encode( managedEndpointDigestInput(cf.namespace, input.userId, input.environmentId), ), ) .pipe( Effect.map(Encoding.encodeHex), Effect.mapError( (cause) => new ManagedEndpointProvisioningFailed({ userId: input.userId, environmentId: input.environmentId, stage: "derive-environment-hash", cause, }), ), );Lines 1–500 of 835. Load more
import * as Alchemy from "alchemy"; import * as Cloudflare from "alchemy/Cloudflare"; import * as Arr from "effect/Array"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import type { RelayManagedEndpoint, RelayManagedEndpointOrigin, RelayManagedEndpointRuntimeConfig, } from "@t3tools/contracts/relay"; import * as RelayConfiguration from "../Config.ts"; import { managedEndpointDigestInput, managedEndpointForHostname, managedEndpointHostname, managedEndpointTunnelName, } from "../deploymentConfig.ts"; import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts"; export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()( "ManagedEndpointProvisioningNotConfigured", { userId: Schema.String, environmentId: Schema.String, missingSettings: Schema.Array( Schema.Literals(["managedEndpointBaseDomain", "managedEndpointNamespace"]), ), }, ) { override get message(): string { return `Managed endpoint provisioning is not configured for user '${this.userId}', environment '${this.environmentId}': missing ${this.missingSettings.join(", ")}`; } } const ManagedEndpointProvisioningStage = Schema.Literals([ "derive-environment-hash", "reserve-allocation", "ensure-tunnel", "validate-tunnel-response", "record-tunnel", "configure-tunnel", "ensure-dns-record", "record-dns", "get-tunnel-token", "mark-allocation-ready", ]); export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointProvisioningFailed>()( "ManagedEndpointProvisioningFailed", { stage: ManagedEndpointProvisioningStage, userId: Schema.String, environmentId: Schema.String, hostname: Schema.optionalKey(Schema.String), tunnelName: Schema.optionalKey(Schema.String), tunnelId: Schema.optionalKey(Schema.String), dnsRecordId: Schema.optionalKey(Schema.String), returnedTunnelName: Schema.optionalKey(Schema.String), returnedTunnelId: Schema.optionalKey(Schema.String), cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return `Managed endpoint provisioning failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`; } } const ManagedEndpointDeprovisioningStage = Schema.Literals([ "load-allocation", "delete-dns-record", "delete-tunnel", "remove-allocation", ]); export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointDeprovisioningFailed>()( "ManagedEndpointDeprovisioningFailed", { stage: ManagedEndpointDeprovisioningStage, userId: Schema.String, environmentId: Schema.String, tunnelId: Schema.optionalKey(Schema.String), dnsRecordId: Schema.optionalKey(Schema.String), cause: Schema.Defect(), }, ) { override get message(): string { return `Managed endpoint deprovisioning failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`; } } export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass<ManagedEndpointOriginNotAllowed>()( "ManagedEndpointOriginNotAllowed", { userId: Schema.String, environmentId: Schema.String, host: Schema.String, port: Schema.Number, }, ) { override get message(): string { return `Managed endpoint origin '${this.host}:${this.port}' is not allowed for user '${this.userId}', environment '${this.environmentId}'`; } } export type ManagedEndpointProviderError = | ManagedEndpointProvisioningNotConfigured | ManagedEndpointProvisioningFailed | ManagedEndpointOriginNotAllowed; export interface ManagedEndpointProvisioningResult { readonly endpoint: RelayManagedEndpoint; readonly runtime: RelayManagedEndpointRuntimeConfig; } export class ManagedEndpointProvider extends Context.Service< ManagedEndpointProvider, { readonly provision: (input: { readonly userId: string; readonly environmentId: string; readonly origin: RelayManagedEndpointOrigin; }) => Effect.Effect<ManagedEndpointProvisioningResult, ManagedEndpointProviderError>; readonly deprovision: (input: { readonly userId: string; readonly environmentId: string; }) => Effect.Effect<void, ManagedEndpointDeprovisioningFailed>; } >()("t3code-relay/environments/ManagedEndpointProvider") {} interface ManagedEndpointTunnel { readonly id?: string | null; readonly name?: string | null; } const ManagedEndpointTunnelClientOperation = Schema.Literals([ "list", "create", "put-configuration", "get-token", "delete", ]); export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<ManagedEndpointTunnelClientError>()( "ManagedEndpointTunnelClientError", { operation: ManagedEndpointTunnelClientOperation, tunnelName: Schema.optionalKey(Schema.String), tunnelId: Schema.optionalKey(Schema.String), cause: Schema.Defect(), }, ) { override get message(): string { const target = this.tunnelId ?? this.tunnelName; return `Managed endpoint tunnel provider '${this.operation}' request failed${target === undefined ? "" : ` for '${target}'`}`; } } export class ManagedEndpointTunnelClient extends Context.Service< ManagedEndpointTunnelClient, { readonly list: (request: { readonly name: string; readonly isDeleted: false; }) => Effect.Effect< { readonly result: ReadonlyArray<ManagedEndpointTunnel> }, ManagedEndpointTunnelClientError >; readonly create: (request: { readonly name: string; readonly configSrc: "cloudflare"; }) => Effect.Effect<ManagedEndpointTunnel, ManagedEndpointTunnelClientError>; readonly putConfiguration: ( tunnelId: string, config: { readonly ingress: Array<{ readonly hostname?: string; readonly service: string; }>; }, ) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>; readonly getToken: ( tunnelId: string, ) => Effect.Effect<string, ManagedEndpointTunnelClientError>; readonly delete: (tunnelId: string) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>; } >()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointTunnelClient") {} export const layerTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) => Layer.succeed(ManagedEndpointTunnelClient, client); interface ManagedEndpointCnameRecordInput { readonly type: "CNAME"; readonly name: string; readonly content: string; readonly ttl: 1; readonly proxied: true; } const ManagedEndpointDnsClientOperation = Schema.Literals([ "list-records", "create-record", "update-record", "delete-record", ]); export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<ManagedEndpointDnsClientError>()( "ManagedEndpointDnsClientError", { operation: ManagedEndpointDnsClientOperation, hostname: Schema.optionalKey(Schema.String), dnsRecordId: Schema.optionalKey(Schema.String), cause: Schema.Defect(), }, ) { override get message(): string { const target = this.dnsRecordId ?? this.hostname; return `Managed endpoint DNS provider '${this.operation}' request failed${target === undefined ? "" : ` for '${target}'`}`; } } export class ManagedEndpointDnsClient extends Context.Service< ManagedEndpointDnsClient, { readonly listRecords: ( hostname: string, ) => Effect.Effect<ReadonlyArray<{ readonly id: string }>, ManagedEndpointDnsClientError>; readonly createRecord: ( request: ManagedEndpointCnameRecordInput, ) => Effect.Effect<{ readonly id: string }, ManagedEndpointDnsClientError>; readonly updateRecord: ( dnsRecordId: string, request: ManagedEndpointCnameRecordInput, ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>; readonly deleteRecord: ( dnsRecordId: string, ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>; } >()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointDnsClient") {} export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) => Layer.succeed(ManagedEndpointDnsClient, client); const requireCloudflareSettings = Effect.fnUntraced(function* ( settings: RelayConfiguration.RelayConfiguration["Service"], input: { readonly userId: string; readonly environmentId: string }, ) { const baseDomain = settings.managedEndpointBaseDomain; const namespace = settings.managedEndpointNamespace; const missingSettings: Array<"managedEndpointBaseDomain" | "managedEndpointNamespace"> = []; if (!baseDomain) { missingSettings.push("managedEndpointBaseDomain"); } if (!namespace) { missingSettings.push("managedEndpointNamespace"); } if (!baseDomain || !namespace) { return yield* new ManagedEndpointProvisioningNotConfigured({ ...input, missingSettings, }); } return { baseDomain, namespace, }; }); function formatOriginService(origin: RelayManagedEndpointOrigin): string { const host = origin.localHttpHost.includes(":") ? `[${origin.localHttpHost.replace(/^\[(.*)\]$/u, "$1")}]` : origin.localHttpHost; return `http://${host}:${origin.localHttpPort}`; } function normalizeHostname(hostname: string): string { return hostname .trim() .toLowerCase() .replace(/\.$/u, "") .replace(/^\[(.*)\]$/u, "$1"); } function isLoopbackOrigin(origin: RelayManagedEndpointOrigin): boolean { const hostname = normalizeHostname(origin.localHttpHost); return ( (hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost") && Number.isInteger(origin.localHttpPort) && origin.localHttpPort > 0 && origin.localHttpPort <= 65_535 ); } function isNotFoundCause(cause: unknown): boolean { if (typeof cause !== "object" || cause === null) { return false; } if ("_tag" in cause && cause._tag === "NotFound") { return true; } if ("status" in cause && cause.status === 404) { return true; } return "cause" in cause && isNotFoundCause(cause.cause); } type ManagedEndpointClientError = ManagedEndpointTunnelClientError | ManagedEndpointDnsClientError; const ignoreNotFound = <A>( effect: Effect.Effect<A, ManagedEndpointClientError>, ): Effect.Effect<void, ManagedEndpointClientError> => effect.pipe( Effect.asVoid, Effect.catchTags({ ManagedEndpointTunnelClientError: (error) => isNotFoundCause(error.cause) ? Effect.void : Effect.fail(error), ManagedEndpointDnsClientError: (error) => isNotFoundCause(error.cause) ? Effect.void : Effect.fail(error), }), ); export const make = Effect.gen(function* () { const config = yield* RelayConfiguration.RelayConfiguration; const crypto = yield* Crypto.Crypto; const tunnels = yield* ManagedEndpointTunnelClient; const dns = yield* ManagedEndpointDnsClient; const allocations = yield* ManagedEndpointAllocations.ManagedEndpointAllocations; const updateExistingDnsRecords = Effect.fnUntraced(function* ( records: ReadonlyArray<{ readonly id: string }>, preferredDnsRecordId: string | null, dnsRecord: ManagedEndpointCnameRecordInput, ) { const keptRecord = records.find((record) => record.id === preferredDnsRecordId) ?? records[0]; if (keptRecord === undefined) { return null; } yield* Effect.forEach( records, (record) => (record.id === keptRecord.id ? Effect.void : dns.deleteRecord(record.id)), { discard: true }, ); yield* dns.updateRecord(keptRecord.id, dnsRecord); return keptRecord.id; }); const ensureDnsRecord = Effect.fnUntraced(function* ( hostname: string, preferredDnsRecordId: string | null, dnsRecord: ManagedEndpointCnameRecordInput, ) { if (preferredDnsRecordId !== null) { const checkpointedRecordUpdated = yield* dns .updateRecord(preferredDnsRecordId, dnsRecord) .pipe( Effect.as(true), Effect.catchTags({ ManagedEndpointDnsClientError: (error) => isNotFoundCause(error.cause) ? Effect.succeed(false) : Effect.fail(error), }), ); if (checkpointedRecordUpdated) { return preferredDnsRecordId; } } const existingDnsRecords = yield* dns.listRecords(hostname); const existingDnsRecordId = yield* updateExistingDnsRecords( existingDnsRecords, preferredDnsRecordId, dnsRecord, ); if (existingDnsRecordId !== null) { return existingDnsRecordId; } return yield* dns.createRecord(dnsRecord).pipe( Effect.map((record) => record.id), Effect.catchTags({ ManagedEndpointDnsClientError: (createError) => Effect.gen(function* () { let records = yield* dns.listRecords(hostname); for (let attempt = 0; records.length === 0 && attempt < 4; attempt++) { yield* Effect.sleep("200 millis"); records = yield* dns.listRecords(hostname); } return records; }).pipe( Effect.flatMap((records) => records.length > 0 ? updateExistingDnsRecords(records, preferredDnsRecordId, dnsRecord) : Effect.fail(createError), ), Effect.flatMap((dnsRecordId) => dnsRecordId === null ? Effect.fail(createError) : Effect.succeed(dnsRecordId), ), ), }), ); }); return ManagedEndpointProvider.of({ deprovision: Effect.fn("relay.managed_endpoint_provider.deprovision")(function* (input) { yield* Effect.annotateCurrentSpan({ "relay.user_id": input.userId, "relay.environment_id": input.environmentId, }); const allocation = yield* allocations.get(input).pipe( Effect.mapError( (cause) => new ManagedEndpointDeprovisioningFailed({ ...input, stage: "load-allocation", cause, }), ), ); if (allocation === null) { return; } const dnsRecordId = allocation.dnsRecordId; if (dnsRecordId !== null) { yield* ignoreNotFound(dns.deleteRecord(dnsRecordId)).pipe( Effect.mapError( (cause) => new ManagedEndpointDeprovisioningFailed({ ...input, stage: "delete-dns-record", dnsRecordId, cause, }), ), ); } const tunnelId = allocation.tunnelId; if (tunnelId !== null) { yield* ignoreNotFound(tunnels.delete(tunnelId)).pipe( Effect.mapError( (cause) => new ManagedEndpointDeprovisioningFailed({ ...input, stage: "delete-tunnel", tunnelId, cause, }), ), ); } yield* allocations.remove(input).pipe( Effect.mapError( (cause) => new ManagedEndpointDeprovisioningFailed({ ...input, stage: "remove-allocation", ...(allocation.tunnelId === null ? {} : { tunnelId: allocation.tunnelId }), ...(allocation.dnsRecordId === null ? {} : { dnsRecordId: allocation.dnsRecordId }), cause, }), ), ); }), provision: Effect.fn("relay.managed_endpoint_provider.provision")(function* (input) { yield* Effect.annotateCurrentSpan({ "relay.user_id": input.userId, "relay.environment_id": input.environmentId, "relay.managed_endpoint.origin_host": input.origin.localHttpHost, "relay.managed_endpoint.origin_port": input.origin.localHttpPort, }); if (!isLoopbackOrigin(input.origin)) { return yield* new ManagedEndpointOriginNotAllowed({ userId: input.userId, environmentId: input.environmentId, host: input.origin.localHttpHost, port: input.origin.localHttpPort, }); } const cf = yield* requireCloudflareSettings(config, input); const environmentHash = yield* crypto .digest( "SHA-256", new TextEncoder().encode( managedEndpointDigestInput(cf.namespace, input.userId, input.environmentId), ), ) .pipe( Effect.map(Encoding.encodeHex), Effect.mapError( (cause) => new ManagedEndpointProvisioningFailed({ userId: input.userId, environmentId: input.environmentId, stage: "derive-environment-hash", cause, }), ), );