835 lines · 27.4 KB · TypeScript
RawDownload
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.