const LOOPBACK_IPV4 = /^127(?:\.\d{1,3}){3}$/; /** * A release gate must not promote a local preview server as live hosting * evidence. * * @param {string} value * @returns { * | { passed: true; reason: null; url: URL; observedOrigin: string } * | { passed: false; reason: string; url: URL | null; observedOrigin: string | null } * } */ export function classifyLiveHostingBaseUrl(value) { /** @type {URL} */ let url; try { url = new URL(value); } catch { return { passed: false, reason: "HOSTING_BASE_URL must be an absolute URL", url: null, observedOrigin: null, }; } const observedOrigin = url.origin; const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, ""); if (url.protocol !== "https:") { return { passed: false, reason: "live hosting evidence requires HTTPS", url, observedOrigin, }; } if (url.username || url.password) { return { passed: false, reason: "HOSTING_BASE_URL must not contain credentials", url, observedOrigin, }; } if ( hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname === "0.0.0.0" || LOOPBACK_IPV4.test(hostname) ) { return { passed: false, reason: "local or loopback hosts are not live deployment evidence", url, observedOrigin, }; } if (url.pathname !== "/" || url.search || url.hash) { return { passed: false, reason: "HOSTING_BASE_URL must be the canonical root URL", url, observedOrigin, }; } return { passed: true, reason: null, url, observedOrigin }; }