1import type { RelayManagedEndpoint } from "@t3tools/contracts/relay";
2import * as Schema from "effect/Schema";
3
4const DNS_LABEL_MAX_LENGTH = 63;
5const MANAGED_ENDPOINT_HASH_LENGTH = 16;
6const MANAGED_ENDPOINT_TUNNEL_PREFIX = "t3coderelay-managedendpoint";
7export const MANAGED_ENDPOINT_ZONE_OWNER_STAGE = "prod";
8
9export 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
22function normalizeZoneName(zoneName: string): string {
23 return zoneName
24 .trim()
25 .toLowerCase()
26 .replace(/^\.+|\.+$/g, "");
27}
28
29function 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
44function stableSuffix(hash: string): string {
45 return hash.toLowerCase().slice(0, MANAGED_ENDPOINT_HASH_LENGTH);
46}
47
48function 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// Match Alchemy's post-stage name sanitization for domains and runtime-created resources.
56function relayStageSlug(stage: string): string {
57 return stage
58 .toLowerCase()
59 .replaceAll(/[^a-z0-9-]/g, "-")
60 .replaceAll(/-+/g, "-")
61 .replace(/^-+|-+$/g, "");
62}
63
64export function relayResourceNameForStage(name: string, stage: string): string {
65 return `${name}-${relayStageSlug(stage)}`;
66}
67
68export function relayOwnsManagedEndpointZone(stage: string): boolean {
69 return stage === MANAGED_ENDPOINT_ZONE_OWNER_STAGE;
70}
71
72export function relayPublicDomainForStage(stage: string, zoneName: string): string {
73 const stageSlug = relayStageSlug(stage);
74 const relayLabel = stage === "prod" ? "relay" : `relay-${stageSlug}`;
75 if (relayLabel.length > DNS_LABEL_MAX_LENGTH) {
76 throw new RelayPublicDomainLabelTooLongError({
77 stage,
78 label: relayLabel,
79 maxLength: DNS_LABEL_MAX_LENGTH,
80 });
81 }
82 return `${relayLabel}.${normalizeZoneName(zoneName)}`;
83}
84
85export function managedEndpointDigestInput(
86 stage: string,
87 userId: string,
88 environmentId: string,
89): string {
90 return `${stage}:${userId}:${environmentId}`;
91}
92
93export function managedEndpointHostname(stage: string, baseDomain: string, hash: string): string {
94 const label = appendDnsSafeSuffix(relayStageSlug(stage), stableSuffix(hash));
95 return `${label}.${normalizeZoneName(baseDomain)}`;
96}
97
98export function isManagedEndpointHostname(hostname: string, baseDomain: string): boolean {
99 const normalizedHostname = normalizeZoneName(hostname);
100 const normalizedBaseDomain = normalizeZoneName(baseDomain);
101 return (
102 hostname === normalizedHostname &&
103 isDnsName(normalizedHostname) &&
104 isDnsName(normalizedBaseDomain) &&
105 normalizedHostname.endsWith(`.${normalizedBaseDomain}`)
106 );
107}
108
109export function managedEndpointForHostname(hostname: string): RelayManagedEndpoint {
110 return {
111 httpBaseUrl: `https://${hostname}/`,
112 wsBaseUrl: `wss://${hostname}/ws`,
113 providerKind: "cloudflare_tunnel",
114 };
115}
116
117export function managedEndpointTunnelNamePrefix(stage: string): string {
118 return `${MANAGED_ENDPOINT_TUNNEL_PREFIX}-${relayStageSlug(stage)}-`;
119}
120
121export function managedEndpointTunnelName(stage: string, hash: string): string {
122 return `${managedEndpointTunnelNamePrefix(stage)}${stableSuffix(hash)}`;
123}
124
125/** Tunnel-name hash covers user and environment; public environment ids alone do not identify an authorized link. */
126export const MANAGED_ENDPOINT_KEY_PATTERN = new RegExp(
127 `^[0-9a-f]{${MANAGED_ENDPOINT_HASH_LENGTH}}$`,
128);
129
130export function managedEndpointTunnelNameForKey(stage: string, endpointKey: string): string {
131 return `${managedEndpointTunnelNamePrefix(stage)}${endpointKey}`;
132}