diff --git a/infra/relay/src/deploymentConfig.test.ts b/infra/relay/src/deploymentConfig.test.ts index d7940b803188..44c7627a4daf 100644 --- a/infra/relay/src/deploymentConfig.test.ts +++ b/infra/relay/src/deploymentConfig.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vite-plus/test"; +import * as Schema from "effect/Schema"; import { managedEndpointDigestInput, @@ -7,11 +8,14 @@ import { isManagedEndpointHostname, managedEndpointTunnelName, relayOwnsManagedEndpointZone, + RelayPublicDomainLabelTooLongError, relayPublicDomainForStage, relayResourceNameForStage, relayStageSlug, } from "./deploymentConfig.ts"; +const isRelayPublicDomainLabelTooLongError = Schema.is(RelayPublicDomainLabelTooLongError); + describe("relayStageSlug", () => { it("matches Alchemy physical-name sanitization for default developer stages", () => { expect(relayStageSlug("dev_julius")).toBe("dev-julius"); @@ -28,6 +32,29 @@ describe("relayPublicDomainForStage", () => { "relay-dev-julius.example.com", ); }); + + it("reports the stage and derived DNS label when the label is too long", () => { + const stage = `dev_${"x".repeat(60)}`; + let error: unknown; + + try { + relayPublicDomainForStage(stage, "example.com"); + } catch (cause) { + error = cause; + } + + if (!isRelayPublicDomainLabelTooLongError(error)) { + throw error; + } + expect(error).toMatchObject({ + stage, + label: `relay-dev-${"x".repeat(60)}`, + maxLength: 63, + }); + expect(error.message).toBe( + `Relay stage '${stage}' produces custom domain label 'relay-dev-${"x".repeat(60)}' (70 characters), exceeding the DNS label limit of 63.`, + ); + }); }); describe("relayOwnsManagedEndpointZone", () => { diff --git a/infra/relay/src/deploymentConfig.ts b/infra/relay/src/deploymentConfig.ts index fbb130548220..fe9d37b29988 100644 --- a/infra/relay/src/deploymentConfig.ts +++ b/infra/relay/src/deploymentConfig.ts @@ -1,10 +1,24 @@ import type { RelayManagedEndpoint } from "@t3tools/contracts/relay"; +import * as Schema from "effect/Schema"; const DNS_LABEL_MAX_LENGTH = 63; const MANAGED_ENDPOINT_HASH_LENGTH = 16; const MANAGED_ENDPOINT_TUNNEL_PREFIX = "t3coderelay-managedendpoint"; export const MANAGED_ENDPOINT_ZONE_OWNER_STAGE = "prod"; +export class RelayPublicDomainLabelTooLongError extends Schema.TaggedErrorClass()( + "RelayPublicDomainLabelTooLongError", + { + stage: Schema.String, + label: Schema.String, + maxLength: Schema.Number, + }, +) { + override get message(): string { + return `Relay stage '${this.stage}' produces custom domain label '${this.label}' (${this.label.length} characters), exceeding the DNS label limit of ${this.maxLength}.`; + } +} + function normalizeZoneName(zoneName: string): string { return zoneName .trim() @@ -62,7 +76,11 @@ export function relayPublicDomainForStage(stage: string, zoneName: string): stri const stageSlug = relayStageSlug(stage); const relayLabel = stage === "prod" ? "relay" : `relay-${stageSlug}`; if (relayLabel.length > DNS_LABEL_MAX_LENGTH) { - throw new Error(`Relay stage is too long for a custom domain: ${stage}`); + throw new RelayPublicDomainLabelTooLongError({ + stage, + label: relayLabel, + maxLength: DNS_LABEL_MAX_LENGTH, + }); } return `${relayLabel}.${normalizeZoneName(zoneName)}`; }