/*
RFC-1325: single predicate module for site-host classification. Every
host-dependent branch (access protection, dns upsert, search verification,
dns-aid generation, certify) MUST import from here — a local `workers.dev`
or `^(dev|alt)[.-]` literal is a HOST-01 defect.
DNS record management or domain registration — this module only classifies and derives host strings.PIN/access logic — access-protection.ts consumes isDevOrAltHost from here.Lives under middleware/ because the deployed Worker bundle reaches shared only via ./middleware/* subpaths; werkstatt-shared has no sternsystem/ export (plan escalation resolution).channelHost takes the channel workerName so provisional hosts map to <workerName>.<account-subdomain> — the caller owns worker-name derivation (dev-<id> etc.).RFC-1325: initial — SiteHostKind, siteHostKind, isProvisionalHost, channelHost; isDevOrAltHost rehomed from access-protection.ts.RFC-1325: provisional workers.dev as first-class site host
Single host predicate module in werkstatt-shared; provisional registration derives dev/alt/main workers.dev channels from the account subdomain; first dev deploy auto-protects provisional hosts; provisional main responses carry noindex; dns-aid skips provisional sites; site.domain.adopt cuts over to a custom domain.RFC-1325: review r1 — pin-carrying probes, AC-3 write ordering, dns-aid validate parity, hardened host lint
*/
/** RFC-1325 host classification: a real registered domain vs the provisional account workers.dev host. */
export type SiteHostKind = "custom-domain" | "provisional-workers-dev";
const WORKERS_DEV_SUFFIX = ".workers.dev";
/** Strip a scheme, port, and trailing path so callers may pass a domain or a URL. */
function normalizeHost(domain: string): string {
return domain
.toLowerCase()
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "")
.replace(/:\d+$/, "")
.replace(/\.$/, "");
}
/** Classify a site domain: `*.workers.dev` hosts are provisional, everything else is a custom domain. */
export function siteHostKind(domain: string): SiteHostKind {
const host = normalizeHost(domain);
return host === "workers.dev" || host.endsWith(WORKERS_DEV_SUFFIX)
? "provisional-workers-dev"
: "custom-domain";
}
/** True when `domain` is set and resolves to the provisional workers.dev host. */
export function isProvisionalHost(domain: string | undefined): boolean {
return (
domain !== undefined && domain.length > 0 && siteHostKind(domain) === "provisional-workers-dev"
);
}
/**
* Derive the serving host for a channel.
* - custom-domain: `dev.` / `alt.` / `` (RFC-0899 convention).
* - provisional-workers-dev: `.` — the worker
* name IS the host label (`dev-`, `alt-`, ``), so main and dev/alt
* share the same derivation. `workerName` is the caller-derived channel worker
* name (RFC-0902 rule: adoption renames hosts, never worker names).
*/
export function channelHost(
kind: SiteHostKind,
domain: string,
channel: "dev" | "alt" | "main",
workerName: string,
): string {
if (kind === "custom-domain") {
if (channel === "dev") return `dev.${domain}`;
if (channel === "alt") return `alt.${domain}`;
return domain;
}
const parts = normalizeHost(domain).split(".");
if (parts.length < 4) {
throw new Error(
`[site-host] provisional channel host needs a ..workers.dev domain, got '${domain}'`,
);
}
const suffix = parts.slice(1).join(".");
return `${workerName}.${suffix}`;
}
/** Build a worker's FQDN on the account subdomain: `..workers.dev`. */
export function workersDevHost(workerName: string, accountSubdomain: string): string {
return `${workerName}.${accountSubdomain}${WORKERS_DEV_SUFFIX}`;
}
/**
* Dev/alt channel hosts: `dev.` / `alt.` on a custom domain,
* and `dev-..workers.dev` / `alt-…` on a provisional
* workers.dev host — the single `^(dev|alt)[.-]` rule (rehomed from
* access-protection.ts; the private copy at certify.ts:1364 is deleted).
*/
export function isDevOrAltHost(host: string): boolean {
return /^(dev|alt)[.-]/i.test(host);
}
const HOST_LABEL_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
/**
* True for a bare lowercase host name (`