import type { RuntimeConfigArtifact } from "./release-artifacts.ts"; /** * ยง6.4. Which environment an artifact is allowed to be deployed to. * * Release coherence answers "do these artifacts describe each other?". It does * not answer "is this the artifact production should receive?", and the two are * not the same question: a build whose runtime document says `APP_ENV: local`, * `AUTH_MODE: demo` and `API_BASE_URL: http://localhost:8080/` is perfectly * coherent with itself. Without an admission step such a build is a valid * release candidate, and the only thing standing between it and production is * that nobody happened to promote it. * * Admission is therefore a separate, declared decision: a caller states the * target it intends, and this module says whether the artifact may go there. * Every rule below is a refusal, so an unrecognised target or an unreadable * field fails closed rather than passing by omission. */ export const DEPLOYMENT_TARGETS = Object.freeze([ "local", "development", "staging", "production", ] as const); export type DeploymentTarget = (typeof DEPLOYMENT_TARGETS)[number]; /** * Targets that serve real users over the public internet. They carry the full * rule set; `local` and `development` only have to be honest about what they * are. */ const PUBLIC_TARGETS: ReadonlySet = new Set([ "staging", "production", ]); /** Placeholder identifiers a developer build emits when nothing supplied one. */ const PLACEHOLDER_IDENTIFIERS: ReadonlySet = new Set([ "local-build", "local-release", "local", "dev", "unknown", ]); export type AdmissionViolation = Readonly<{ field: string; reason: string }>; export type AdmissionInput = RuntimeConfigArtifact & Readonly<{ BUILD_ID?: string; RELEASE_ID?: string }>; export function isDeploymentTarget(value: unknown): value is DeploymentTarget { return ( typeof value === "string" && (DEPLOYMENT_TARGETS as readonly string[]).includes(value) ); } /** * Every reason this artifact may not be deployed to `target`. An empty list is * the only admission. */ export function findAdmissionViolations( target: DeploymentTarget, config: AdmissionInput, ): readonly AdmissionViolation[] { const violations: AdmissionViolation[] = []; if (config.APP_ENV !== target) { violations.push({ field: "APP_ENV", reason: `artifact declares ${config.APP_ENV} but is being admitted to ${target}`, }); } if (!PUBLIC_TARGETS.has(target)) return Object.freeze(violations); if (config.AUTH_MODE !== "external") { violations.push({ field: "AUTH_MODE", reason: `${target} requires an external identity provider, not ${config.AUTH_MODE}`, }); } violations.push(...publicEndpointViolations("API_BASE_URL", config.API_BASE_URL)); if (config.TELEMETRY_ENABLED && !config.TELEMETRY_ENDPOINT) { violations.push({ field: "TELEMETRY_ENDPOINT", reason: "telemetry is enabled without an endpoint", }); } if (config.TELEMETRY_ENDPOINT) { violations.push( ...publicEndpointViolations("TELEMETRY_ENDPOINT", config.TELEMETRY_ENDPOINT), ); } for (const field of ["BUILD_ID", "RELEASE_ID"] as const) { const value = config[field]; if (typeof value !== "string" || value.length === 0) { violations.push({ field, reason: `${target} requires a build identity` }); continue; } if (PLACEHOLDER_IDENTIFIERS.has(value.toLowerCase())) { violations.push({ field, reason: `${value} is a developer placeholder, not a released identity`, }); } } return Object.freeze(violations); } function publicEndpointViolations( field: string, value: string, ): readonly AdmissionViolation[] { let url: URL; try { url = new URL(value); } catch { return [{ field, reason: "is not an absolute URL" }]; } const violations: AdmissionViolation[] = []; if (url.protocol !== "https:") { violations.push({ field, reason: `${url.protocol} is not permitted; use https` }); } if (isNonPublicHost(url.hostname)) { violations.push({ field, reason: `${url.hostname} is not reachable from a user's browser`, }); } return violations; } /** * Hosts that only resolve inside the machine or network that built the * artifact. A deployment pointing at one of these is a developer configuration * that escaped, not a production endpoint. */ function isNonPublicHost(hostname: string): boolean { const host = hostname.toLowerCase().replace(/^\[|\]$/gu, ""); if ( host === "localhost" || host.endsWith(".localhost") || host === "::1" || host === "0.0.0.0" || host === "::" ) { return true; } const octets = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u.exec(host); if (!octets) return false; const [first, second] = [Number(octets[1]), Number(octets[2])]; return ( first === 127 || first === 10 || (first === 192 && second === 168) || (first === 172 && second >= 16 && second <= 31) || (first === 169 && second === 254) ); }