chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
import { mkdir, readFile, readdir } from "node:fs/promises";
|
||||
|
||||
import { hostingHeadersArtifactSchema } from "./contracts/release-artifacts.ts";
|
||||
import { classifyLiveHostingBaseUrl } from "./lib/hosting-probe.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
|
||||
type Document = Record<string, unknown>;
|
||||
type ResponseHeaders = Record<string, Record<string, string>>;
|
||||
type HostingMode = "live" | "invalid-live" | "fixture";
|
||||
type ProbeResult = Readonly<{
|
||||
surface: string;
|
||||
header: string;
|
||||
expected: unknown;
|
||||
observed: unknown;
|
||||
reason?: string;
|
||||
passed: boolean;
|
||||
}>;
|
||||
|
||||
function isRecord(value: unknown): value is Document {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function recordValue(value: unknown): Document {
|
||||
return isRecord(value) ? value : {};
|
||||
}
|
||||
|
||||
function strings(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.filter((entry): entry is string => typeof entry === "string")
|
||||
: [];
|
||||
}
|
||||
|
||||
function stringRecord(value: unknown): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(recordValue(value)).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function responseHeaders(value: unknown): ResponseHeaders {
|
||||
return Object.fromEntries(
|
||||
Object.entries(recordValue(value)).map(([surface, headers]) => [
|
||||
surface,
|
||||
stringRecord(headers),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
async function readDocument(file: string): Promise<Document> {
|
||||
const parsed: unknown = JSON.parse(await readFile(file, "utf8"));
|
||||
if (!isRecord(parsed)) throw new Error(`${file} must be a JSON object`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const cachePolicy = await readDocument("config/hosting/cache-policy.json");
|
||||
const cacheSurfaces = recordValue(cachePolicy.surfaces);
|
||||
const securityPolicy = await readDocument(
|
||||
"config/hosting/security-headers.json",
|
||||
);
|
||||
const securityHeaders = stringRecord(securityPolicy.headers);
|
||||
const baseUrl = process.env.HOSTING_BASE_URL;
|
||||
const liveTarget = baseUrl ? classifyLiveHostingBaseUrl(baseUrl) : null;
|
||||
const distFiles = (await readdir("dist", { recursive: true })).map(String);
|
||||
const publicSourceMaps = distFiles.filter((file) => file.endsWith(".map"));
|
||||
const publicServiceWorkers = distFiles.filter((file) =>
|
||||
/(?:^|\/)(?:service-worker|sw)(?:[.-][^/]*)?\.js$/i.test(file),
|
||||
);
|
||||
|
||||
let responses: ResponseHeaders = {};
|
||||
let mode: HostingMode;
|
||||
const probeResults: ProbeResult[] = [];
|
||||
|
||||
if (liveTarget?.passed) {
|
||||
mode = "live";
|
||||
const assets = await readdir("dist/assets");
|
||||
const hashedJavaScript = assets.find((file) => file.endsWith(".js"));
|
||||
if (!hashedJavaScript) throw new Error("No built hashed JavaScript found.");
|
||||
const paths = {
|
||||
index: "/",
|
||||
runtimeConfig: "/config.json",
|
||||
releaseManifest: "/release-manifest.json",
|
||||
hashedAsset: `/assets/${hashedJavaScript}`,
|
||||
};
|
||||
for (const [surface, pathname] of Object.entries(paths)) {
|
||||
const requestedUrl = new URL(pathname, liveTarget.url);
|
||||
try {
|
||||
const response = await fetch(requestedUrl, { redirect: "follow" });
|
||||
const finalUrl = new URL(response.url);
|
||||
probeResults.push(
|
||||
{
|
||||
surface,
|
||||
header: "http-status",
|
||||
expected: 200,
|
||||
observed: response.status,
|
||||
passed: response.status === 200,
|
||||
},
|
||||
{
|
||||
surface,
|
||||
header: "final-origin",
|
||||
expected: liveTarget.url.origin,
|
||||
observed: finalUrl.origin,
|
||||
passed: finalUrl.origin === liveTarget.url.origin,
|
||||
},
|
||||
);
|
||||
responses[surface] = Object.fromEntries(
|
||||
[...response.headers.entries()].map(([name, value]) => [
|
||||
name.toLowerCase(),
|
||||
value,
|
||||
]),
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
probeResults.push({
|
||||
surface,
|
||||
header: "transport",
|
||||
expected: "reachable",
|
||||
observed: error instanceof Error ? error.name : "UnknownError",
|
||||
passed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (liveTarget) {
|
||||
mode = "invalid-live";
|
||||
probeResults.push({
|
||||
surface: "deployment",
|
||||
header: "base-url",
|
||||
expected: "canonical non-loopback HTTPS root URL",
|
||||
observed: liveTarget.observedOrigin,
|
||||
reason: liveTarget.reason,
|
||||
passed: false,
|
||||
});
|
||||
} else {
|
||||
mode = "fixture";
|
||||
const fixture = await readDocument(
|
||||
"config/hosting/response-headers.fixture.json",
|
||||
);
|
||||
responses = responseHeaders(fixture.responses);
|
||||
}
|
||||
|
||||
const results: ProbeResult[] = [...probeResults];
|
||||
for (const [surface, rawPolicy] of Object.entries(cacheSurfaces)) {
|
||||
const policy = recordValue(rawPolicy);
|
||||
if (typeof policy.cacheControl !== "string") continue;
|
||||
const contentTypes = strings(policy.contentTypes);
|
||||
const observed = responses[surface]?.["cache-control"];
|
||||
results.push({
|
||||
surface,
|
||||
header: "cache-control",
|
||||
expected: policy.cacheControl,
|
||||
observed,
|
||||
passed: observed === policy.cacheControl,
|
||||
});
|
||||
const observedContentType = responses[surface]?.["content-type"];
|
||||
const observedMime = observedContentType
|
||||
?.split(";", 1)[0]
|
||||
?.trim()
|
||||
.toLowerCase();
|
||||
results.push({
|
||||
surface,
|
||||
header: "content-type",
|
||||
expected: contentTypes,
|
||||
observed: observedContentType,
|
||||
passed: observedMime !== undefined && contentTypes.includes(observedMime),
|
||||
});
|
||||
if (policy.securityHeaders === true) {
|
||||
for (const [header, expected] of Object.entries(securityHeaders)) {
|
||||
const observedSecurity = responses[surface]?.[header.toLowerCase()];
|
||||
results.push({
|
||||
surface,
|
||||
header: header.toLowerCase(),
|
||||
expected,
|
||||
observed: observedSecurity,
|
||||
passed: observedSecurity === expected,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sourceMapPolicy = recordValue(cacheSurfaces.sourceMap);
|
||||
results.push({
|
||||
surface: "sourceMap",
|
||||
header: "public",
|
||||
expected: false,
|
||||
observed: publicSourceMaps.length > 0,
|
||||
passed: sourceMapPolicy.public === false && publicSourceMaps.length === 0,
|
||||
});
|
||||
const serviceWorkerPolicy = recordValue(cacheSurfaces.serviceWorker);
|
||||
results.push({
|
||||
surface: "serviceWorker",
|
||||
header: "enabled",
|
||||
expected: false,
|
||||
observed: publicServiceWorkers.length > 0,
|
||||
passed:
|
||||
serviceWorkerPolicy.enabled === false && publicServiceWorkers.length === 0,
|
||||
});
|
||||
|
||||
const passed = results.every((result) => result.passed);
|
||||
await mkdir("artifacts/release", { recursive: true });
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/hosting-headers.json",
|
||||
schema: hostingHeadersArtifactSchema,
|
||||
value: {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
mode,
|
||||
baseUrl: liveTarget?.observedOrigin ?? null,
|
||||
providerVerificationRequired: mode !== "live",
|
||||
results,
|
||||
passed,
|
||||
},
|
||||
});
|
||||
|
||||
if (!passed) {
|
||||
process.stderr.write(
|
||||
"Hosting cache/content-type/security header verification failed.\n",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(
|
||||
`Hosting header contract: PASS (${mode}; live verification ${
|
||||
mode === "live" ? "complete" : "required before promotion"
|
||||
})\n`,
|
||||
);
|
||||
Reference in New Issue
Block a user