| 1 | import type { RelayManagedEndpoint } from "@t3tools/contracts/relay"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | |
| 4 | const DNS_LABEL_MAX_LENGTH = 63; |
| 5 | const MANAGED_ENDPOINT_HASH_LENGTH = 16; |
| 6 | const MANAGED_ENDPOINT_TUNNEL_PREFIX = "t3coderelay-managedendpoint"; |
| 7 | export const MANAGED_ENDPOINT_ZONE_OWNER_STAGE = "prod"; |
| 8 | |
| 9 | export class RelayPublicDomainLabelTooLongError extends Schema.TaggedError<RelayPublicDomainLabelTooLongError>()( |
| 10 | "RelayPublicDomainLabelTooLongError", |
| 11 | { |
| 12 | stage: Schema.String, |
| 13 | label: Schema.String, |
| 14 | maxLength: Schema.Number, |
| 15 | }, |
| 16 | ) { |
| 17 | override get message(): string { |
| 18 | return `Relay stage '${this.stage}' produces custom domain label '${this.label}' (${this.label.length} characters), exceeding the DNS label limit of ${this.maxLength}.`; |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | function normalizeZoneName(zoneName: string): string { |
| 23 | return zoneName |
| 24 | .trim() |
| 25 | .toLowerCase() |
| 26 | .replace(/^\.+|\.+$/g, ""); |
| 27 | } |
| 28 | |
| 29 | function isDnsName(name: string): boolean { |
| 30 | return ( |
| 31 | name.length > 0 && |
| 32 | name.length <= 253 && |
| 33 | name |
| 34 | .split(".") |
| 35 | .every( |
| 36 | (label) => |
| 37 | label.length > 0 && |
| 38 | label.length <= DNS_LABEL_MAX_LENGTH && |
| 39 | /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label), |
| 40 | ) |
| 41 | ); |
| 42 | } |
| 43 | |
| 44 | function stableSuffix(hash: string): string { |
| 45 | return hash.toLowerCase().slice(0, MANAGED_ENDPOINT_HASH_LENGTH); |
| 46 | } |
| 47 | |
| 48 | function appendDnsSafeSuffix(prefix: string, suffix: string): string { |
| 49 | const truncatedPrefix = prefix |
| 50 | .slice(0, DNS_LABEL_MAX_LENGTH - suffix.length - 1) |
| 51 | .replace(/-+$/g, ""); |
| 52 | return `${truncatedPrefix}-${suffix}`; |
| 53 | } |
| 54 | |
| 55 | /** |
| 56 | * Alchemy's physical-name helper sanitizes resource names after adding the |
| 57 | * stage. Keep custom domains and runtime-created resources aligned with it. |
| 58 | */ |
| 59 | function relayStageSlug(stage: string): string { |
| 60 | return stage |
| 61 | .toLowerCase() |
| 62 | .replaceAll(/[^a-z0-9-]/g, "-") |
| 63 | .replaceAll(/-+/g, "-") |
| 64 | .replace(/^-+|-+$/g, ""); |
| 65 | } |
| 66 | |
| 67 | export function relayResourceNameForStage(name: string, stage: string): string { |
| 68 | return `${name}-${relayStageSlug(stage)}`; |
| 69 | } |
| 70 | |
| 71 | export function relayOwnsManagedEndpointZone(stage: string): boolean { |
| 72 | return stage === MANAGED_ENDPOINT_ZONE_OWNER_STAGE; |
| 73 | } |
| 74 | |
| 75 | export function relayPublicDomainForStage(stage: string, zoneName: string): string { |
| 76 | const stageSlug = relayStageSlug(stage); |
| 77 | const relayLabel = stage === "prod" ? "relay" : `relay-${stageSlug}`; |
| 78 | if (relayLabel.length > DNS_LABEL_MAX_LENGTH) { |
| 79 | throw new RelayPublicDomainLabelTooLongError({ |
| 80 | stage, |
| 81 | label: relayLabel, |
| 82 | maxLength: DNS_LABEL_MAX_LENGTH, |
| 83 | }); |
| 84 | } |
| 85 | return `${relayLabel}.${normalizeZoneName(zoneName)}`; |
| 86 | } |
| 87 | |
| 88 | export function managedEndpointDigestInput( |
| 89 | stage: string, |
| 90 | userId: string, |
| 91 | environmentId: string, |
| 92 | ): string { |
| 93 | return `${stage}:${userId}:${environmentId}`; |
| 94 | } |
| 95 | |
| 96 | export function managedEndpointHostname(stage: string, baseDomain: string, hash: string): string { |
| 97 | const label = appendDnsSafeSuffix(relayStageSlug(stage), stableSuffix(hash)); |
| 98 | return `${label}.${normalizeZoneName(baseDomain)}`; |
| 99 | } |
| 100 | |
| 101 | export function isManagedEndpointHostname(hostname: string, baseDomain: string): boolean { |
| 102 | const normalizedHostname = normalizeZoneName(hostname); |
| 103 | const normalizedBaseDomain = normalizeZoneName(baseDomain); |
| 104 | return ( |
| 105 | hostname === normalizedHostname && |
| 106 | isDnsName(normalizedHostname) && |
| 107 | isDnsName(normalizedBaseDomain) && |
| 108 | normalizedHostname.endsWith(`.${normalizedBaseDomain}`) |
| 109 | ); |
| 110 | } |
| 111 | |
| 112 | export function managedEndpointForHostname(hostname: string): RelayManagedEndpoint { |
| 113 | return { |
| 114 | httpBaseUrl: `https://${hostname}/`, |
| 115 | wsBaseUrl: `wss://${hostname}/ws`, |
| 116 | providerKind: "cloudflare_tunnel", |
| 117 | }; |
| 118 | } |
| 119 | |
| 120 | export function managedEndpointTunnelNamePrefix(stage: string): string { |
| 121 | return `${MANAGED_ENDPOINT_TUNNEL_PREFIX}-${relayStageSlug(stage)}-`; |
| 122 | } |
| 123 | |
| 124 | export function managedEndpointTunnelName(stage: string, hash: string): string { |
| 125 | return `${managedEndpointTunnelNamePrefix(stage)}${stableSuffix(hash)}`; |
| 126 | } |
| 127 | |
| 128 | /** |
| 129 | * A managed endpoint's public key in webhook URLs: the hash suffix its tunnel |
| 130 | * name ends with. The hash covers user and environment, so one key names |
| 131 | * exactly one link, unlike the environment id, which any account can claim. |
| 132 | */ |
| 133 | export const MANAGED_ENDPOINT_KEY_PATTERN = new RegExp( |
| 134 | `^[0-9a-f]{${MANAGED_ENDPOINT_HASH_LENGTH}}$`, |
| 135 | ); |
| 136 | |
| 137 | export function managedEndpointTunnelNameForKey(stage: string, endpointKey: string): string { |
| 138 | return `${managedEndpointTunnelNamePrefix(stage)}${endpointKey}`; |
| 139 | } |