chore: initialize from frontend template 4dc033c

This commit is contained in:
DongHyeonka
2026-08-13 18:23:26 +09:00
commit 40107eec84
897 changed files with 234824 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
import type { SpawnSyncOptionsWithStringEncoding } from "node:child_process";
export const PNPM_SCRIPT_TIMEOUT_MS = 60_000;
export const PNPM_SCRIPT_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
type BoundedChildInvocation = Readonly<{
command: string;
arguments: readonly string[];
options: SpawnSyncOptionsWithStringEncoding;
}>;
type PnpmScriptResult = Readonly<{
status: number | null;
signal: NodeJS.Signals | null;
error?: Error;
}>;
export function createBoundedPnpmScriptInvocation(input: Readonly<{
nodePath: string;
pnpmCli: string;
script: string;
environment: NodeJS.ProcessEnv;
}>): BoundedChildInvocation {
return createBoundedChildInvocation({
command: input.nodePath,
arguments: [input.pnpmCli, "run", input.script],
environment: input.environment,
});
}
export function createBoundedChildInvocation(input: Readonly<{
command: string;
arguments: readonly string[];
environment: NodeJS.ProcessEnv;
cwd?: string;
}>): BoundedChildInvocation {
return Object.freeze({
command: input.command,
arguments: Object.freeze([...input.arguments]),
options: Object.freeze({
...(input.cwd === undefined ? {} : { cwd: input.cwd }),
encoding: "utf8",
env: input.environment,
killSignal: "SIGTERM",
maxBuffer: PNPM_SCRIPT_MAX_OUTPUT_BYTES,
timeout: PNPM_SCRIPT_TIMEOUT_MS,
}),
});
}
export function formatPnpmScriptFailure(
script: string,
result: PnpmScriptResult,
): string {
const code =
result.error && "code" in result.error &&
typeof result.error.code === "string"
? result.error.code
: null;
const message = result.error?.message.trim().replace(/\s+/g, " ") ?? null;
const error =
result.error === undefined
? "none"
: `${code ?? result.error.name}: ${message || "no message"}`;
return `${script} failed: exit=${String(result.status)}, signal=${result.signal ?? "none"}, error=${error}`;
}
+96
View File
@@ -0,0 +1,96 @@
export const CI_BUILD_ENVIRONMENT_VARIABLES = Object.freeze([
"VITE_BUILD_ID",
"VITE_COMMIT_SHA",
"RELEASE_ID",
"CI_RUNNER_IMAGE",
"SOURCE_DATE_EPOCH",
]);
export function ciBuildEnvironmentFailures(
environment: Readonly<Record<string, string | undefined>>,
) {
if (environment.CI !== "true") return [];
const failures = CI_BUILD_ENVIRONMENT_VARIABLES.filter(
(name) => !environment[name]?.trim(),
).map((name) => `missing required CI build environment: ${name}`);
const commitSha = environment.VITE_COMMIT_SHA?.trim();
if (commitSha && !isValidCommitSha(commitSha)) {
failures.push(
"VITE_COMMIT_SHA must be a full 40- or 64-character hexadecimal commit ID",
);
}
const sourceDateEpoch = environment.SOURCE_DATE_EPOCH?.trim();
if (sourceDateEpoch && !isValidSourceDateEpoch(sourceDateEpoch)) {
failures.push("SOURCE_DATE_EPOCH must be non-negative epoch seconds");
}
const runnerImage = environment.CI_RUNNER_IMAGE?.trim();
if (
runnerImage &&
!/@sha256:[0-9a-f]{64}$/i.test(runnerImage)
) {
failures.push(
"CI_RUNNER_IMAGE must end with an immutable @sha256 image digest",
);
}
return failures;
}
export function assertCiBuildEnvironment(
environment: Readonly<Record<string, string | undefined>>,
) {
const failures = ciBuildEnvironmentFailures(environment);
if (failures.length > 0) {
throw new Error(failures.join("; "));
}
}
export function isValidCommitSha(value: string) {
return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value);
}
export function isValidSourceDateEpoch(value: string) {
if (!/^\d+$/.test(value)) return false;
const milliseconds = Number(value) * 1_000;
return Number.isSafeInteger(milliseconds) && Number.isFinite(
new Date(milliseconds).getTime(),
);
}
export function ciCheckoutIdentityFailures(
environment: Readonly<Record<string, string | undefined>>,
checkout: { commitSha: string; sourceDateEpoch: string },
) {
if (environment.CI !== "true") return [];
const failures = [];
const configuredCommitSha = environment.VITE_COMMIT_SHA?.trim();
if (
configuredCommitSha &&
configuredCommitSha.toLowerCase() !== checkout.commitSha.toLowerCase()
) {
failures.push("VITE_COMMIT_SHA does not identify the checked-out commit");
}
const configuredEpoch = environment.SOURCE_DATE_EPOCH?.trim();
if (configuredEpoch && configuredEpoch !== checkout.sourceDateEpoch) {
failures.push(
"SOURCE_DATE_EPOCH does not match the checked-out commit timestamp",
);
}
return failures;
}
export function buildDate(
environment: Readonly<Record<string, string | undefined>>,
) {
const sourceDateEpoch = environment.SOURCE_DATE_EPOCH?.trim();
if (!sourceDateEpoch) return new Date();
if (!isValidSourceDateEpoch(sourceDateEpoch)) {
throw new Error("SOURCE_DATE_EPOCH must be non-negative epoch seconds");
}
return new Date(Number(sourceDateEpoch) * 1_000);
}
+188
View File
@@ -0,0 +1,188 @@
import { createHash } from "node:crypto";
import { lstat, readFile, realpath } from "node:fs/promises";
import path from "node:path";
import type { BuildManifestArtifact } from "../../src/contracts/release-artifacts.ts";
import { moduleInventoryArtifactSchema } from "../contracts/release-artifacts.ts";
type VerifyBuildManifestOutputsDependencies = Readonly<{
repositoryRoot?: string;
readBytes?: (target: string) => Promise<Buffer>;
realpathPath?: (target: string) => Promise<string>;
assertRegularFile?: (target: string) => Promise<void>;
assertDirectory?: (target: string) => Promise<void>;
}>;
export const CANONICAL_VITE_MANIFEST_PATH = "dist/.vite/manifest.json";
function isSafeRelativePath(value: string): boolean {
return (
value.length > 0 &&
!path.posix.isAbsolute(value) &&
!path.win32.isAbsolute(value) &&
!value.includes("\\") &&
!value.includes("\0") &&
path.posix.normalize(value) === value &&
value !== ".." &&
!value.startsWith("../")
);
}
function isWithinRoot(root: string, target: string): boolean {
const relative = path.relative(root, target);
return (
relative === "" ||
(relative !== ".." &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative))
);
}
function belongsToApprovedRoot(value: string, approvedRoot: string): boolean {
return value.startsWith(`${approvedRoot}/`);
}
async function defaultAssertRegularFile(target: string): Promise<void> {
const metadata = await lstat(target);
if (!metadata.isFile() || metadata.isSymbolicLink()) {
throw new TypeError("not a regular file");
}
}
async function defaultAssertDirectory(target: string): Promise<void> {
const metadata = await lstat(target);
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
throw new TypeError("not a directory");
}
}
export async function verifyBuildManifestOutputs(
manifest: BuildManifestArtifact,
dependencies: VerifyBuildManifestOutputsDependencies = {},
): Promise<string[]> {
const repositoryRoot = path.resolve(dependencies.repositoryRoot ?? process.cwd());
const readBytes = dependencies.readBytes ?? readFile;
const realpathPath = dependencies.realpathPath ?? realpath;
const assertRegularFile = dependencies.assertRegularFile ?? defaultAssertRegularFile;
const assertDirectory = dependencies.assertDirectory ?? defaultAssertDirectory;
const mismatches: string[] = [];
const resolvedRoot = await realpathPath(repositoryRoot);
const approvedRoots = new Map<string, Promise<string | null>>();
function resolveApprovedRoot(relativeRoot: string): Promise<string | null> {
const existing = approvedRoots.get(relativeRoot);
if (existing) return existing;
const pending = (async () => {
const absoluteRoot = path.resolve(repositoryRoot, relativeRoot);
try {
await assertDirectory(absoluteRoot);
const resolvedApprovedRoot = await realpathPath(absoluteRoot);
return isWithinRoot(resolvedRoot, resolvedApprovedRoot)
? resolvedApprovedRoot
: null;
} catch {
return null;
}
})();
approvedRoots.set(relativeRoot, pending);
return pending;
}
async function confinedPath(
label: string,
relativePath: string,
kind: "file" | "directory",
approvedRoot?: string,
): Promise<string | null> {
if (
!isSafeRelativePath(relativePath) ||
(approvedRoot !== undefined &&
!belongsToApprovedRoot(relativePath, approvedRoot))
) {
mismatches.push(`buildManifest:${label}:path`);
return null;
}
const absolutePath = path.resolve(repositoryRoot, relativePath);
if (!isWithinRoot(repositoryRoot, absolutePath)) {
mismatches.push(`buildManifest:${label}:path`);
return null;
}
try {
if (kind === "file") await assertRegularFile(absolutePath);
else await assertDirectory(absolutePath);
const resolvedPath = await realpathPath(absolutePath);
const resolvedApprovedRoot = approvedRoot
? await resolveApprovedRoot(approvedRoot)
: resolvedRoot;
if (
resolvedApprovedRoot === null ||
!isWithinRoot(resolvedRoot, resolvedPath) ||
!isWithinRoot(resolvedApprovedRoot, resolvedPath)
) {
mismatches.push(`buildManifest:${label}:path`);
return null;
}
return absolutePath;
} catch {
mismatches.push(`buildManifest:${label}:missing`);
return null;
}
}
if (manifest.outputs.directory !== "dist") {
mismatches.push("buildManifest:directory:path");
} else {
await confinedPath("directory", manifest.outputs.directory, "directory");
}
if (manifest.outputs.viteManifest !== CANONICAL_VITE_MANIFEST_PATH) {
mismatches.push("buildManifest:viteManifest:path");
} else {
await confinedPath(
"viteManifest",
manifest.outputs.viteManifest,
"file",
"dist",
);
}
await confinedPath(
"runtimeConfigSchema",
manifest.outputs.runtimeConfigSchema,
"file",
"dist",
);
for (const [chunkId, chunkPath] of Object.entries(manifest.outputs.routeChunks)) {
if (!isSafeRelativePath(chunkPath)) {
mismatches.push(`buildManifest:routeChunk:${chunkId}:path`);
continue;
}
await confinedPath(
`routeChunk:${chunkId}`,
path.posix.join(manifest.outputs.directory, chunkPath),
"file",
"dist",
);
}
const moduleInventoryPath = await confinedPath(
"moduleInventory",
manifest.outputs.moduleInventory,
"file",
"artifacts/quality",
);
if (moduleInventoryPath) {
try {
const bytes = await readBytes(moduleInventoryPath);
const digest = createHash("sha256").update(bytes).digest("hex");
if (digest !== manifest.moduleInventoryHash) {
mismatches.push("buildManifest:moduleInventoryHash");
}
try {
moduleInventoryArtifactSchema.parse(JSON.parse(bytes.toString("utf8")));
} catch {
mismatches.push("buildManifest:moduleInventory:invalid");
}
} catch {
mismatches.push("buildManifest:moduleInventory:missing");
}
}
return mismatches;
}
+740
View File
@@ -0,0 +1,740 @@
import { constants, type Stats } from "node:fs";
import { lstat, open, realpath } from "node:fs/promises";
import path from "node:path";
import { z, type ZodType } from "zod";
import type {
CiGateArtifact,
CiGateArtifactSchema,
} from "../contracts/ci-gates.ts";
import { ciContractReportSchema } from "./ci-contract-report.ts";
import {
architectureDependencyReportArtifactSchema,
automatedA11yArtifactSchema,
buildManifestArtifactSchema,
bundlePerformanceArtifactSchema,
compatibilityFixturesArtifactSchema,
dependencyDiffArtifactSchema,
dependencyInventoryArtifactSchema,
designSystemReportArtifactSchema,
diagnosticsReportArtifactSchema,
documentationReviewArtifactSchema,
fieldWebVitalsArtifactSchema,
hostingHeadersArtifactSchema,
i18nReportArtifactSchema,
jsonSchemaDocumentArtifactSchema,
labPerformanceArtifactSchema,
licenseReportArtifactSchema,
manualA11yReportArtifactSchema,
moduleInventoryArtifactSchema,
optionalRecipeFixturesArtifactSchema,
optionalRecipesArtifactSchema,
provenanceArtifactSchema,
realtimeBoundariesArtifactSchema,
registryGovernanceRunArtifactSchema,
registryCompatibilityFixturesArtifactSchema,
registrySnapshotArtifactSchema,
releaseVerificationArtifactSchema,
reproducibleBuildArtifactSchema,
runbookRecordArtifactSchema,
sbomArtifactSchema,
supplyChainFixturesArtifactSchema,
supplyChainProviderFixturesArtifactSchema,
supplyChainVerificationArtifactSchema,
vulnerabilityReportArtifactSchema,
} from "../contracts/release-artifacts.ts";
import { httpScenarioReceiptSchema } from "./http-scenario-evidence.ts";
import { supplyChainCoherenceReportSchema } from "./local-release-evidence.ts";
import {
providerVerificationArtifactSchema,
provenanceProviderAttestationSchema,
vulnerabilityProviderReportSchema,
} from "./provider-evidence.ts";
import { releaseCandidateManifestSchema } from "./release-candidate.ts";
import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts";
import { secretScanSarifSchema } from "./secret-scan-evaluator.ts";
import { testEvidenceReportSchema } from "./test-evidence-artifact.ts";
const coverageCounterSchema = z
.object({
total: z.number().int().nonnegative(),
covered: z.number().int().nonnegative(),
skipped: z.number().int().nonnegative(),
pct: z.number().min(0).max(100),
})
.strict()
.superRefine((counter, context) => {
if (counter.covered + counter.skipped > counter.total) {
context.addIssue({ code: "custom", message: "coverage counter exceeds total" });
}
const expected = counter.total === 0
? 100
: Math.floor((counter.covered / counter.total) * 10_000) / 100;
if (counter.pct !== expected) {
context.addIssue({ code: "custom", path: ["pct"], message: "coverage pct is not exact" });
}
});
const coverageSummarySchema = z
.record(
z.string(),
z
.object({
lines: coverageCounterSchema,
statements: coverageCounterSchema,
functions: coverageCounterSchema,
branches: coverageCounterSchema,
})
.strict(),
)
.refine((value) => "total" in value, "coverage summary lacks total");
const riskCoverageArtifactSchema = z
.object({
schemaVersion: z.literal(3),
policy: z.string().min(1),
summary: z.string().min(1),
status: z.enum(["PASS", "FAIL"]),
selectedTotal: z.number().int().nonnegative(),
repositoryTotal: z.number().int().positive(),
counterBearingTotal: z.number().int().nonnegative(),
instrumentedCounterBearingTotal: z.number().int().nonnegative(),
counterlessTotal: z.number().int().nonnegative(),
counterlessModules: z.array(z.string()),
preExclusionTotal: z.number().int().positive(),
generatedExclusionCount: z.number().int().nonnegative(),
generatedExclusions: z.array(z.string()),
ownershipScope: z.literal("ALL_POLICY_HIGH_RISK"),
ownedHighRiskPaths: z.array(z.string()),
waivedHighRiskPaths: z.array(z.string()),
uncoveredModules: z.array(z.string()),
results: z
.array(
z
.object({
scope: z.string().min(1),
metric: z.enum(["lines", "statements", "functions", "branches"]),
threshold: z.number().min(0).max(100),
received: z.number().min(0).max(100),
passed: z.boolean(),
})
.strict(),
)
.min(4),
failures: z.array(z.string()),
})
.strict()
.superRefine((artifact, context) => {
const fail = (path: PropertyKey[], message: string) =>
context.addIssue({ code: "custom", path, message });
if (artifact.counterBearingTotal + artifact.counterlessTotal !== artifact.repositoryTotal) {
fail(["counterBearingTotal"], "counter partition must equal repositoryTotal");
}
if (artifact.instrumentedCounterBearingTotal > artifact.counterBearingTotal) {
fail(["instrumentedCounterBearingTotal"], "instrumented counters exceed counter-bearing total");
}
if (artifact.counterlessModules.length !== artifact.counterlessTotal) {
fail(["counterlessModules"], "counterless list length drift");
}
if (artifact.generatedExclusions.length !== artifact.generatedExclusionCount) {
fail(["generatedExclusions"], "generated exclusion list length drift");
}
if (
artifact.preExclusionTotal !==
artifact.repositoryTotal + artifact.generatedExclusionCount
) {
fail(["preExclusionTotal"], "pre-exclusion inventory total drift");
}
if (
artifact.selectedTotal > artifact.repositoryTotal ||
artifact.uncoveredModules.length !== artifact.repositoryTotal - artifact.selectedTotal
) {
fail(["selectedTotal"], "selected/uncovered repository totals drift");
}
if (
(artifact.status === "PASS") !==
(artifact.failures.length === 0 && artifact.results.every(({ passed }) => passed))
) {
fail(["status"], "status must agree with failures and threshold results");
}
artifact.results.forEach((result, index) => {
if (result.passed !== (result.received >= result.threshold)) {
fail(["results", index, "passed"], "threshold result is inconsistent");
}
});
for (const [field, values] of [
["counterlessModules", artifact.counterlessModules],
["generatedExclusions", artifact.generatedExclusions],
["ownedHighRiskPaths", artifact.ownedHighRiskPaths],
["waivedHighRiskPaths", artifact.waivedHighRiskPaths],
["uncoveredModules", artifact.uncoveredModules],
] as const) {
if (new Set(values).size !== values.length) fail([field], "path list contains duplicates");
}
const owned = new Set(artifact.ownedHighRiskPaths);
if (artifact.waivedHighRiskPaths.some((modulePath) => owned.has(modulePath))) {
fail(["waivedHighRiskPaths"], "owned and waived high-risk paths overlap");
}
const resultsByScope = new Map<string, Set<string>>();
artifact.results.forEach(({ scope, metric }, index) => {
const metrics = resultsByScope.get(scope) ?? new Set<string>();
if (metrics.has(metric)) {
fail(["results", index, "metric"], "threshold metric is duplicated within scope");
}
metrics.add(metric);
resultsByScope.set(scope, metrics);
});
for (const [scope, metrics] of resultsByScope) {
if (metrics.size !== 4) {
fail(["results"], `threshold scope must contain all four metrics: ${scope}`);
}
}
});
type ExecutableJsonSchemaId = Extract<
CiGateArtifactSchema,
Readonly<{ kind: "json" }>
>["executableSchemaId"];
const executableJsonSchemas: Readonly<Record<ExecutableJsonSchemaId, ZodType>> = Object.freeze({
"automated-a11y": automatedA11yArtifactSchema,
"manual-a11y": manualA11yReportArtifactSchema,
"architecture-dependency-report": architectureDependencyReportArtifactSchema,
"design-system-contract": designSystemReportArtifactSchema,
"i18n-contract": i18nReportArtifactSchema,
"diagnostics-contract": diagnosticsReportArtifactSchema,
"realtime-boundaries": realtimeBoundariesArtifactSchema,
"optional-recipes": optionalRecipesArtifactSchema,
"optional-recipe-fixtures": optionalRecipeFixturesArtifactSchema,
"registry-compatibility-fixtures": registryCompatibilityFixturesArtifactSchema,
"reproducible-build": reproducibleBuildArtifactSchema,
"supply-chain-fixtures": supplyChainFixturesArtifactSchema,
"supply-chain-provider-fixtures": supplyChainProviderFixturesArtifactSchema,
"compatibility-fixtures": compatibilityFixturesArtifactSchema,
"documentation-review": documentationReviewArtifactSchema,
"hosting-headers": hostingHeadersArtifactSchema,
"coverage-summary-v8": coverageSummarySchema,
"risk-coverage-v3": riskCoverageArtifactSchema,
"build-manifest": buildManifestArtifactSchema,
"module-inventory": moduleInventoryArtifactSchema,
"dependency-inventory": dependencyInventoryArtifactSchema,
"registry-snapshot": registrySnapshotArtifactSchema,
"registry-governance-run": registryGovernanceRunArtifactSchema,
"bundle-performance": bundlePerformanceArtifactSchema,
sbom: sbomArtifactSchema,
provenance: provenanceArtifactSchema,
"dependency-diff": dependencyDiffArtifactSchema,
"license-report": licenseReportArtifactSchema,
"vulnerability-report": vulnerabilityReportArtifactSchema,
"field-web-vitals": fieldWebVitalsArtifactSchema,
"lab-performance": labPerformanceArtifactSchema,
"release-verification": releaseVerificationArtifactSchema,
"runbook-record": runbookRecordArtifactSchema,
"supply-chain-verification": supplyChainVerificationArtifactSchema,
"release-candidate": releaseCandidateManifestSchema,
"supply-chain-coherence": supplyChainCoherenceReportSchema,
"http-scenario-receipt": httpScenarioReceiptSchema,
"test-evidence-report": testEvidenceReportSchema,
"provider-vulnerability": vulnerabilityProviderReportSchema,
"provider-provenance": provenanceProviderAttestationSchema,
"provider-verification": providerVerificationArtifactSchema,
"ci-contract-report": ciContractReportSchema,
});
export function hasCiArtifactSemanticValidator(
schema: CiGateArtifactSchema,
): boolean {
return schema.kind !== "json" || schema.executableSchemaId in executableJsonSchemas;
}
type ReadHandle = Readonly<{
stat(): Promise<Stats>;
read(
buffer: Buffer,
offset: number,
length: number,
position: number,
): Promise<Readonly<{ bytesRead: number }>>;
close(): Promise<unknown>;
}>;
type ValidatorDependencies = Readonly<{
lstatPath?: typeof lstat;
realpathPath?: typeof realpath;
openFile?: (target: string, flags: number) => Promise<ReadHandle>;
}>;
export async function validateCiArtifact(
input: Readonly<{
root: string;
artifact: CiGateArtifact;
schema: CiGateArtifactSchema;
}>,
dependencies: ValidatorDependencies = {},
): Promise<void> {
const relative = normalizeRepositoryRelativePath(input.artifact.path, "CI artifact path");
assertExtensionCoherence(relative, input.schema.kind);
const bytes = await readBoundedRegularFile(
{ root: input.root, relativePath: relative, maxBytes: input.schema.maxBytes },
dependencies,
);
if (input.schema.kind === "candidate-archive") return;
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
if (!text.trim()) throw new TypeError(`CI artifact is empty: ${relative}`);
switch (input.schema.kind) {
case "text":
return;
case "markdown":
if (!/^#|\[[^\]]+\]|\S/u.test(text)) throw new TypeError(`invalid Markdown artifact: ${relative}`);
return;
case "html":
assertWellFormedHtml(text, relative);
return;
case "junit":
assertWellFormedJUnitXml(text, relative);
return;
case "sarif":
secretScanSarifSchema.parse(JSON.parse(text) as unknown);
return;
case "json-schema":
jsonSchemaDocumentArtifactSchema.parse(JSON.parse(text) as unknown);
return;
case "json": {
const schema = executableJsonSchemas[input.schema.executableSchemaId];
if (!schema) throw new TypeError(`unknown executable artifact schema: ${input.schema.executableSchemaId}`);
schema.parse(JSON.parse(text) as unknown);
return;
}
}
}
export async function readBoundedRegularFile(
input: Readonly<{ root: string; relativePath: string; maxBytes: number }>,
dependencies: ValidatorDependencies = {},
): Promise<Buffer> {
const root = path.resolve(input.root);
const relative = normalizeRepositoryRelativePath(input.relativePath, "bounded file path");
const maxBytes = input.maxBytes;
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 268_435_456) {
throw new RangeError("bounded file maximum must be within 1..268435456");
}
const lstatPath = dependencies.lstatPath ?? lstat;
const realpathPath = dependencies.realpathPath ?? realpath;
const openFile = dependencies.openFile ?? (async (target, flags) => open(target, flags));
const rootMetadata = await lstatPath(root);
if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) {
throw new TypeError("bounded file root is unsafe");
}
const rootRealpath = await realpathPath(root);
let ancestor = root;
const segments = relative.split("/");
for (const segment of segments.slice(0, -1)) {
ancestor = path.join(ancestor, segment);
const metadata = await lstatPath(ancestor);
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
throw new TypeError(`CI artifact ancestor is unsafe: ${relative}`);
}
}
const absolute = path.join(root, relative);
const before = await lstatPath(absolute);
if (before.isSymbolicLink() || !before.isFile()) {
throw new TypeError(`CI artifact is not a regular file: ${relative}`);
}
if (before.size <= 0 || before.size > maxBytes) {
throw new RangeError(`CI artifact size is outside 1..${maxBytes}: ${relative}`);
}
const resolved = await realpathPath(absolute);
const outside = path.relative(rootRealpath, resolved);
if (outside === ".." || outside.startsWith(`..${path.sep}`) || path.isAbsolute(outside)) {
throw new TypeError(`CI artifact escapes repository: ${relative}`);
}
const handle = await openFile(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
const opened = await handle.stat();
assertSameIdentity(before, opened, relative);
const bytes = await readHandleBounded(handle, before.size, maxBytes, relative);
const after = await handle.stat();
assertSameIdentity(opened, after, relative);
if (bytes.byteLength <= 0 || bytes.byteLength > maxBytes || after.size !== bytes.byteLength) {
throw new RangeError(`CI artifact changed size or exceeds bound: ${relative}`);
}
return bytes;
} finally {
await handle.close();
}
}
async function readHandleBounded(
handle: ReadHandle,
expectedSize: number,
maxBytes: number,
relative: string,
): Promise<Buffer> {
const captured = Buffer.allocUnsafe(Math.min(maxBytes + 1, expectedSize + 1));
let offset = 0;
while (offset < captured.byteLength) {
const { bytesRead } = await handle.read(
captured,
offset,
captured.byteLength - offset,
offset,
);
if (bytesRead === 0) break;
offset += bytesRead;
}
if (offset !== expectedSize) {
throw new RangeError(`CI artifact changed size or exceeds bound: ${relative}`);
}
return captured.subarray(0, offset);
}
const MAX_DOCUMENT_DEPTH = 256;
const MAX_DOCUMENT_UNITS = 100_000;
function assertWellFormedJUnitXml(source: string, relative: string): void {
const invalid = () => new TypeError(`invalid JUnit artifact: ${relative}`);
if (/<!DOCTYPE\b|<!ENTITY\b/iu.test(source)) throw invalid();
if (!hasOnlyXmlCharacters(source)) throw invalid();
const stack: string[] = [];
let root: string | undefined;
let rootClosed = false;
let declarationSeen = false;
let units = 0;
let cursor = 0;
while (cursor < source.length) {
const open = source.indexOf("<", cursor);
const text = source.slice(cursor, open < 0 ? source.length : open);
if (stack.length === 0 && text.trim()) throw invalid();
if ((text.includes("]]>") || !hasValidXmlEntities(text)) && text.length > 0) throw invalid();
if (text.length > 0 && ++units > MAX_DOCUMENT_UNITS) throw invalid();
if (open < 0) break;
if (source.startsWith("<!--", open)) {
const close = source.indexOf("-->", open + 4);
if (close < 0 || source.slice(open + 4, close).includes("--")) throw invalid();
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 3;
continue;
}
if (source.startsWith("<![CDATA[", open)) {
const close = source.indexOf("]]>", open + 9);
if (stack.length === 0 || close < 0) throw invalid();
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 3;
continue;
}
if (source.startsWith("<?", open)) {
const close = source.indexOf("?>", open + 2);
if (root || close < 0) throw invalid();
const processingInstruction = source.slice(open, close + 2);
const match = /^<\?([A-Za-z_][\w:.-]*)(?:\s+[\s\S]*?)?\?>$/u.exec(
processingInstruction,
);
if (!match) throw invalid();
if (match[1]!.toLowerCase() === "xml") {
if (
declarationSeen ||
source.slice(0, open).trim() ||
!/^<\?xml\s+version\s*=\s*(["'])1\.0\1(?:\s+encoding\s*=\s*(["'])UTF-8\2)?\s*\?>$/u.test(
processingInstruction,
)
) {
throw invalid();
}
declarationSeen = true;
}
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 2;
continue;
}
if (source.startsWith("<!", open)) throw invalid();
const close = markupEnd(source, open + 1);
if (close < 0) throw invalid();
const tag = source.slice(open, close + 1);
const closing = /^<\/([A-Za-z_][\w:.-]*)\s*>$/u.exec(tag);
if (closing) {
if (stack.pop() !== closing[1]) throw invalid();
if (stack.length === 0) rootClosed = true;
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 1;
continue;
}
const opening = /^<([A-Za-z_][\w:.-]*)([\s\S]*?)(\/?)>$/u.exec(tag);
if (!opening || rootClosed || !hasValidXmlAttributes(opening[2] ?? "")) throw invalid();
root ??= opening[1];
if (opening[3] !== "/") {
if (stack.length >= MAX_DOCUMENT_DEPTH) throw invalid();
stack.push(opening[1]!);
}
else if (stack.length === 0) rootClosed = true;
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 1;
}
if (stack.length > 0 || !rootClosed || (root !== "testsuite" && root !== "testsuites")) {
throw invalid();
}
}
function hasValidXmlAttributes(source: string): boolean {
let remaining = source;
const names = new Set<string>();
while (remaining.length > 0) {
if (!remaining.trim()) return true;
const match = /^\s+([A-Za-z_:][\w:.-]*)\s*=\s*(?:"([^"<]*)"|'([^'<]*)')/u.exec(remaining);
if (!match || names.has(match[1]!)) return false;
if (!hasValidXmlEntities(match[2] ?? match[3] ?? "")) return false;
names.add(match[1]!);
remaining = remaining.slice(match[0].length);
}
return true;
}
function hasOnlyXmlCharacters(source: string): boolean {
for (const character of source) {
const codePoint = character.codePointAt(0)!;
if (
codePoint !== 0x09 &&
codePoint !== 0x0a &&
codePoint !== 0x0d &&
(codePoint < 0x20 ||
(codePoint > 0xd7ff && codePoint < 0xe000) ||
(codePoint > 0xfffd && codePoint < 0x10000) ||
codePoint > 0x10ffff)
) {
return false;
}
}
return true;
}
function hasValidXmlEntities(source: string): boolean {
let cursor = 0;
while (cursor < source.length) {
const ampersand = source.indexOf("&", cursor);
if (ampersand < 0) return true;
const semicolon = source.indexOf(";", ampersand + 1);
if (semicolon < 0) return false;
const entity = source.slice(ampersand + 1, semicolon);
if (!["amp", "lt", "gt", "apos", "quot"].includes(entity)) {
const decimal = /^#([0-9]+)$/u.exec(entity);
const hexadecimal = /^#x([a-fA-F0-9]+)$/u.exec(entity);
if (!decimal && !hexadecimal) return false;
const codePoint = Number.parseInt((decimal ?? hexadecimal)![1]!, decimal ? 10 : 16);
if (
!Number.isSafeInteger(codePoint) ||
(codePoint !== 0x09 &&
codePoint !== 0x0a &&
codePoint !== 0x0d &&
(codePoint < 0x20 ||
(codePoint > 0xd7ff && codePoint < 0xe000) ||
(codePoint > 0xfffd && codePoint < 0x10000) ||
codePoint > 0x10ffff))
) {
return false;
}
}
cursor = semicolon + 1;
}
return true;
}
const HTML_VOID_ELEMENTS = new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
]);
const HTML_RAW_TEXT_ELEMENTS = new Set(["script", "style", "textarea", "title"]);
function assertWellFormedHtml(source: string, relative: string): void {
const invalid = () => new TypeError(`invalid HTML artifact: ${relative}`);
if (/<!ENTITY\b|<!DOCTYPE\s+html\s+[^>]*\[/iu.test(source)) throw invalid();
const stack: string[] = [];
let cursor = 0;
let units = 0;
let doctypeSeen = false;
let rootSeen = false;
let rootClosed = false;
let playwrightPayloadSeen = false;
while (cursor < source.length) {
const rawElement = stack.at(-1);
if (rawElement && HTML_RAW_TEXT_ELEMENTS.has(rawElement)) {
const closingStart = source.toLowerCase().indexOf(`</${rawElement}`, cursor);
if (closingStart < 0) throw invalid();
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = closingStart;
}
const open = source.indexOf("<", cursor);
const text = source.slice(cursor, open < 0 ? source.length : open);
if (stack.length === 0 && text.trim()) throw invalid();
if (text.length > 0 && ++units > MAX_DOCUMENT_UNITS) throw invalid();
if (open < 0) break;
if (source.startsWith("<!--", open)) {
const close = source.indexOf("-->", open + 4);
if (close < 0 || source.slice(open + 4, close).includes("--")) throw invalid();
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 3;
continue;
}
const declarationEnd = source.indexOf(">", open + 2);
if (source.slice(open, open + 9).toLowerCase() === "<!doctype") {
if (
declarationEnd < 0 ||
doctypeSeen ||
rootSeen ||
source.slice(open, declarationEnd + 1).toLowerCase() !== "<!doctype html>"
) {
throw invalid();
}
doctypeSeen = true;
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = declarationEnd + 1;
continue;
}
if (source.startsWith("<!", open) || source.startsWith("<?", open)) throw invalid();
const close = markupEnd(source, open + 1);
if (close < 0) throw invalid();
const tag = source.slice(open, close + 1);
const closing = /^<\/([A-Za-z][A-Za-z0-9:-]*)\s*>$/u.exec(tag);
if (closing) {
const name = closing[1]!.toLowerCase();
if (stack.pop() !== name) throw invalid();
if (stack.length === 0) {
if (name === "html") rootClosed = true;
else if (name === "template" && playwrightPayloadSeen) {
// Playwright emits its base64 report template after </html>; HTML5
// reparents this token into the document body. It is the sole
// permitted generated-report sidecar and does not create a new root.
} else {
throw invalid();
}
}
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 1;
continue;
}
const opening = /^<([A-Za-z][A-Za-z0-9:-]*)([\s\S]*?)(\/?)>$/u.exec(tag);
if (!opening) throw invalid();
const name = opening[1]!.toLowerCase();
const attributes = parseHtmlAttributes(opening[2] ?? "");
if (!attributes || name.includes(":")) throw invalid();
if (!rootSeen) {
if (name !== "html" || opening[3] === "/") throw invalid();
rootSeen = true;
} else if (name === "html") {
throw invalid();
}
if (rootClosed && stack.length === 0) {
if (
playwrightPayloadSeen ||
name !== "template" ||
attributes.get("id") !== "playwrightReportBase64" ||
opening[3] === "/"
) {
throw invalid();
}
playwrightPayloadSeen = true;
}
if (!HTML_VOID_ELEMENTS.has(name) && opening[3] !== "/") {
if (stack.length >= MAX_DOCUMENT_DEPTH) throw invalid();
stack.push(name);
} else if (stack.length === 0 && name === "html") {
rootClosed = true;
}
if (++units > MAX_DOCUMENT_UNITS) throw invalid();
cursor = close + 1;
}
if (stack.length > 0 || !rootSeen || !rootClosed) throw invalid();
}
function markupEnd(source: string, start: number): number {
let quote: "\"" | "'" | undefined;
for (let index = start; index < source.length; index += 1) {
const character = source[index];
if (quote) {
if (character === quote) quote = undefined;
} else if (character === "\"" || character === "'") {
quote = character;
} else if (character === ">") {
return index;
}
}
return -1;
}
function parseHtmlAttributes(source: string): ReadonlyMap<string, string> | null {
const attributes = new Map<string, string>();
let cursor = 0;
while (cursor < source.length) {
const whitespace = /^\s+/u.exec(source.slice(cursor));
if (!whitespace) return source.slice(cursor).trim() ? null : attributes;
cursor += whitespace[0].length;
if (cursor >= source.length) return attributes;
const nameMatch = /^[A-Za-z_:][A-Za-z0-9:._-]*/u.exec(source.slice(cursor));
if (!nameMatch) return null;
const name = nameMatch[0].toLowerCase();
if (attributes.has(name) || name.includes(":")) return null;
cursor += nameMatch[0].length;
const spacing = /^\s*/u.exec(source.slice(cursor))![0];
cursor += spacing.length;
let value = "";
if (source[cursor] === "=") {
cursor += 1;
cursor += /^\s*/u.exec(source.slice(cursor))![0].length;
const quote = source[cursor];
if (quote === "\"" || quote === "'") {
const end = source.indexOf(quote, cursor + 1);
if (end < 0) return null;
value = source.slice(cursor + 1, end);
if (value.includes("<")) return null;
cursor = end + 1;
} else {
const unquoted = /^[^\s"'`=<>]+/u.exec(source.slice(cursor));
if (!unquoted) return null;
value = unquoted[0];
cursor += value.length;
}
}
attributes.set(name, value);
}
return attributes;
}
function assertSameIdentity(before: Stats, after: Stats, relative: string): void {
if (
!Number.isSafeInteger(before.dev) ||
!Number.isSafeInteger(before.ino) ||
before.dev <= 0 ||
before.ino <= 0 ||
before.dev !== after.dev ||
before.ino !== after.ino ||
!after.isFile()
) {
throw new TypeError(`CI artifact file identity changed: ${relative}`);
}
}
function assertExtensionCoherence(relative: string, kind: CiGateArtifactSchema["kind"]): void {
const valid =
kind === "json" || kind === "json-schema"
? relative.endsWith(".json")
: kind === "sarif"
? relative.endsWith(".sarif")
: kind === "junit"
? relative.endsWith(".xml")
: kind === "html"
? relative.endsWith(".html")
: kind === "markdown"
? relative.endsWith(".md")
: kind === "candidate-archive"
? relative.endsWith(".tar.gz")
: !/\.(?:json|sarif|xml|html|md|tar\.gz)$/u.test(relative);
if (!valid) throw new TypeError(`CI artifact extension/kind mismatch: ${relative} (${kind})`);
}
+26
View File
@@ -0,0 +1,26 @@
export const CANDIDATE_ARCHIVE_USAGE =
"Usage: verify-ci-candidate-archive --archive <path> [--extract-to <path>] [--github-output <path>]\n";
export function parseCandidateArchiveArguments(arguments_: readonly string[]): Readonly<{
archivePath: string;
extractTo?: string;
githubOutput?: string;
}> | null {
const allowed = new Set(["--archive", "--extract-to", "--github-output"]);
const values = new Map<string, string>();
for (let index = 0; index < arguments_.length; index += 2) {
const flag = arguments_[index];
const value = arguments_[index + 1];
if (!flag || !allowed.has(flag) || values.has(flag) || !value || value.startsWith("--")) {
return null;
}
values.set(flag, value);
}
const archivePath = values.get("--archive");
if (!archivePath) return null;
return Object.freeze({
archivePath,
...(values.has("--extract-to") ? { extractTo: values.get("--extract-to")! } : {}),
...(values.has("--github-output") ? { githubOutput: values.get("--github-output")! } : {}),
});
}
+663
View File
@@ -0,0 +1,663 @@
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { constants } from "node:fs";
import type { FileHandle } from "node:fs/promises";
import {
lstat,
mkdir,
mkdtemp,
open,
readFile,
readdir,
rename,
rm,
unlink,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import {
RELEASE_CANDIDATE_EVIDENCE_PATHS,
RELEASE_CANDIDATE_MANIFEST_PATH,
distSha256,
releaseCandidateManifestSchema,
type ReleaseCandidateManifest,
} from "./release-candidate.ts";
import { supplyChainDigest } from "./supply-chain.ts";
import {
assertSafePublishLeaf,
ensureSafePublishDirectory,
} from "./ci-gate-log.ts";
const MAX_ARCHIVE_BYTES = 268_435_456;
const MAX_CANDIDATE_FILES = 4_096;
const MAX_ARCHIVE_MEMBERS = 8_192;
const MAX_MEMBER_PATH_BYTES = 1_024;
const TAR_EXECUTABLE = "/usr/bin/tar";
const TAR_ENVIRONMENT = Object.freeze({ PATH: "/usr/bin:/bin", LC_ALL: "C", LANG: "C" });
export type CapturedCandidateArchive = Readonly<{
bytes: Buffer;
archiveSha256: string;
}>;
export async function captureCiCandidateArchive(input: Readonly<{
archivePath: string;
expectedSha256: string;
}>): Promise<CapturedCandidateArchive> {
if (!/^[a-f0-9]{64}$/u.test(input.expectedSha256)) {
throw new TypeError("expected candidate archive SHA-256 is invalid");
}
const absolute = path.resolve(input.archivePath);
const before = await lstat(absolute);
if (!before.isFile() || before.isSymbolicLink()) {
throw new TypeError("candidate archive must be a regular non-symlink file");
}
if (before.size <= 0 || before.size > MAX_ARCHIVE_BYTES) {
throw new RangeError(`candidate archive size is outside 1..${MAX_ARCHIVE_BYTES}`);
}
const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
let bytes: Buffer;
try {
assertSameIdentity(before, await handle.stat());
bytes = await readCapturedArchive(handle, before.size);
assertSameIdentity(before, await handle.stat());
} finally {
await handle.close();
}
const archiveSha256 = createHash("sha256").update(bytes).digest("hex");
if (archiveSha256 !== input.expectedSha256) {
throw new Error("candidate archive SHA-256 mismatch");
}
return Object.freeze({ bytes, archiveSha256 });
}
export async function withVerifiedCapturedCandidate<T>(input: Readonly<{
captured: CapturedCandidateArchive;
verify: (view: Readonly<{
extractionRoot: string;
manifest: ReleaseCandidateManifest;
}>) => Promise<T>;
}>): Promise<T> {
let result: T | undefined;
await verifyCapturedCiCandidateArchive(
input.captured.bytes,
input.captured.archiveSha256,
{
verifyExtracted: async (extractionRoot, manifest) => {
result = await input.verify({ extractionRoot, manifest });
},
},
);
return result as T;
}
export async function verifyCiCandidateArchive(
input: Readonly<{
archivePath: string;
expectedSha256?: string;
extractTo?: string;
repositoryRoot?: string;
}>,
dependencies: Readonly<{ afterArchiveRead?: () => Promise<void> }> = {},
): Promise<Readonly<{
archiveSha256: string;
memberCount: number;
manifest: ReleaseCandidateManifest;
}>> {
if (input.expectedSha256 && !/^[a-f0-9]{64}$/u.test(input.expectedSha256)) {
throw new TypeError("expected candidate archive SHA-256 is invalid");
}
const absolute = path.resolve(input.archivePath);
const before = await lstat(absolute);
if (!before.isFile() || before.isSymbolicLink()) {
throw new TypeError("candidate archive must be a regular non-symlink file");
}
if (before.size <= 0 || before.size > MAX_ARCHIVE_BYTES) {
throw new RangeError(`candidate archive size is outside 1..${MAX_ARCHIVE_BYTES}`);
}
const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
let archive: Buffer;
try {
assertSameIdentity(before, await handle.stat());
archive = await readCapturedArchive(handle, before.size);
assertSameIdentity(before, await handle.stat());
} finally {
await handle.close();
}
if (archive.byteLength !== before.size) {
throw new Error("candidate archive changed size during capture");
}
await dependencies.afterArchiveRead?.();
const archiveSha256 = createHash("sha256").update(archive).digest("hex");
if (input.expectedSha256 && archiveSha256 !== input.expectedSha256) {
throw new Error("candidate archive SHA-256 mismatch");
}
const extractionTarget = input.extractTo ? path.resolve(input.extractTo) : undefined;
let extractionRoot: string;
let extractionParentIdentity: Awaited<ReturnType<typeof ensureSafePublishDirectory>> | undefined;
if (extractionTarget) {
if (!input.repositoryRoot) {
throw new TypeError("repositoryRoot is required when publishing an extracted candidate");
}
const repositoryRoot = path.resolve(input.repositoryRoot);
extractionParentIdentity = await ensureSafePublishDirectory(
repositoryRoot,
path.dirname(extractionTarget),
);
await assertSafePublishLeaf(extractionTarget, input.extractTo);
extractionRoot = await mkdtemp(
path.join(path.dirname(extractionTarget), `.${path.basename(extractionTarget)}.verified-`),
);
} else {
extractionRoot = await mkdtemp(path.join(tmpdir(), "ci-candidate-archive-"));
}
let published = false;
try {
const captured = await materializeCapturedArchive(archive);
try {
const preflightManifest = preflightArchiveHandle(captured.handle);
extractArchiveHandle(captured.handle, extractionRoot);
const verified = await verifyExtractedTree(extractionRoot, preflightManifest);
if (extractionTarget) {
const repositoryRoot = path.resolve(input.repositoryRoot!);
const currentParentIdentity = await ensureSafePublishDirectory(
repositoryRoot,
path.dirname(extractionTarget),
);
if (
!extractionParentIdentity ||
extractionParentIdentity.dev <= 0 ||
extractionParentIdentity.ino <= 0 ||
currentParentIdentity.dev !== extractionParentIdentity.dev ||
currentParentIdentity.ino !== extractionParentIdentity.ino
) {
throw new Error("verified extraction parent identity changed");
}
await assertSafePublishLeaf(extractionTarget, input.extractTo);
if (await pathExists(extractionTarget)) {
throw new Error(`verified extraction target already exists: ${input.extractTo}`);
}
await rename(extractionRoot, extractionTarget);
published = true;
}
return Object.freeze({
archiveSha256,
memberCount: verified.memberCount,
manifest: verified.manifest,
});
} finally {
await captured.handle.close();
await rm(captured.root, { recursive: true, force: true });
}
} finally {
if (!published) await rm(extractionRoot, { recursive: true, force: true });
}
}
export async function verifyCapturedCiCandidateArchive(
archive: Buffer,
expectedSha256: string,
dependencies: Readonly<{
verifyExtracted?: (
extractionRoot: string,
manifest: ReleaseCandidateManifest,
) => Promise<void>;
}> = {},
): Promise<Readonly<{
archiveSha256: string;
memberCount: number;
manifest: ReleaseCandidateManifest;
}>> {
if (archive.byteLength <= 0 || archive.byteLength > MAX_ARCHIVE_BYTES) {
throw new RangeError(`candidate archive size is outside 1..${MAX_ARCHIVE_BYTES}`);
}
if (!/^[a-f0-9]{64}$/u.test(expectedSha256)) {
throw new TypeError("expected candidate archive SHA-256 is invalid");
}
const archiveSha256 = createHash("sha256").update(archive).digest("hex");
if (archiveSha256 !== expectedSha256) {
throw new Error("candidate archive SHA-256 mismatch");
}
const captured = await materializeCapturedArchive(archive);
const extractionRoot = await mkdtemp(path.join(tmpdir(), "ci-captured-candidate-"));
try {
const manifest = preflightArchiveHandle(captured.handle);
extractArchiveHandle(captured.handle, extractionRoot);
const verified = await verifyExtractedTree(extractionRoot, manifest);
await dependencies.verifyExtracted?.(extractionRoot, verified.manifest);
return Object.freeze({
archiveSha256,
memberCount: verified.memberCount,
manifest: verified.manifest,
});
} finally {
await rm(extractionRoot, { recursive: true, force: true });
await captured.handle.close();
await rm(captured.root, { recursive: true, force: true });
}
}
function preflightArchiveHandle(archiveHandle: FileHandle): ReleaseCandidateManifest {
const listed = spawnSync(
TAR_EXECUTABLE,
["--list", "--verbose", "--numeric-owner", "--full-time", "--gzip", "--file", "/proc/self/fd/3"],
{
encoding: "utf8",
maxBuffer: 16_777_216,
timeout: 10_000,
env: TAR_ENVIRONMENT,
stdio: ["ignore", "pipe", "pipe", archiveHandle.fd],
},
);
if (listed.status !== 0 || listed.signal || listed.error) {
throw new Error(
`candidate archive listing failed: ${listed.stderr || listed.error?.message || listed.signal}`,
);
}
const seen = new Set<string>();
const regularMembers = new Set<string>();
const directoryMembers = new Set<string>();
let totalBytes = 0;
const lines = listed.stdout.split(/\r?\n/u).filter(Boolean);
if (lines.length === 0 || lines.length > MAX_ARCHIVE_MEMBERS) {
throw new RangeError(`candidate archive member count is outside 1..${MAX_ARCHIVE_MEMBERS}`);
}
for (const line of lines) {
const match = /^(?<mode>.{10})\s+\d+\/\d+\s+(?<bytes>\d+)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:\s+[+-]\d{4})?\s+(?<path>.+)$/u.exec(line);
if (!match?.groups) throw new Error(`candidate archive listing is unparseable: ${line}`);
const member = match.groups.path!.endsWith("/")
? match.groups.path!.slice(0, -1)
: match.groups.path!;
assertSafeMemberPath(member);
if (seen.has(member)) throw new Error(`candidate archive duplicate member: ${member}`);
seen.add(member);
const mode = match.groups.mode!;
if (!mode.startsWith("-") && !mode.startsWith("d")) {
throw new Error(`candidate archive contains non-regular member: ${member}`);
}
if (mode.startsWith("-")) {
const memberBytes = Number(match.groups.bytes);
if (
member === RELEASE_CANDIDATE_MANIFEST_PATH &&
memberBytes > 8_388_608
) {
throw new RangeError("candidate manifest exceeds 8388608 bytes");
}
totalBytes += memberBytes;
if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_ARCHIVE_BYTES) {
throw new RangeError("candidate archive expanded bytes exceed the bound");
}
regularMembers.add(member);
} else {
directoryMembers.add(member);
}
}
const manifest = readManifestFromArchive(archiveHandle);
validateManifestSemantics(manifest);
const expectedFiles = new Set([
...manifest.files.map(({ path: member }) => member),
RELEASE_CANDIDATE_MANIFEST_PATH,
]);
for (const member of expectedFiles) assertSafeMemberPath(member);
const expectedDirectories = new Set(
directoryAncestors([...expectedFiles]).filter(
(member) => member === "dist" || member.startsWith("dist/"),
),
);
if (
JSON.stringify([...regularMembers].sort(asciiCompare)) !==
JSON.stringify([...expectedFiles].sort(asciiCompare)) ||
JSON.stringify([...directoryMembers].sort(asciiCompare)) !==
JSON.stringify([...expectedDirectories].sort(asciiCompare))
) {
throw new Error("candidate archive exact member set drift before extraction");
}
return manifest;
}
function extractArchiveHandle(archiveHandle: FileHandle, extractionRoot: string): void {
const extracted = spawnSync(
TAR_EXECUTABLE,
[
"--extract",
"--gzip",
"--file",
"/proc/self/fd/3",
"--directory",
extractionRoot,
"--no-same-owner",
"--no-same-permissions",
],
{
encoding: "utf8",
maxBuffer: 1_048_576,
timeout: 30_000,
env: TAR_ENVIRONMENT,
stdio: ["ignore", "pipe", "pipe", archiveHandle.fd],
},
);
if (extracted.status !== 0 || extracted.signal || extracted.error) {
throw new Error(
`candidate archive isolated extraction failed: ${extracted.stderr || extracted.error?.message || extracted.signal}`,
);
}
}
function validateManifestSemantics(manifest: ReleaseCandidateManifest): void {
if (manifest.files.length === 0 || manifest.files.length > MAX_CANDIDATE_FILES) {
throw new RangeError(`candidate manifest exceeds ${MAX_CANDIDATE_FILES} files`);
}
const canonicalFiles = [...manifest.files].sort((left, right) =>
asciiCompare(left.path, right.path),
);
if (JSON.stringify(manifest.files) !== JSON.stringify(canonicalFiles)) {
throw new Error("candidate manifest files are not in canonical ASCII order");
}
const expectedFiles = new Map<string, Readonly<{ bytes: number; sha256: string }>>();
let declaredBytes = 0;
for (const file of manifest.files) {
assertSafeMemberPath(file.path);
if (expectedFiles.has(file.path)) {
throw new Error(`candidate manifest duplicate file: ${file.path}`);
}
declaredBytes += file.bytes;
if (!Number.isSafeInteger(declaredBytes) || declaredBytes > MAX_ARCHIVE_BYTES) {
throw new RangeError("candidate manifest declared bytes exceed the archive bound");
}
expectedFiles.set(file.path, { bytes: file.bytes, sha256: file.sha256 });
}
const evidencePaths = [...expectedFiles.keys()]
.filter((member) => !member.startsWith("dist/"))
.sort(asciiCompare);
if (
JSON.stringify(evidencePaths) !==
JSON.stringify([...RELEASE_CANDIDATE_EVIDENCE_PATHS].sort(asciiCompare))
) {
throw new Error("candidate manifest evidence member set drift");
}
const distFiles = manifest.files.filter(({ path: member }) => member.startsWith("dist/"));
if (distFiles.length === 0) throw new Error("candidate manifest has no dist files");
const lockfile = expectedFiles.get("pnpm-lock.yaml");
if (!lockfile || lockfile.sha256 !== manifest.lockfileSha256) {
throw new Error("candidate manifest lockfile digest summary mismatch");
}
if (
distSha256(distFiles.map((file) => ({ ...file, gzipBytes: 0 }))) !==
manifest.distSha256
) {
throw new Error("candidate manifest dist digest summary mismatch");
}
if (supplyChainDigest(manifest.files) !== manifest.bundleSha256) {
throw new Error("candidate manifest bundle digest summary mismatch");
}
}
async function verifyExtractedTree(
extractionRoot: string,
preflightManifest: ReleaseCandidateManifest,
): Promise<Readonly<{ memberCount: number; manifest: ReleaseCandidateManifest }>> {
const entries = await walkExtractedTree(extractionRoot);
if (entries.length === 0 || entries.length > MAX_ARCHIVE_MEMBERS) {
throw new RangeError(`candidate archive member count is outside 1..${MAX_ARCHIVE_MEMBERS}`);
}
const manifest = releaseCandidateManifestSchema.parse(
JSON.parse(
await readFile(path.join(extractionRoot, RELEASE_CANDIDATE_MANIFEST_PATH), "utf8"),
) as unknown,
);
if (JSON.stringify(manifest) !== JSON.stringify(preflightManifest)) {
throw new Error("candidate manifest changed between preflight and extraction");
}
if (manifest.files.length === 0 || manifest.files.length > MAX_CANDIDATE_FILES) {
throw new RangeError(`candidate manifest exceeds ${MAX_CANDIDATE_FILES} files`);
}
const canonicalFiles = [...manifest.files].sort((left, right) =>
asciiCompare(left.path, right.path),
);
if (JSON.stringify(manifest.files) !== JSON.stringify(canonicalFiles)) {
throw new Error("candidate manifest files are not in canonical ASCII order");
}
const expectedFiles = new Map<string, Readonly<{ bytes: number; sha256: string }>>();
let declaredBytes = 0;
for (const file of manifest.files) {
assertSafeMemberPath(file.path);
if (expectedFiles.has(file.path)) throw new Error(`candidate manifest duplicate file: ${file.path}`);
declaredBytes += file.bytes;
if (!Number.isSafeInteger(declaredBytes) || declaredBytes > MAX_ARCHIVE_BYTES) {
throw new RangeError("candidate manifest declared bytes exceed the archive bound");
}
expectedFiles.set(file.path, { bytes: file.bytes, sha256: file.sha256 });
}
const evidencePaths = [...expectedFiles.keys()]
.filter((member) => !member.startsWith("dist/"))
.sort(asciiCompare);
if (
JSON.stringify(evidencePaths) !==
JSON.stringify([...RELEASE_CANDIDATE_EVIDENCE_PATHS].sort(asciiCompare))
) {
throw new Error("candidate manifest evidence member set drift");
}
const distFiles = manifest.files.filter(({ path: member }) => member.startsWith("dist/"));
if (distFiles.length === 0) throw new Error("candidate manifest has no dist files");
const lockfile = expectedFiles.get("pnpm-lock.yaml");
if (!lockfile || lockfile.sha256 !== manifest.lockfileSha256) {
throw new Error("candidate manifest lockfile digest summary mismatch");
}
if (
distSha256(distFiles.map((file) => ({ ...file, gzipBytes: 0 }))) !==
manifest.distSha256
) {
throw new Error("candidate manifest dist digest summary mismatch");
}
if (supplyChainDigest(manifest.files) !== manifest.bundleSha256) {
throw new Error("candidate manifest bundle digest summary mismatch");
}
const expectedFilePaths = new Set([
...expectedFiles.keys(),
RELEASE_CANDIDATE_MANIFEST_PATH,
]);
const expectedDirectories = new Set(directoryAncestors([...expectedFilePaths]));
for (const entry of entries) {
assertSafeMemberPath(entry.path);
if (entry.type === "directory") {
if (!expectedDirectories.has(entry.path)) {
throw new Error(`candidate archive contains unexpected directory: ${entry.path}`);
}
} else if (!expectedFilePaths.has(entry.path)) {
throw new Error(`candidate archive contains unexpected file: ${entry.path}`);
}
}
const actualFiles = new Set(
entries.filter(({ type }) => type === "file").map(({ path: member }) => member),
);
for (const expected of expectedFilePaths) {
if (!actualFiles.has(expected)) throw new Error(`candidate archive is missing file: ${expected}`);
}
for (const [member, expected] of expectedFiles) {
const bytes = await readFile(path.join(extractionRoot, member));
if (bytes.byteLength !== expected.bytes) {
throw new Error(`candidate archive member size mismatch: ${member}`);
}
if (createHash("sha256").update(bytes).digest("hex") !== expected.sha256) {
throw new Error(`candidate archive member digest mismatch: ${member}`);
}
}
return Object.freeze({ memberCount: entries.length, manifest });
}
async function walkExtractedTree(
root: string,
relativeDirectory = "",
): Promise<ReadonlyArray<Readonly<{ path: string; type: "file" | "directory" }>>> {
const children = await readdir(path.join(root, relativeDirectory), {
withFileTypes: true,
});
const entries: Array<Readonly<{ path: string; type: "file" | "directory" }>> = [];
for (const child of children.sort((left, right) => asciiCompare(left.name, right.name))) {
const relative = relativeDirectory ? `${relativeDirectory}/${child.name}` : child.name;
assertSafeMemberPath(relative);
const metadata = await lstat(path.join(root, relative));
if (metadata.isSymbolicLink()) {
throw new Error(`candidate archive contains non-regular member: ${relative}`);
}
if (metadata.isDirectory() && child.isDirectory()) {
entries.push(Object.freeze({ path: relative, type: "directory" }));
entries.push(...(await walkExtractedTree(root, relative)));
} else if (metadata.isFile() && child.isFile()) {
if (metadata.nlink !== 1) {
throw new Error(`candidate archive contains hard-linked member: ${relative}`);
}
entries.push(Object.freeze({ path: relative, type: "file" }));
} else {
throw new Error(`candidate archive contains non-regular member: ${relative}`);
}
if (entries.length > MAX_ARCHIVE_MEMBERS) {
throw new RangeError(`candidate archive exceeds ${MAX_ARCHIVE_MEMBERS} members`);
}
}
return entries;
}
function assertSameIdentity(
before: Awaited<ReturnType<typeof lstat>>,
after: Awaited<ReturnType<typeof lstat>>,
): void {
if (
!after.isFile() ||
before.dev !== after.dev ||
before.ino !== after.ino ||
before.size !== after.size
) {
throw new Error("candidate archive file identity changed");
}
}
async function readCapturedArchive(
handle: FileHandle,
expectedSize: number,
): Promise<Buffer> {
const captured = Buffer.allocUnsafe(expectedSize + 1);
let offset = 0;
while (offset < captured.byteLength) {
const { bytesRead } = await handle.read(
captured,
offset,
captured.byteLength - offset,
offset,
);
if (bytesRead === 0) break;
offset += bytesRead;
}
if (offset !== expectedSize) {
throw new Error("candidate archive changed size during bounded capture");
}
return captured.subarray(0, offset);
}
function readManifestFromArchive(archiveHandle: FileHandle): ReleaseCandidateManifest {
const extracted = spawnSync(
TAR_EXECUTABLE,
[
"--extract",
"--gzip",
"--to-stdout",
"--file",
"/proc/self/fd/3",
"--",
RELEASE_CANDIDATE_MANIFEST_PATH,
],
{
maxBuffer: 8_388_609,
timeout: 10_000,
env: TAR_ENVIRONMENT,
stdio: ["ignore", "pipe", "pipe", archiveHandle.fd],
},
);
if (extracted.status !== 0 || extracted.signal || extracted.error) {
throw new Error(
`candidate manifest preflight failed: ${String(extracted.stderr) || extracted.error?.message || extracted.signal}`,
);
}
const bytes = Buffer.from(extracted.stdout);
if (bytes.byteLength === 0 || bytes.byteLength > 8_388_608) {
throw new RangeError("candidate manifest preflight size is outside 1..8388608");
}
const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
return releaseCandidateManifestSchema.parse(JSON.parse(source) as unknown);
}
async function materializeCapturedArchive(
archive: Buffer,
): Promise<Readonly<{ root: string; handle: FileHandle }>> {
const root = await mkdtemp(path.join(tmpdir(), "ci-captured-archive-"));
const file = path.join(root, "candidate.tar.gz");
let handle: FileHandle | undefined;
try {
handle = await open(
file,
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
0o600,
);
await handle.writeFile(archive);
await handle.sync();
await unlink(file);
return Object.freeze({ root, handle });
} catch (error) {
if (handle) await handle.close().catch(() => undefined);
await rm(root, { recursive: true, force: true });
throw error;
}
}
function assertSafeMemberPath(member: string): void {
if (
!member ||
member.startsWith("-") ||
Buffer.byteLength(member, "utf8") > MAX_MEMBER_PATH_BYTES ||
member.includes("\\") ||
[...member].some((character) => {
const codePoint = character.codePointAt(0)!;
return codePoint <= 0x1f || codePoint === 0x7f;
}) ||
path.posix.isAbsolute(member) ||
path.posix.normalize(member) !== member ||
member === ".." ||
member.startsWith("../") ||
member.includes("/../")
) {
throw new TypeError(`candidate archive contains unsafe member path: ${member}`);
}
}
function directoryAncestors(files: readonly string[]): string[] {
const directories = new Set<string>();
for (const file of files) {
let directory = path.posix.dirname(file);
while (directory !== ".") {
directories.add(directory);
directory = path.posix.dirname(directory);
}
}
return [...directories];
}
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
async function pathExists(target: string): Promise<boolean> {
try {
await lstat(target);
return true;
} catch (error) {
if (hasErrorCode(error, "ENOENT")) return false;
throw error;
}
}
+45
View File
@@ -0,0 +1,45 @@
import { z } from "zod";
export const ciContractReportSchema = z
.object({
schemaVersion: z.literal(2),
nodeVersion: z.string().regex(/^\d+\.\d+\.\d+$/u),
gateCount: z.literal(26),
commandDefinitionCount: z.number().int().positive(),
commandReferenceCount: z.number().int().positive(),
artifactCount: z.number().int().positive(),
jobCount: z.literal(9),
workflowSha256: z.string().regex(/^[a-f0-9]{64}$/u),
durationStatus: z.string().min(1),
negativeFixtures: z.array(
z
.object({
readiness: z.enum([
"MERGE_READY",
"RELEASE_READY",
"PROD_PROMOTION_READY",
"FIELD_SLO_READY",
"DOCUMENTATION_READY",
]),
failedGate: z.string().regex(/^FE-GATE-\d{3}$/u),
passed: z.boolean(),
})
.strict(),
),
failures: z.array(z.string()),
passed: z.boolean(),
})
.strict()
.superRefine((report, context) => {
const fail = (path: PropertyKey[], message: string) =>
context.addIssue({ code: "custom", path, message });
if ((report.passed === true) !== (report.failures.length === 0)) {
fail(["passed"], "passed must agree with failures");
}
if (
report.negativeFixtures.length !== 5 ||
report.negativeFixtures.some((fixture) => !fixture.passed)
) {
fail(["negativeFixtures"], "every readiness negative fixture must pass");
}
});
+187
View File
@@ -0,0 +1,187 @@
import { randomUUID } from "node:crypto";
import { constants, type Stats } from "node:fs";
import { lstat, mkdir, open, rename, rm } from "node:fs/promises";
import path from "node:path";
import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts";
export async function writeCiGateLogAtomic(input: Readonly<{
root: string;
relativePath: string;
content: string;
maxBytes?: number;
}>): Promise<void> {
const root = path.resolve(input.root);
const relative = normalizeRepositoryRelativePath(input.relativePath, "CI gate log path");
const target = path.join(root, relative);
const maxBytes = input.maxBytes ?? 67_108_864;
const contentBytes = Buffer.byteLength(input.content, "utf8");
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || contentBytes < 1 || contentBytes > maxBytes) {
throw new RangeError(`CI gate log size is outside 1..${maxBytes}: ${relative}`);
}
const parentIdentity = await ensureSafePublishDirectory(root, path.dirname(target));
await assertSafePublishLeaf(target, relative);
const temporary = path.join(
path.dirname(target),
`.${path.basename(target)}.${randomUUID()}.tmp`,
);
let ownsTemporary = false;
try {
const handle = await open(
temporary,
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
0o644,
);
ownsTemporary = true;
let failure: unknown;
try {
await handle.writeFile(input.content, "utf8");
await handle.sync();
} catch (error) {
failure = error;
}
try {
await handle.close();
} catch (error) {
failure ??= error;
}
if (failure) throw failure;
await assertDirectoryIdentity(path.dirname(target), parentIdentity, relative);
await assertSafePublishLeaf(target, relative);
await rename(temporary, target);
ownsTemporary = false;
const directory = await open(path.dirname(target), constants.O_RDONLY);
try {
try {
await directory.sync();
} catch (error) {
if (!hasErrorCode(error, "EINVAL") && !hasErrorCode(error, "ENOTSUP")) throw error;
}
} finally {
await directory.close();
}
} catch (error) {
if (ownsTemporary) {
try {
await rm(temporary, { force: true });
} catch {
// Preserve the publication failure and clean only the owned sibling temp.
}
}
throw error;
}
}
export async function ensureSafePublishDirectory(
rootInput: string,
directoryInput: string,
): Promise<Stats> {
const root = path.resolve(rootInput);
const directory = path.resolve(directoryInput);
const relativeDirectory = path.relative(root, directory);
if (
relativeDirectory === ".." ||
relativeDirectory.startsWith(`..${path.sep}`) ||
path.isAbsolute(relativeDirectory)
) {
throw new TypeError("CI publish directory escapes root");
}
const rootMetadata = await lstat(root);
if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) {
throw new TypeError("CI gate log root is unsafe");
}
let ancestor = root;
for (const segment of relativeDirectory.split(path.sep).filter(Boolean)) {
ancestor = path.join(ancestor, segment);
let metadata;
try {
metadata = await lstat(ancestor);
} catch (error) {
if (!hasErrorCode(error, "ENOENT")) throw error;
try {
await mkdir(ancestor);
} catch (mkdirError) {
if (!hasErrorCode(mkdirError, "EEXIST")) throw mkdirError;
}
metadata = await lstat(ancestor);
}
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
throw new TypeError(`CI publish ancestor is unsafe: ${relativeDirectory}`);
}
}
return lstat(directory);
}
export async function assertSafePublishLeaf(
target: string,
label = target,
): Promise<void> {
try {
const metadata = await lstat(target);
if (metadata.isSymbolicLink() || !metadata.isFile()) {
throw new TypeError(`CI publish leaf is unsafe: ${label}`);
}
} catch (error) {
if (!hasErrorCode(error, "ENOENT")) throw error;
}
}
export async function assertSafeExistingPublishPath(
rootInput: string,
targetInput: string,
): Promise<boolean> {
const root = path.resolve(rootInput);
const target = path.resolve(targetInput);
const relative = path.relative(root, target);
if (
relative === "" ||
relative === ".." ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
throw new TypeError("CI publish target escapes root");
}
const rootMetadata = await lstat(root);
if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) {
throw new TypeError("CI publish root is unsafe");
}
const segments = relative.split(path.sep).filter(Boolean);
let current = root;
for (const [index, segment] of segments.entries()) {
current = path.join(current, segment);
let metadata: Stats;
try {
metadata = await lstat(current);
} catch (error) {
if (hasErrorCode(error, "ENOENT")) return false;
throw error;
}
const leaf = index === segments.length - 1;
if (metadata.isSymbolicLink() || (leaf ? !metadata.isFile() : !metadata.isDirectory())) {
throw new TypeError(`CI publish path is unsafe: ${relative}`);
}
}
return true;
}
async function assertDirectoryIdentity(
directory: string,
expected: Stats,
label: string,
): Promise<void> {
const actual = await lstat(directory);
if (
actual.isSymbolicLink() ||
!actual.isDirectory() ||
expected.dev <= 0 ||
expected.ino <= 0 ||
actual.dev !== expected.dev ||
actual.ino !== expected.ino
) {
throw new TypeError(`CI publish directory identity changed: ${label}`);
}
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+82
View File
@@ -0,0 +1,82 @@
export type GateStepExpectation =
| Readonly<{ kind: "pass" }>
| Readonly<{
kind: "fail";
expectedExitCode: number;
expectedDiagnosticId: string;
}>;
export type GateProcessResult = Readonly<{
status: number | null;
signal: string | null;
stdout: string;
stderr: string;
error?: Readonly<{ code?: string }>;
}>;
export type GateStepClassification =
| Readonly<{
kind: "EXPECTED_PASS" | "EXPECTED_FAILURE";
expectationMet: true;
}>
| Readonly<{
kind: "UNEXPECTED_EXIT";
expectationMet: false;
}>
| Readonly<{
kind: "UNEXPECTED_DIAGNOSTIC";
expectationMet: false;
}>
| Readonly<{
kind: "INFRASTRUCTURE_FAILURE";
expectationMet: false;
detail: string;
}>;
/** A negative fixture passes only with its registered exit and diagnostic. */
export function classifyGateStepResult(
expectation: GateStepExpectation,
result: GateProcessResult,
): GateStepClassification {
const errorCode = result.error?.code;
const spawnFailed = result.error !== undefined;
if (spawnFailed || result.signal || result.status === null) {
return Object.freeze({
kind: "INFRASTRUCTURE_FAILURE" as const,
expectationMet: false as const,
detail:
errorCode ??
result.signal ??
(spawnFailed ? "SPAWN_ERROR" : "NO_EXIT_STATUS"),
});
}
if (expectation.kind === "pass" && result.status === 0) {
return Object.freeze({
kind: "EXPECTED_PASS" as const,
expectationMet: true as const,
});
}
if (expectation.kind === "fail") {
if (result.status !== expectation.expectedExitCode) {
return Object.freeze({
kind: "UNEXPECTED_EXIT" as const,
expectationMet: false as const,
});
}
const diagnosticOutput = `${result.stdout}\n${result.stderr}`;
if (!diagnosticOutput.includes(expectation.expectedDiagnosticId)) {
return Object.freeze({
kind: "UNEXPECTED_DIAGNOSTIC" as const,
expectationMet: false as const,
});
}
return Object.freeze({
kind: "EXPECTED_FAILURE" as const,
expectationMet: true as const,
});
}
return Object.freeze({
kind: "UNEXPECTED_EXIT" as const,
expectationMet: false as const,
});
}
+49
View File
@@ -0,0 +1,49 @@
type ViteManifestEntry = Readonly<{
file: string;
isEntry?: boolean;
imports?: readonly string[];
}>;
/**
* Static imports of an entry are part of initial JavaScript. Every remaining
* JavaScript output is governed by the lazy-chunk budget.
*
*/
export function classifyViteJavascript(
manifest: Readonly<Record<string, ViteManifestEntry>>,
) {
const initialFiles = new Set<string>();
const visitedKeys = new Set<string>();
const pendingKeys = Object.entries(manifest)
.filter(([, entry]) => entry.isEntry)
.map(([key]) => key);
const missingImports: string[] = [];
while (pendingKeys.length > 0) {
const key = pendingKeys.pop();
if (key === undefined) break;
if (visitedKeys.has(key)) continue;
visitedKeys.add(key);
const entry = manifest[key];
if (!entry) {
missingImports.push(key);
continue;
}
if (entry.file.endsWith(".js")) initialFiles.add(entry.file);
pendingKeys.push(...(entry.imports ?? []));
}
const allJavaScript = new Set(
Object.values(manifest)
.map((entry) => entry.file)
.filter((file) => file.endsWith(".js")),
);
const lazyFiles = [...allJavaScript].filter(
(file) => !initialFiles.has(file),
);
return Object.freeze({
initialFiles: Object.freeze([...initialFiles].sort()),
lazyFiles: Object.freeze(lazyFiles.sort()),
missingImports: Object.freeze(missingImports.sort()),
});
}
+353
View File
@@ -0,0 +1,353 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import {
PROMOTED_FILE_NAMES,
type PromotedFileName,
} from "../contracts/promotion-artifacts.ts";
import {
PROMOTION_VERIFIER_ID,
PROMOTION_VERIFIER_VERSION,
assertDistinctProviderTrust,
evaluatePromotionEvidence,
providerVerificationArtifactSchema,
provenanceProviderAttestationSchema,
vulnerabilityProviderReportSchema,
trustPolicySha256,
type ProviderTrust,
} from "./provider-evidence.ts";
import { verifyCapturedCiCandidateArchive } from "./ci-candidate-archive.ts";
import { LOCAL_EVIDENCE_ASSESSMENT_PATH } from "./release-candidate.ts";
import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts";
export type ExactPromotionBundle = Readonly<
Partial<Record<PromotedFileName, Buffer>>
>;
export type ExactPromotionExpectedContext = Readonly<{
run: Readonly<{ id: string; attempt: number }>;
sourceRevision: string;
archiveSha256: string;
sourceSetSha256?: string;
bundleSha256?: string;
distSha256?: string;
lockfileSha256?: string;
}>;
export async function verifyExactPromotionBundle(
files: ExactPromotionBundle,
options: Readonly<{
vulnerabilityTrust: ProviderTrust;
provenanceTrust: ProviderTrust;
expected: ExactPromotionExpectedContext;
nowEpochMs?: () => number;
}>,
): Promise<Readonly<{ status: "PASS" }>> {
assertDistinctProviderTrust(options);
assertExternalExpectedContext(options.expected);
const names = Object.keys(files).sort(asciiCompare);
const expectedNames = [...PROMOTED_FILE_NAMES].sort(asciiCompare);
if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
throw new Error("promotion bundle must contain the exact five canonical files");
}
for (const name of PROMOTED_FILE_NAMES) {
if (!Buffer.isBuffer(files[name])) {
throw new TypeError(`promotion bundle file is missing or not captured bytes: ${name}`);
}
}
const archiveBytes = files["release-candidate.tar.gz"]!;
const vulnerabilityBytes = files["vulnerability-report.json"]!;
const provenanceBytes = files["provenance-attestation.json"]!;
const providerBytes = files["provider-verification.json"]!;
const promotionBytes = files["promotion-verification.json"]!;
const vulnerability = vulnerabilityProviderReportSchema.parse(parseJson(
vulnerabilityBytes,
"vulnerability report",
));
const provenance = provenanceProviderAttestationSchema.parse(parseJson(
provenanceBytes,
"provenance attestation",
));
const provider = providerVerificationArtifactSchema.parse(parseJson(
providerBytes,
"provider verification",
));
const promotion = providerVerificationArtifactSchema.parse(parseJson(
promotionBytes,
"promotion verification",
));
if (
provider.artifactType !== "provider-verification" ||
promotion.artifactType !== "promotion-verification"
) {
throw new Error("promotion verification artifact role mismatch");
}
for (const [label, record] of [
["provider", provider],
["promotion", promotion],
] as const) {
if (
record.verifier.id !== PROMOTION_VERIFIER_ID ||
record.verifier.version !== PROMOTION_VERIFIER_VERSION
) {
throw new Error(`${label} verification literal verifier identity mismatch`);
}
if (record.status !== "PASS" || record.failures.length !== 0) {
throw new Error(`${label} verification must be PASS without failures`);
}
}
if (
provider.vulnerabilityStatus !== "PASS" ||
provider.provenanceAttestationStatus !== "PASS"
) {
throw new Error("provider verification subordinate statuses must both be PASS");
}
if (promotion.localEvidenceStatus !== "PASS") {
throw new Error("promotion local evidence subordinate status must be PASS");
}
assertEqual("shared verifiedAt", provider.verifiedAt, promotion.verifiedAt);
assertEqual("shared run", provider.run, promotion.run);
assertEqual("shared source", provider.source, promotion.source);
assertEqual("shared candidate", provider.candidate, promotion.candidate);
assertEqual(
"shared provider evidence",
provider.providerEvidence,
promotion.providerEvidence,
);
assertEqual(
"shared trust policy",
provider.trustPolicySha256,
promotion.trustPolicySha256,
);
assertEqual("external expected run", provider.run, options.expected.run);
assertEqual(
"external expected source revision",
provider.source.revision,
options.expected.sourceRevision,
);
assertEqual(
"external expected archive digest",
provider.candidate.archiveSha256,
options.expected.archiveSha256,
);
for (const [label, actual, expected] of [
["source set", provider.source.sourceSetSha256, options.expected.sourceSetSha256],
["bundle", provider.candidate.bundleSha256, options.expected.bundleSha256],
["dist", provider.candidate.distSha256, options.expected.distSha256],
["lockfile", provider.candidate.lockfileSha256, options.expected.lockfileSha256],
] as const) {
if (expected !== undefined) {
assertEqual(`external expected ${label} digest`, actual, expected);
}
}
const anchoredTrustPolicySha256 = trustPolicySha256(options);
if (provider.trustPolicySha256 !== anchoredTrustPolicySha256) {
throw new Error("verification trust policy does not match anchored provider keys");
}
if (promotion.providerVerificationSha256 !== sha256(providerBytes)) {
throw new Error("promotion provider verification byte hash mismatch");
}
if (
provider.candidate.archiveSha256 !== sha256(archiveBytes) ||
provider.providerEvidence.vulnerabilityReportSha256 !== sha256(vulnerabilityBytes) ||
provider.providerEvidence.provenanceAttestationSha256 !== sha256(provenanceBytes)
) {
if (provider.candidate.archiveSha256 !== sha256(archiveBytes)) {
throw new Error("candidate archive actual digest mismatch");
}
if (
provider.providerEvidence.vulnerabilityReportSha256 !==
sha256(vulnerabilityBytes)
) {
throw new Error("vulnerability report actual digest mismatch");
}
throw new Error("provenance attestation actual digest mismatch");
}
for (const [label, evidence, nonce, keyId, fingerprint] of [
[
"vulnerability",
vulnerability,
provider.providerEvidence.vulnerabilityInvocationNonce,
provider.providerEvidence.vulnerabilityKeyId,
provider.providerEvidence.vulnerabilityKeyFingerprint,
],
[
"provenance",
provenance,
provider.providerEvidence.provenanceInvocationNonce,
provider.providerEvidence.provenanceKeyId,
provider.providerEvidence.provenanceKeyFingerprint,
],
] as const) {
assertEqual(`${label} run`, { id: evidence.run.id, attempt: evidence.run.attempt }, provider.run);
assertEqual(`${label} source`, evidence.source, provider.source);
assertEqual(`${label} candidate`, evidence.candidate, provider.candidate);
if (
evidence.run.invocationNonce !== nonce ||
evidence.signature.keyId !== keyId ||
evidence.signature.publicKeyFingerprint !== fingerprint
) {
throw new Error(`${label} provider evidence nonce or trust role mismatch`);
}
}
if (provenance.subject.digest.sha256 !== provider.candidate.distSha256) {
throw new Error("provenance subject dist digest mismatch");
}
if (vulnerability.findings.length !== 0) {
throw new Error("vulnerability report is not PASS");
}
assertEqual(
"signed secret scan attestation",
vulnerability.secretScanAttestation,
provider.providerEvidence.secretScanAttestation,
);
let assessmentSha256: string | null = null;
const localIdentityHolder: {
current: null | Readonly<{
sourceRevision: string;
sourceSetSha256: string;
assessmentSha256: string;
secretScan: Readonly<{
policySha256: string;
sarifSha256: string;
scanInputSha256: string;
}>;
}>;
} = { current: null };
await verifyCapturedCiCandidateArchive(
archiveBytes,
provider.candidate.archiveSha256,
{
verifyExtracted: async (extractionRoot, manifest) => {
assertEqual("archive candidate", {
bundleSha256: manifest.bundleSha256,
distSha256: manifest.distSha256,
lockfileSha256: manifest.lockfileSha256,
}, {
bundleSha256: provider.candidate.bundleSha256,
distSha256: provider.candidate.distSha256,
lockfileSha256: provider.candidate.lockfileSha256,
});
assessmentSha256 = sha256(
await readFile(path.join(extractionRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH)),
);
const local = await verifyArchivedLocalEvidence({
extractionRoot,
expectedManifest: manifest,
});
if (local.status !== "PASS" || !local.identity) {
throw new Error(
`exact-five archived local verification is not PASS: ${local.failures.join(", ")}`,
);
}
localIdentityHolder.current = local.identity;
},
},
);
if (assessmentSha256 !== promotion.localEvidenceAssessmentSha256) {
throw new Error("promotion local evidence assessment actual digest mismatch");
}
if (
!localIdentityHolder.current ||
localIdentityHolder.current.sourceRevision !== provider.source.revision ||
localIdentityHolder.current.sourceSetSha256 !== provider.source.sourceSetSha256 ||
localIdentityHolder.current.assessmentSha256 !== promotion.localEvidenceAssessmentSha256
) {
throw new Error("exact-five archived local identity mismatch");
}
assertEqual("archived secret scan attestation", {
status: "PASS",
localEvidenceAssessmentSha256: localIdentityHolder.current.assessmentSha256,
sourceSetSha256: localIdentityHolder.current.sourceSetSha256,
policySha256: localIdentityHolder.current.secretScan.policySha256,
sarifSha256: localIdentityHolder.current.secretScan.sarifSha256,
scanInputSha256: localIdentityHolder.current.secretScan.scanInputSha256,
}, vulnerability.secretScanAttestation);
const reevaluated = evaluatePromotionEvidence({
expected: {
run: provider.run,
source: provider.source,
candidate: provider.candidate,
vulnerabilityInvocationNonce:
provider.providerEvidence.vulnerabilityInvocationNonce,
provenanceInvocationNonce:
provider.providerEvidence.provenanceInvocationNonce,
secretScanAttestation: provider.providerEvidence.secretScanAttestation,
},
localStatus: "PASS",
vulnerabilityReport: vulnerability,
provenanceAttestation: provenance,
vulnerabilityTrust: options.vulnerabilityTrust,
provenanceTrust: options.provenanceTrust,
nowEpochMs: options.nowEpochMs,
});
if (
reevaluated.status !== "PASS" ||
reevaluated.vulnerabilityStatus !== "PASS" ||
reevaluated.provenanceAttestationStatus !== "PASS"
) {
throw new Error(
`exact-five provider signature/freshness revalidation is not PASS: ${reevaluated.failures.join(", ")}`,
);
}
return Object.freeze({ status: "PASS" as const });
}
function assertExternalExpectedContext(
expected: ExactPromotionExpectedContext,
): void {
if (
!expected ||
typeof expected.run?.id !== "string" ||
expected.run.id.length === 0 ||
!Number.isSafeInteger(expected.run.attempt) ||
expected.run.attempt < 1 ||
!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(expected.sourceRevision) ||
!isSha256(expected.archiveSha256)
) {
throw new TypeError("external expected promotion context is invalid or incomplete");
}
for (const digest of [
expected.sourceSetSha256,
expected.bundleSha256,
expected.distSha256,
expected.lockfileSha256,
]) {
if (digest !== undefined && !isSha256(digest)) {
throw new TypeError("external optional expected promotion digest is invalid");
}
}
}
function isSha256(value: unknown): value is string {
return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
}
function parseJson(bytes: Buffer, label: string): unknown {
try {
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown;
} catch {
throw new TypeError(`${label} is not strict UTF-8 JSON`);
}
}
function assertEqual(label: string, left: unknown, right: unknown): void {
if (JSON.stringify(left) !== JSON.stringify(right)) {
throw new Error(`${label} mismatch`);
}
}
function sha256(bytes: Buffer): string {
return createHash("sha256").update(bytes).digest("hex");
}
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
+117
View File
@@ -0,0 +1,117 @@
import { z } from "zod";
const WINDOW_MILLISECONDS = 28 * 24 * 60 * 60 * 1000;
const nonEmptyString = z.string().trim().min(1);
const timestamp = nonEmptyString.refine(
(value) => Number.isFinite(Date.parse(value)),
"must be an RFC 3339 timestamp",
);
const sampleSchema = z
.object({
timestamp,
consent: z.boolean(),
releaseId: nonEmptyString,
routeId: nonEmptyString.regex(/^[A-Z][A-Z0-9_]*$/),
lcpMs: z.number().finite().nonnegative(),
cls: z.number().finite().nonnegative(),
inpMs: z.number().finite().nonnegative(),
})
.strict();
const fieldEvidenceInputSchema = z
.object({
schemaVersion: z.literal(1),
environment: z.literal("production"),
releaseId: nonEmptyString.refine(
(value) => value !== "local-release",
"must identify an immutable production release",
),
source: z
.object({
system: nonEmptyString,
exportId: nonEmptyString,
})
.strict(),
privacy: z
.object({
approved: z.literal(true),
approvalRef: nonEmptyString,
})
.strict(),
window: z
.object({
start: timestamp,
end: timestamp,
})
.strict(),
thresholdDecision: z
.object({
status: z.literal("approved"),
minimumEligibleSamples: z.number().int().positive(),
owner: nonEmptyString,
reviewedAt: timestamp,
evidenceRef: nonEmptyString,
})
.strict(),
samples: z.array(sampleSchema),
})
.strict()
.superRefine((input, context) => {
const start = Date.parse(input.window.start);
const end = Date.parse(input.window.end);
if (end - start !== WINDOW_MILLISECONDS) {
context.addIssue({
code: "custom",
path: ["window"],
message: "must cover exactly 28 days",
});
}
});
export function validateFieldEvidenceInput(
input: unknown,
configuredMinimum: string | undefined,
now: Date = new Date(),
) {
const parsed = fieldEvidenceInputSchema.safeParse(input);
const failures = parsed.success
? []
: parsed.error.issues.map(
(issue) => `${issue.path.join(".") || "input"}: ${issue.message}`,
);
const minimumEligibleSamples = Number(configuredMinimum);
if (
configuredMinimum === undefined ||
!Number.isInteger(minimumEligibleSamples) ||
minimumEligibleSamples <= 0
) {
failures.push("MIN_ELIGIBLE_SAMPLES: must be a positive integer");
}
if (parsed.success) {
if (
parsed.data.thresholdDecision.minimumEligibleSamples !==
minimumEligibleSamples
) {
failures.push(
"MIN_ELIGIBLE_SAMPLES: does not match the approved threshold decision",
);
}
if (Date.parse(parsed.data.window.end) > now.getTime()) {
failures.push("window.end: must not be in the future");
}
if (Date.parse(parsed.data.thresholdDecision.reviewedAt) > now.getTime()) {
failures.push("thresholdDecision.reviewedAt: must not be in the future");
}
}
return Object.freeze({
data: parsed.success ? parsed.data : null,
failures: Object.freeze(failures),
minimumEligibleSamples:
Number.isInteger(minimumEligibleSamples) && minimumEligibleSamples > 0
? minimumEligibleSamples
: null,
passed: parsed.success && failures.length === 0,
});
}
+74
View File
@@ -0,0 +1,74 @@
const LOOPBACK_IPV4 = /^127(?:\.\d{1,3}){3}$/;
/**
* A release gate must not promote a local preview server as live hosting
* evidence.
*
*/
export function classifyLiveHostingBaseUrl(value: string):
| {
passed: true;
reason: null;
url: URL;
observedOrigin: string;
}
| {
passed: false;
reason: string;
url: URL | null;
observedOrigin: string | null;
} {
let url: 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 };
}
+281
View File
@@ -0,0 +1,281 @@
import { createHash } from "node:crypto";
import { z } from "zod";
export type AttemptStatus = number | "NETWORK_REJECTION" | "PENDING_ABORT";
export type RetryReason = "NETWORK_FAILURE" | "HTTP_429" | "HTTP_503";
export type BodyDisposition =
| "FULLY_READ_WITHIN_BOUND"
| "CANCELLED_WITHOUT_READ"
| "REJECTED_LIMIT"
| "NO_RESPONSE";
const attemptStatusSchema = z.union([
z.number().int().min(100).max(599),
z.literal("NETWORK_REJECTION"),
z.literal("PENDING_ABORT"),
]);
export const httpScenarioAssertionGroupsSchema = z
.object({
status: z
.object({
attempts: z.array(attemptStatusSchema).min(1),
final: attemptStatusSchema,
})
.strict(),
outcome: z.object({ kind: z.string().min(1), detail: z.string().nullable() }).strict(),
effect: z.object({ outcome: z.string().min(1), observer: z.string().min(1) }).strict(),
retry: z
.object({
count: z.number().int().nonnegative(),
reasons: z.array(z.enum(["NETWORK_FAILURE", "HTTP_429", "HTTP_503"])),
})
.strict(),
fetch: z
.object({
count: z.number().int().positive(),
observerAttempts: z.number().int().positive(),
agrees: z.boolean(),
})
.strict(),
media: z.object({ attempts: z.array(z.string().nullable()).min(1), final: z.string().nullable() }).strict(),
body: z
.object({
attempts: z
.array(
z
.object({
disposition: z.enum([
"FULLY_READ_WITHIN_BOUND",
"CANCELLED_WITHOUT_READ",
"REJECTED_LIMIT",
"NO_RESPONSE",
]),
pulledBytes: z.number().int().nonnegative(),
ceiling: z.number().int().nonnegative(),
})
.strict(),
)
.min(1),
})
.strict(),
scope: z
.object({
start: z.literal("CURRENT"),
end: z.enum(["CURRENT", "STALE"]),
signal: z.enum(["ACTIVE", "ABORTED"]),
cancellationOwner: z.enum(["NONE", "CALLER", "SCOPE_FENCE", "DEADLINE"]),
})
.strict(),
})
.strict()
.superRefine((groups, context) => {
const fail = (path: (string | number)[], message: string) => {
context.addIssue({ code: "custom", path, message });
};
if (groups.status.final !== groups.status.attempts.at(-1)) {
fail(["status", "final"], "must equal the final attempt status");
}
if (groups.retry.count !== groups.retry.reasons.length) {
fail(["retry", "count"], "must equal retry reasons length");
}
if (groups.retry.count !== groups.fetch.count - 1) {
fail(["retry", "count"], "must equal the non-final fetch attempt count");
}
for (const [index, reason] of groups.retry.reasons.entries()) {
const status = groups.status.attempts[index];
const expectedReason =
status === "NETWORK_REJECTION"
? "NETWORK_FAILURE"
: status === 429
? "HTTP_429"
: status === 503
? "HTTP_503"
: null;
if (reason !== expectedReason) {
fail(
["retry", "reasons", index],
"must match the corresponding non-final attempt status",
);
}
}
if (groups.fetch.count !== groups.status.attempts.length) {
fail(["fetch", "count"], "must equal status attempts length");
}
if (groups.fetch.count !== groups.media.attempts.length) {
fail(["media", "attempts"], "must equal fetch count");
}
if (groups.fetch.count !== groups.body.attempts.length) {
fail(["body", "attempts"], "must equal fetch count");
}
if (groups.fetch.observerAttempts !== groups.fetch.count) {
fail(["fetch", "observerAttempts"], "must equal physical fetch count");
}
if (!groups.fetch.agrees) {
fail(["fetch", "agrees"], "must prove physical/observer agreement");
}
if (groups.media.final !== groups.media.attempts.at(-1)) {
fail(["media", "final"], "must equal the final attempt media essence");
}
});
export type HttpScenarioAssertionGroups = Readonly<{
status: Readonly<{ attempts: readonly AttemptStatus[]; final: AttemptStatus }>;
outcome: Readonly<{ kind: string; detail: string | null }>;
effect: Readonly<{ outcome: string; observer: string }>;
retry: Readonly<{ count: number; reasons: readonly RetryReason[] }>;
fetch: Readonly<{
count: number;
observerAttempts: number;
agrees: boolean;
}>;
media: Readonly<{
attempts: readonly (string | null)[];
final: string | null;
}>;
body: Readonly<{
attempts: readonly Readonly<{
disposition: BodyDisposition;
pulledBytes: number;
ceiling: number;
}>[];
}>;
scope: Readonly<{
start: "CURRENT";
end: "CURRENT" | "STALE";
signal: "ACTIVE" | "ABORTED";
cancellationOwner: "NONE" | "CALLER" | "SCOPE_FENCE" | "DEADLINE";
}>;
}>;
export const httpScenarioExpectationSchema = z
.object({
executionId: z.string().min(1),
operationId: z.string().min(1),
scenarioId: z.string().min(1),
expected: httpScenarioAssertionGroupsSchema,
testDeadlineOverrideMs: z.number().int().positive().nullable(),
})
.strict()
.superRefine((entry, context) => {
if (entry.executionId !== `${entry.operationId}::${entry.scenarioId}`) {
context.addIssue({
code: "custom",
path: ["executionId"],
message: "must equal operationId::scenarioId",
});
}
});
export type HttpScenarioExpectation = Readonly<{
executionId: string;
operationId: string;
scenarioId: string;
expected: HttpScenarioAssertionGroups;
testDeadlineOverrideMs: number | null;
}>;
export const httpScenarioReceiptRowSchema = z
.object({
executionId: z.string().min(1),
expected: httpScenarioAssertionGroupsSchema,
observed: httpScenarioAssertionGroupsSchema,
testDeadlineOverrideMs: z.number().int().positive().nullable(),
})
.strict();
export const httpScenarioReceiptSchema = z
.object({
schemaVersion: z.number().int().positive(),
catalogDigest: z.string().regex(/^sha256:[0-9a-f]{64}$/),
catalogTotal: z.number().int().nonnegative(),
executedIds: z.array(z.string().min(1)),
rows: z.array(httpScenarioReceiptRowSchema),
})
.strict()
.superRefine((receipt, context) => {
const rowIds = receipt.rows.map((row) => row.executionId);
const sortedIds = [...receipt.executedIds].sort((left, right) =>
left.localeCompare(right),
);
const sortedRowIds = [...rowIds].sort((left, right) =>
left.localeCompare(right),
);
if (!sameScenarioJson(receipt.executedIds, sortedIds)) {
context.addIssue({
code: "custom",
path: ["executedIds"],
message: "must be sorted by execution ID",
});
}
if (!sameScenarioJson(rowIds, sortedRowIds)) {
context.addIssue({
code: "custom",
path: ["rows"],
message: "must be sorted by execution ID",
});
}
if (!sameScenarioJson(receipt.executedIds, rowIds)) {
context.addIssue({
code: "custom",
path: ["rows"],
message: "row IDs must exactly equal executed IDs",
});
}
});
export type HttpScenarioReceipt = Readonly<{
schemaVersion: number;
catalogDigest: string;
catalogTotal: number;
executedIds: readonly string[];
rows: readonly Readonly<{
executionId: string;
expected: HttpScenarioAssertionGroups;
observed: HttpScenarioAssertionGroups;
testDeadlineOverrideMs: number | null;
}>[];
}>;
export function stableScenarioJson(value: unknown): string {
const normalize = (candidate: unknown): unknown => {
if (Array.isArray(candidate)) return candidate.map(normalize);
if (candidate && typeof candidate === "object") {
return Object.fromEntries(
Object.entries(candidate as Readonly<Record<string, unknown>>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, nested]) => [key, normalize(nested)]),
);
}
return candidate;
};
return JSON.stringify(normalize(value));
}
export function sameScenarioJson(left: unknown, right: unknown): boolean {
return stableScenarioJson(left) === stableScenarioJson(right);
}
export function computeHttpScenarioCatalogDigest(
schemaVersion: number,
expectations: readonly HttpScenarioExpectation[],
): `sha256:${string}` {
const tuples = [...expectations]
.sort((left, right) => left.executionId.localeCompare(right.executionId))
.map((entry) => [
entry.executionId,
entry.expected.status,
entry.expected.outcome,
entry.expected.effect,
entry.expected.retry,
entry.expected.fetch,
entry.expected.media,
entry.expected.body,
entry.expected.scope,
entry.testDeadlineOverrideMs,
]);
return `sha256:${createHash("sha256")
.update(stableScenarioJson([schemaVersion, ...tuples]))
.digest("hex")}`;
}
+24
View File
@@ -0,0 +1,24 @@
import { z } from "zod";
/**
* Compile a checked-in JSON Schema and assert a concrete artifact against it.
* Conversion failures are fatal too: an unsupported or malformed schema must
* not silently turn a release schema into documentation-only metadata.
*/
export function assertMatchesJsonSchema(
schemaDocument: unknown,
value: unknown,
label: string,
): void {
try {
const schema = z.fromJSONSchema(schemaDocument as never);
const result = schema.safeParse(value);
if (!result.success) {
throw new TypeError(z.prettifyError(result.error));
}
} catch (error) {
throw new TypeError(`${label} does not satisfy its checked-in JSON Schema.`, {
cause: error,
});
}
}
+206
View File
@@ -0,0 +1,206 @@
import {
dependencyDiffArtifactSchema,
dependencyInventoryArtifactSchema,
licenseReportArtifactSchema,
supplyChainVerificationArtifactSchema,
vulnerabilityReportArtifactSchema,
} from "../contracts/release-artifacts.ts";
import type { DistOutput } from "./release-candidate.ts";
import {
diffDependencyInventories,
supplyChainDigest,
validateDependencyReview,
validateLicensePolicy,
} from "./supply-chain.ts";
type Document = Record<string, unknown>;
export const LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS = Object.freeze({
promotionStatus: "FAIL_UNVERIFIED" as const,
vulnerabilityStatus: "FAIL_UNVERIFIED" as const,
provenanceAttestationStatus: "FAIL_UNVERIFIED" as const,
});
export function verifyLocalSupplyChainDefaults(stored: unknown): string[] {
const parsed = supplyChainVerificationArtifactSchema.safeParse(stored);
if (
!parsed.success ||
parsed.data.promotionStatus !==
LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS.promotionStatus ||
parsed.data.vulnerabilityStatus !==
LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS.vulnerabilityStatus ||
parsed.data.provenanceAttestationStatus !==
LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS.provenanceAttestationStatus
) {
return [
"supply-chain verification provider defaults are not local FAIL_UNVERIFIED",
];
}
return [];
}
export function createLocalVulnerabilityReport(lockfileSha256: string) {
return vulnerabilityReportArtifactSchema.parse({
schemaVersion: 1,
provider: "UNCONFIGURED",
scannedLockfileSha256: lockfileSha256,
status: "FAIL_UNVERIFIED",
findings: [],
exceptionsApplied: [],
failures: ["external vulnerability provider report is missing"],
blocking: [],
});
}
export function compareStoredLocalVulnerabilityReport(
lockfileSha256: string,
stored: unknown,
): string[] {
const parsed = vulnerabilityReportArtifactSchema.safeParse(stored);
if (
!parsed.success ||
supplyChainDigest(parsed.data) !==
supplyChainDigest(createLocalVulnerabilityReport(lockfileSha256))
) {
return [
"local vulnerability report does not match exact unconfigured defaults",
];
}
return [];
}
export function recomputeDependencyEvidence(input: Readonly<{
inventory: unknown;
baseline: unknown;
baselineApproval: unknown;
dependencyChangeEvidence: unknown;
skipBaseline?: boolean;
}>) {
const inventory = dependencyInventoryArtifactSchema.parse(input.inventory);
const baseline = asDocument(input.baseline);
const approval = asDocument(input.baselineApproval);
const dependencyChangeEvidence = asDocument(input.dependencyChangeEvidence);
const failures: string[] = [];
const skipBaseline = input.skipBaseline === true;
let diff: ReturnType<typeof diffDependencyInventories> = Object.freeze({
added: Object.freeze([]),
removed: Object.freeze([]),
changed: Object.freeze([]),
upgrades: Object.freeze([]),
});
let review: ReturnType<typeof validateDependencyReview> = Object.freeze({
passed: skipBaseline,
highRisk: Object.freeze([]),
failures: Object.freeze(
skipBaseline ? [] : ["dependency baseline unavailable"],
),
});
const baselineDigest = baseline ? supplyChainDigest(baseline) : null;
if (baseline && approval) {
diff = diffDependencyInventories(baseline, inventory);
review = validateDependencyReview(
diff,
inventory,
dependencyChangeEvidence ?? {},
);
if (
approval.schemaVersion !== 1 ||
approval.snapshotDigest !== baselineDigest ||
typeof approval.owner !== "string" ||
approval.owner.trim().length === 0
) {
failures.push("dependency baseline approval digest mismatch");
}
} else if (!skipBaseline) {
failures.push("dependency baseline and approval are required");
}
failures.push(...review.failures);
const report = dependencyDiffArtifactSchema.parse({
schemaVersion: 2,
baselineDigest,
currentDigest: supplyChainDigest(inventory),
...diff,
highRisk: review.highRisk,
reviewFailures: review.failures,
});
return Object.freeze({
report,
dependencyDiff: diff,
highRisk: review.highRisk,
failures: Object.freeze(failures),
});
}
export function compareStoredDependencyEvidence(
recomputed: ReturnType<typeof recomputeDependencyEvidence>,
stored: unknown,
): string[] {
const parsed = dependencyDiffArtifactSchema.safeParse(stored);
if (
!parsed.success ||
supplyChainDigest(parsed.data) !== supplyChainDigest(recomputed.report)
) {
return ["stored dependency diff does not match recomputed policy evidence"];
}
return [...recomputed.failures];
}
export function recomputeLicenseEvidence(input: Readonly<{
inventory: unknown;
policy: unknown;
}>) {
const inventory = dependencyInventoryArtifactSchema.parse(input.inventory);
const policy = asDocument(input.policy) ?? {};
const result = validateLicensePolicy(inventory, policy);
const report = licenseReportArtifactSchema.parse({
schemaVersion: 1,
status: result.passed ? "PASS" : "FAIL",
dependencyCount: inventory.dependencyCount,
results: result.results,
failures: result.failures,
});
return Object.freeze({
report,
failures: result.failures,
});
}
export function compareStoredLicenseEvidence(
recomputed: ReturnType<typeof recomputeLicenseEvidence>,
stored: unknown,
): string[] {
const parsed = licenseReportArtifactSchema.safeParse(stored);
if (
!parsed.success ||
supplyChainDigest(parsed.data) !== supplyChainDigest(recomputed.report)
) {
return ["stored license report does not match recomputed policy evidence"];
}
return [...recomputed.failures];
}
export function distChecksumsText(outputs: readonly DistOutput[]): string {
return `${[...outputs]
.sort((left, right) => left.path.localeCompare(right.path))
.map((output) => `${output.sha256} ${output.path}`)
.join("\n")}\n`;
}
export function verifyStoredDistChecksums(
outputs: readonly DistOutput[],
stored: string,
): string[] {
return stored === distChecksumsText(outputs)
? []
: ["stored dist checksums do not match current outputs"];
}
function asDocument(value: unknown): Document | null {
return value !== null &&
typeof value === "object" &&
!Array.isArray(value)
? (value as Document)
: null;
}
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
export const MANUAL_A11Y_ROUTE_IDS = Object.freeze(
Object.values(ROUTE_REGISTRY).map((route) => route.routeId),
);
const REVIEW_FIELDS = Object.freeze([
"M1 Keyboard",
"M2 Visible focus",
"M3 Route focus",
"M4 Modal focus",
"M5 Error association",
"M6 Color signal",
"M7 Reduced motion",
"Screen reader",
]);
export function validateManualA11yEvidence(content: string) {
const fields = Object.fromEntries(
content
.split(/\r?\n/)
.map((line) => /^([^:]+):\s*(.*)$/.exec(line))
.filter((match): match is RegExpExecArray => match !== null)
.map((match) => [
match[1].trim(),
match[2].trim(),
]),
);
const failures: string[] = [];
if (fields.Status !== "reviewed") failures.push("Status");
if (!fields["Route ID"]) failures.push("Route ID");
if (!fields["Release ID"]) failures.push("Release ID");
if (!fields.Reviewer) failures.push("Reviewer");
if (!fields.Signature) failures.push("Signature");
if (fields.Attestation !== "accepted") failures.push("Attestation");
if (
!fields["Reviewed at"] ||
!Number.isFinite(Date.parse(fields["Reviewed at"]))
) {
failures.push("Reviewed at");
}
for (const field of REVIEW_FIELDS) {
const result = fields[field];
if (
result !== "pass" &&
!/^not-applicable \(.+\)$/.test(result ?? "")
) {
failures.push(field);
}
}
return Object.freeze({
fields: Object.freeze(fields),
failures: Object.freeze(failures),
passed: failures.length === 0,
});
}
+373
View File
@@ -0,0 +1,373 @@
import { createHash } from "node:crypto";
import { lstat, readdir, realpath } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { gzipSync } from "node:zlib";
import {
build,
normalizePath,
type Plugin,
version as viteVersion,
} from "vite";
const RECIPE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const SOURCE_EXTENSION = /\.(?:[cm]?[jt]s|[jt]sx)$/;
const DECLARATION_FILE = /\.d\.[cm]?ts$/;
type EmittedOutput =
| Readonly<{
type: "chunk";
fileName: string;
code: string;
}>
| Readonly<{
type: "asset";
fileName: string;
source: string | Uint8Array;
}>;
export type OptionalRecipeBundleOutput = Readonly<{
fileName: string;
bytes: number;
gzipBytes: number;
sha256: string;
}>;
export type OptionalRecipeBundleMeasurement = Readonly<{
recipeId: string;
sourceRoots: readonly string[];
sourceFileCount: number;
toolchain: Readonly<{
bundler: "vite";
viteVersion: string;
mode: "production";
target: "es2022";
format: "es";
minifier: "esbuild";
treeshake: false;
compression: "node-zlib-gzip";
}>;
outputs: readonly OptionalRecipeBundleOutput[];
bytes: number;
gzipBytes: number;
bundleBudgetGzipBytes: number;
remainingGzipBytes: number;
sha256: string;
passed: boolean;
}>;
/**
* Builds an uncomposed reference runtime as a synthetic production consumer.
* Every catalog-owned source module is exposed as an entry namespace and
* tree-shaking is disabled so internal fail-closed paths remain in the budget.
*/
export async function measureOptionalRecipeBundle(input: Readonly<{
recipeId: string;
sourceRoots: readonly string[];
bundleBudgetGzipBytes: number;
workspaceRoot?: string;
}>): Promise<OptionalRecipeBundleMeasurement> {
const recipeId = validateRecipeId(input.recipeId);
const bundleBudgetGzipBytes = positiveSafeInteger(
input.bundleBudgetGzipBytes,
"Optional recipe bundle budget",
);
const workspaceRoot = await realpath(
path.resolve(input.workspaceRoot ?? process.cwd()),
);
const sourceRoots = validateSourceRoots(input.sourceRoots);
const sourceFiles = await resolveSourceFiles(
workspaceRoot,
sourceRoots,
);
const virtualEntry =
`virtual:optional-reference-runtime-entry/${recipeId}`;
const resolvedVirtualEntry = `\0${virtualEntry}`;
const entrySource = sourceFiles
.map(
(sourceFile, index) =>
`export * as source${index} from ${JSON.stringify(
viteSourceSpecifier(sourceFile),
)};`,
)
.join("\n")
.concat("\n");
const preservePublicEntryPlugin = {
name: "optional-reference-runtime-entry",
enforce: "pre",
resolveId(id) {
return id === virtualEntry ? resolvedVirtualEntry : null;
},
load(id) {
return id === resolvedVirtualEntry ? entrySource : null;
},
options(options) {
return {
...options,
preserveEntrySignatures: "strict",
};
},
} satisfies Plugin;
const buildResult = await build({
root: workspaceRoot,
configFile: false,
envFile: false,
mode: "production",
publicDir: false,
clearScreen: false,
logLevel: "silent",
plugins: [preservePublicEntryPlugin],
build: {
target: "es2022",
minify: "esbuild",
sourcemap: false,
write: false,
emptyOutDir: false,
copyPublicDir: false,
cssCodeSplit: false,
reportCompressedSize: false,
rollupOptions: {
input: virtualEntry,
// Budget the complete selected runtime, including internal fail-closed
// guards that a synthetic consumer cannot predict it will exercise.
treeshake: false,
output: {
format: "es",
entryFileNames: `${recipeId}.js`,
chunkFileNames: `${recipeId}-chunk-[hash].js`,
assetFileNames: `${recipeId}-asset-[name]-[hash][extname]`,
},
},
},
});
const emitted = emittedOutputs(buildResult);
if (emitted.length === 0) {
throw new TypeError("Optional recipe bundle emitted no output.");
}
const outputs = emitted
.map((output) => {
const bytes = outputBytes(output);
return Object.freeze({
fileName: output.fileName,
bytes: bytes.byteLength,
gzipBytes: gzipSync(bytes).byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
});
})
.sort((left, right) => compareText(left.fileName, right.fileName));
const aggregateHash = createHash("sha256");
for (const output of outputs) {
aggregateHash.update(output.fileName);
aggregateHash.update("\0");
aggregateHash.update(output.sha256);
aggregateHash.update("\0");
}
const bytes = outputs.reduce(
(total, output) => total + output.bytes,
0,
);
const gzipBytes = outputs.reduce(
(total, output) => total + output.gzipBytes,
0,
);
return Object.freeze({
recipeId,
sourceRoots: Object.freeze([...sourceRoots]),
sourceFileCount: sourceFiles.length,
toolchain: Object.freeze({
bundler: "vite" as const,
viteVersion,
mode: "production" as const,
target: "es2022" as const,
format: "es" as const,
minifier: "esbuild" as const,
treeshake: false as const,
compression: "node-zlib-gzip" as const,
}),
outputs: Object.freeze(outputs),
bytes,
gzipBytes,
bundleBudgetGzipBytes,
remainingGzipBytes: bundleBudgetGzipBytes - gzipBytes,
sha256: aggregateHash.digest("hex"),
passed: gzipBytes <= bundleBudgetGzipBytes,
});
}
async function resolveSourceFiles(
workspaceRoot: string,
sourceRoots: readonly string[],
): Promise<readonly string[]> {
const sourceBoundary = await realpath(path.join(workspaceRoot, "src"));
const discovered: string[] = [];
for (const sourceRoot of sourceRoots) {
const target = path.resolve(workspaceRoot, sourceRoot);
assertInsideSourceBoundary(target, sourceBoundary);
const rootMetadata = await lstat(target);
if (rootMetadata.isSymbolicLink()) {
throw new TypeError("Optional recipe source root cannot be a symlink.");
}
assertInsideSourceBoundary(await realpath(target), sourceBoundary);
if (
rootMetadata.isFile() &&
(!SOURCE_EXTENSION.test(target) || DECLARATION_FILE.test(target))
) {
throw new TypeError("Optional recipe source root is not executable source.");
}
discovered.push(
...(await collectExecutableSources(target, sourceBoundary)),
);
}
const unique = [...new Set(discovered)].sort((left, right) =>
compareText(
normalizePath(path.relative(workspaceRoot, left)),
normalizePath(path.relative(workspaceRoot, right)),
),
);
if (unique.length === 0) {
throw new TypeError("Optional recipe source roots contain no executable source.");
}
return Object.freeze(unique);
}
async function collectExecutableSources(
target: string,
sourceBoundary: string,
): Promise<string[]> {
const metadata = await lstat(target);
if (metadata.isSymbolicLink()) {
throw new TypeError("Optional recipe source cannot be a symlink.");
}
assertInsideSourceBoundary(target, sourceBoundary);
if (metadata.isFile()) {
return SOURCE_EXTENSION.test(target) && !DECLARATION_FILE.test(target)
? [target]
: [];
}
if (!metadata.isDirectory()) return [];
const entries = (await readdir(target, { withFileTypes: true })).sort(
(left, right) => compareText(left.name, right.name),
);
const groups = await Promise.all(
entries.map((entry) =>
collectExecutableSources(
path.join(target, entry.name),
sourceBoundary,
),
),
);
return groups.flat();
}
function emittedOutputs(value: unknown): readonly EmittedOutput[] {
const buildOutputs = Array.isArray(value) ? value : [value];
const emitted: EmittedOutput[] = [];
for (const buildOutput of buildOutputs) {
if (!isRecord(buildOutput) || !Array.isArray(buildOutput.output)) {
throw new TypeError("Optional recipe bundle output is invalid.");
}
for (const output of buildOutput.output) {
if (!isRecord(output)) {
throw new TypeError("Optional recipe emitted output is invalid.");
}
if (
output.type === "chunk" &&
typeof output.fileName === "string" &&
typeof output.code === "string"
) {
emitted.push({
type: "chunk",
fileName: output.fileName,
code: output.code,
});
} else if (
output.type === "asset" &&
typeof output.fileName === "string" &&
(typeof output.source === "string" ||
output.source instanceof Uint8Array)
) {
emitted.push({
type: "asset",
fileName: output.fileName,
source: output.source,
});
} else {
throw new TypeError("Optional recipe emitted output shape is invalid.");
}
}
}
return emitted;
}
function outputBytes(output: EmittedOutput): Buffer {
if (output.type === "chunk") {
return Buffer.from(output.code, "utf8");
}
return Buffer.from(output.source);
}
function validateRecipeId(value: unknown): string {
if (typeof value !== "string" || !RECIPE_ID.test(value)) {
throw new TypeError("Optional recipe ID is invalid.");
}
return value;
}
function validateSourceRoots(value: unknown): readonly string[] {
if (
!Array.isArray(value) ||
value.length === 0 ||
value.length > 32 ||
value.some(
(sourceRoot) =>
typeof sourceRoot !== "string" ||
!sourceRoot.startsWith("src/") ||
sourceRoot.includes("\\") ||
sourceRoot
.split("/")
.some(
(segment) =>
segment.length === 0 || segment === "." || segment === "..",
),
) ||
new Set(value).size !== value.length
) {
throw new TypeError("Optional recipe source roots are invalid.");
}
return Object.freeze([...value].sort(compareText));
}
function assertInsideSourceBoundary(
target: string,
sourceBoundary: string,
): void {
const relative = path.relative(sourceBoundary, target);
if (
relative === ".." ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
throw new TypeError("Optional recipe source escaped the source boundary.");
}
}
function viteSourceSpecifier(sourceFile: string): string {
return pathToFileURL(sourceFile).href;
}
function positiveSafeInteger(value: unknown, name: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 1) {
throw new TypeError(`${name} is invalid.`);
}
return value as number;
}
function compareText(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
+441
View File
@@ -0,0 +1,441 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
export const REQUIRED_RECIPE_IDS = Object.freeze([
"analytics-error-sink",
"browser-permission",
"client-workflow",
"feature-flag",
"file-transfer",
"generated-api",
"large-data-ui",
"multi-tab",
"offline-indexeddb",
"realtime",
"service-worker-pwa",
"web-worker",
] as const);
const lifecycleRecipes: ReadonlySet<string> = new Set([
"analytics-error-sink",
"browser-permission",
"client-workflow",
"file-transfer",
"generated-api",
"multi-tab",
"offline-indexeddb",
"realtime",
"service-worker-pwa",
"web-worker",
]);
type Document = Readonly<Record<string, unknown>>;
export type OptionalRecipeSourceViolation = Readonly<{
ruleId: string;
path: string;
}>;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function recordValue(value: unknown): Record<string, unknown> {
return isRecord(value) ? value : {};
}
function recordRows(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter(isRecord) : [];
}
function nonEmptyStrings(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every(
(entry): entry is string =>
typeof entry === "string" && entry.trim().length > 0,
)
);
}
function packageVersions(value: unknown): Record<string, string> {
return Object.fromEntries(
Object.entries(recordValue(value)).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
}
export function validateRecipeCatalog(
input: unknown,
packageDocument: Document,
): string[] {
const document = recordValue(input);
const violations: string[] = [];
if (document.schemaVersion !== 1) violations.push("CATALOG_SCHEMA_VERSION");
if (document.decisionId !== "VD-10") violations.push("CATALOG_DECISION");
if (document.defaultStatus !== "NOT_INSTALLED") {
violations.push("CATALOG_DEFAULT_MUST_BE_NOT_INSTALLED");
}
if (
!Array.isArray(document.productionRuntimeDependencies) ||
document.productionRuntimeDependencies.length > 0
) {
violations.push("UNSELECTED_RUNTIME_DEPENDENCY");
}
if (!nonEmptyStrings(document.vendorPackagePatterns)) {
violations.push("VENDOR_PATTERN_CATALOG");
}
if (!Array.isArray(document.recipes)) {
return [...violations, "RECIPE_CATALOG_MISSING"];
}
const recipes = recordRows(document.recipes);
const actualIds = recipes.map((recipe) => String(recipe.id ?? "")).sort();
if (JSON.stringify(actualIds) !== JSON.stringify(REQUIRED_RECIPE_IDS)) {
violations.push("RECIPE_ID_SET");
}
if (new Set(actualIds).size !== actualIds.length) {
violations.push("RECIPE_ID_DUPLICATE");
}
for (const recipe of recipes) {
const id = typeof recipe.id === "string" ? recipe.id : "unknown";
if (recipe.status !== "RECIPE_AVAILABLE") {
violations.push(`${id}:STATUS_MUST_NOT_CLAIM_INSTALLED`);
}
for (const field of [
"trigger",
"boundary",
"port",
"fake",
"owner",
"fallback",
"serverStatePolicy",
] as const) {
const value = recipe[field];
if (typeof value !== "string" || value.trim().length === 0) {
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
}
}
for (const field of [
"forbiddenWhen",
"failureKinds",
"securityPrivacy",
"removal",
] as const) {
if (!nonEmptyStrings(recipe[field])) {
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
}
}
if (
typeof recipe.bundleBudgetGzipBytes !== "number" ||
!Number.isInteger(recipe.bundleBudgetGzipBytes) ||
recipe.bundleBudgetGzipBytes < 1
) {
violations.push(`${id}:INVALID_BUNDLE_BUDGET`);
}
if (recipe.owner === "frontend-platform") {
violations.push(`${id}:PROJECT_OWNER_NOT_ASSIGNED`);
}
if (lifecycleRecipes.has(id) && !nonEmptyStrings(recipe.lifecycleMethods)) {
violations.push(`${id}:CLEANUP_CONTRACT_MISSING`);
}
if (
id === "client-workflow" &&
recipe.serverStatePolicy !== "reference-only"
) {
violations.push(`${id}:SERVER_STATE_DUPLICATION_POLICY`);
}
}
const dependencies = {
...packageVersions(packageDocument.dependencies),
...packageVersions(packageDocument.devDependencies),
};
const packageScripts = packageVersions(packageDocument.scripts);
for (const recipe of recipes) {
if (recipe.referenceRuntime === undefined) continue;
const runtime = recordValue(recipe.referenceRuntime);
const id = typeof recipe.id === "string" ? recipe.id : "unknown";
if (
runtime.status !== "AVAILABLE_NOT_COMPOSED" ||
runtime.productionComposition !== false
) {
violations.push(`${id}:REFERENCE_RUNTIME_COMPOSITION`);
}
if (!nonEmptyStrings(runtime.sourceRoots)) {
violations.push(`${id}:REFERENCE_RUNTIME_SOURCE_ROOTS`);
} else if (
runtime.sourceRoots.some(
(sourceRoot) =>
!sourceRoot.startsWith("src/") ||
sourceRoot.includes("\\") ||
sourceRoot.split("/").includes(".."),
)
) {
violations.push(`${id}:REFERENCE_RUNTIME_SOURCE_BOUNDARY`);
}
if (!nonEmptyStrings(runtime.coveredCapabilities)) {
violations.push(`${id}:REFERENCE_RUNTIME_CAPABILITIES`);
}
if (!nonEmptyStrings(runtime.conformanceScripts)) {
violations.push(`${id}:REFERENCE_RUNTIME_CONFORMANCE`);
} else {
for (const script of runtime.conformanceScripts) {
if (!(script in packageScripts)) {
violations.push(`${id}:UNKNOWN_CONFORMANCE_SCRIPT:${script}`);
}
}
}
}
const vendorPatterns = Array.isArray(document.vendorPackagePatterns)
? document.vendorPackagePatterns.filter(
(entry): entry is string => typeof entry === "string",
)
: [];
for (const pattern of vendorPatterns) {
const wildcard = pattern.endsWith("*");
const prefix = pattern.replace(/\/?\*$/, "");
if (
Object.keys(dependencies).some(
(dependency) =>
dependency === prefix ||
dependency.startsWith(`${prefix}/`) ||
(wildcard && dependency.startsWith(prefix)),
)
) {
violations.push(`UNSELECTED_VENDOR_INSTALLED:${prefix}`);
}
}
return violations;
}
export async function sourceFiles(directory: string): Promise<string[]> {
let entries;
try {
entries = await readdir(directory, { withFileTypes: true });
} catch (error: unknown) {
if (
isRecord(error) &&
"code" in error &&
error.code === "ENOENT"
) {
return [];
}
throw error;
}
const groups: string[][] = await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory()
? sourceFiles(target)
: /\.(?:[cm]?[jt]s|[jt]sx)$/.test(entry.name)
? [target]
: [];
}),
);
return groups.flat();
}
export async function scanOptionalRecipeSources(
root: string,
{ scanProductionBoundary = true }: Readonly<{
scanProductionBoundary?: boolean;
}> = {},
): Promise<OptionalRecipeSourceViolation[]> {
const violations: OptionalRecipeSourceViolation[] = [];
for (const file of await sourceFiles(root)) {
const relative = path.relative(process.cwd(), file).replaceAll("\\", "/");
const relativeToRoot = path.relative(root, file).replaceAll("\\", "/");
const content = await readFile(file, "utf8");
const imports = [
...content.matchAll(/(?:from\s*|import\s*\(\s*)["']([^"']+)["']/g),
]
.map((match) => match[1])
.filter((specifier): specifier is string => specifier !== undefined);
if (
scanProductionBoundary &&
(relativeToRoot.startsWith("src/") ||
(path.basename(path.resolve(root)) === "src" &&
!relativeToRoot.startsWith(".."))) &&
imports.some((specifier) =>
/(?:^|\/)recipes\/frontend-capabilities(?:\/|$)/.test(specifier),
)
) {
violations.push({ ruleId: "PRODUCTION_IMPORTS_RECIPE", path: relative });
}
const productionRelative =
path.basename(path.resolve(root)) === "src"
? `src/${relativeToRoot}`
: relativeToRoot;
const isCompositionSource =
/(?:^|\/)src\/bootstrap\//.test(productionRelative) ||
/(?:^|\/)src\/features\/installed-feature-(?:adapters|runtimes)\./.test(
productionRelative,
);
if (
scanProductionBoundary &&
isCompositionSource &&
(imports.some((specifier) =>
/(?:^|\/)adapters\/(?:browser-file-storage|browser-files|browser-transfer|cache-storage|storage\/(?:indexeddb|opfs))(?:\/|$)/.test(
specifier,
),
) ||
/["'][^"'\r\n]*(?:^|\/)adapters\/(?:browser-file-storage|browser-files|browser-transfer|cache-storage|storage\/(?:indexeddb|opfs))(?:\/|["'])/u.test(
content,
))
) {
violations.push({
ruleId: "REFERENCE_RUNTIME_COMPOSED_WITHOUT_SELECTION",
path: relative,
});
}
const localVendorAdapter =
relative.includes("recipes/") && relative.includes("/adapters/");
if (
!localVendorAdapter &&
imports.some((specifier) =>
/^(?:@launchdarkly\/|@sentry\/|@opentelemetry\/|@openapitools\/openapi-generator-cli$|@reduxjs\/toolkit$|@tanstack\/react-virtual$|@uppy\/|firebase(?:\/|$)|idb$|react-window$|redux(?:\/|$)|socket\.io-client$|tus-js-client$|workbox-window$|xstate$|zustand$)/.test(
specifier,
),
)
) {
violations.push({ ruleId: "VENDOR_IMPORT_OUTSIDE_ADAPTER", path: relative });
}
if (
/localStorage\s*\.\s*(?:setItem|getItem)\s*\([^)]*(?:credential|password|secret|token)/is.test(
content,
) ||
/searchParams\s*\.\s*set\s*\(\s*["'](?:credential|password|secret|token)/is.test(
content,
) ||
/(?:record|track|emit)\s*\(\s*\{[\s\S]{0,400}(?:credential|password|secret|token)\s*:/i.test(
content,
)
) {
violations.push({ ruleId: "CREDENTIAL_LEAK_PATH", path: relative });
}
if (
/(?:createStore|configureStore|create\s*\()\s*\([\s\S]{0,600}(?:apiResponse|queryData|serverState)\s*:/i.test(
content,
)
) {
violations.push({
ruleId: "CLIENT_STORE_DUPLICATES_SERVER_STATE",
path: relative,
});
}
}
return violations;
}
export async function scanProductionBundle(
distRoot: string,
): Promise<string[]> {
const violations: string[] = [];
const forbiddenRuntimeMarkers = [
"frontend-optional-recipe-must-not-reach-production",
"Browser file runtime hard limits are invalid.",
"Object URL allocation failed",
"Storage pressure policy is invalid.",
"IndexedDB runtime configuration is invalid.",
"Invalid IndexedDB schema migration.",
"OPFS runtime policy is invalid.",
"OPFS operation failed.",
"Public Cache Storage policy is invalid.",
"Public cache validation failed.",
"Presigned capability vault limit is invalid.",
"Resumable upload policy is invalid.",
"Image CDN policy registry is invalid.",
] as const;
for (const file of await sourceFiles(distRoot)) {
const content = await readFile(file, "utf8");
if (forbiddenRuntimeMarkers.some((marker) => content.includes(marker))) {
violations.push(path.relative(process.cwd(), file));
}
}
const viteManifestPath = path.join(distRoot, ".vite/manifest.json");
const emittedModuleInventoryPath = path.join(
distRoot,
".vite/module-inventory.json",
);
const moduleInventoryCandidates = [
emittedModuleInventoryPath,
...(path.resolve(distRoot) === path.resolve("dist")
? ["artifacts/quality/vite-module-inventory.json"]
: []),
];
const viteManifestExists = await readFile(viteManifestPath, "utf8")
.then(() => true)
.catch((error: unknown) => {
if (isRecord(error) && error.code === "ENOENT") return false;
throw error;
});
if (!viteManifestExists) return [...new Set(violations)];
let inventory: unknown;
let moduleInventoryPath = emittedModuleInventoryPath;
for (const candidate of moduleInventoryCandidates) {
try {
inventory = JSON.parse(await readFile(candidate, "utf8"));
moduleInventoryPath = candidate;
break;
} catch {
// A generated build may move the inventory out of the deploy directory.
}
}
if (inventory === undefined) {
violations.push(
path.relative(process.cwd(), moduleInventoryPath),
);
return [...new Set(violations)];
}
const inventoryDocument = recordValue(inventory);
const chunks = recordRows(inventoryDocument.chunks);
if (
inventoryDocument.schemaVersion !== 1 ||
!Array.isArray(inventoryDocument.chunks) ||
chunks.length !== inventoryDocument.chunks.length
) {
violations.push(path.relative(process.cwd(), moduleInventoryPath));
return [...new Set(violations)];
}
const forbiddenSourcePrefixes = [
"src/application/ports/browser-file-storage/",
"src/application/ports/browser-transfer/",
"src/adapters/browser-file-storage/",
"src/adapters/browser-files/",
"src/adapters/browser-transfer/",
"src/adapters/cache-storage/",
"src/adapters/storage/indexeddb/",
"src/adapters/storage/opfs/",
] as const;
for (const chunk of chunks) {
if (
typeof chunk.fileName !== "string" ||
!Array.isArray(chunk.modules) ||
chunk.modules.some((moduleId) => typeof moduleId !== "string")
) {
violations.push(path.relative(process.cwd(), moduleInventoryPath));
continue;
}
for (const moduleId of chunk.modules as string[]) {
if (
forbiddenSourcePrefixes.some((prefix) =>
moduleId.startsWith(prefix),
)
) {
violations.push(`${chunk.fileName}:${moduleId}`);
}
}
}
return [...new Set(violations)];
}
+994
View File
@@ -0,0 +1,994 @@
type PackageManager = "pnpm" | "npm" | "yarn";
type ManagerParseResult = Readonly<{
dependencies: readonly string[];
unsafeLifecycle: boolean;
unsupportedManagerSyntax: boolean;
}>;
type SuppressionState = {
effective: boolean | undefined;
contradictory: boolean;
malformed: boolean;
};
type ShellNpmScopeEnvironmentState = {
autoExport: boolean;
forbidden: boolean;
uncertain: boolean;
};
type ShellCommandPrefix = Readonly<{
assignments: readonly Readonly<{
dynamicName: boolean;
name: string | null;
}>[];
commandIndex: number;
uncertain: boolean;
}>;
type EnvironmentCommandPrefix = Readonly<{
assignmentNames: readonly string[];
uncertain: boolean;
}>;
type TokenizedShellSegment = Readonly<{
tokens: readonly string[];
expansionTokens: readonly boolean[];
}>;
const managerNames = new Set<PackageManager>(["pnpm", "npm", "yarn"]);
const managerOptionsWithValue = new Set([
"-C", "--cache", "--cache-folder", "--config-dir", "--cwd", "--dir", "--filter",
"--global-dir", "--globalconfig", "--home", "--lockfile-dir", "--mutex", "--prefix",
"--registry", "--store-dir", "--userconfig", "--workspace", "--workspace-dir",
]);
const managerBooleanOptions = new Set([
"--color", "--global", "--no-color", "--offline", "--prefer-offline", "--silent",
"--use-stderr", "--verbose", "-g", "-s",
]);
const npmScriptDispatchBooleanOptions = new Set([
"--foreground-scripts", "--if-present", "--ignore-scripts",
]);
const npmScriptDispatchScopeOptions = new Set([
"--prefix", "--workspace", "--workspaces",
]);
const npmDispatchScopeEnvironmentNames = new Set([
"npm_config_globalconfig", "npm_config_prefix", "npm_config_userconfig",
"npm_config_workspace", "npm_config_workspaces",
]);
const npmIndirectConfigAuthorityOptions = new Set([
"--globalconfig", "--userconfig",
]);
const manifestScopeOptions: Readonly<Record<PackageManager, ReadonlySet<string>>> = {
pnpm: new Set(["-C", "--dir", "--filter", "--workspace-dir"]),
npm: new Set(["--prefix", "--workspace"]),
yarn: new Set(["--cwd"]),
};
const lifecycleMutationCommands = new Set([
"add", "ci", "dedupe", "i", "install", "link", "pack", "prune", "publish",
"rebuild", "remove", "rm", "uninstall", "unlink", "up", "update", "upgrade",
]);
const managerBuiltinAliases: Readonly<
Record<PackageManager, ReadonlyMap<string, string>>
> = {
pnpm: new Map([["ln", "link"]]),
npm: new Map(),
yarn: new Map(),
};
const unsupportedBuiltinDispatchers: Readonly<
Record<PackageManager, ReadonlySet<string>>
> = {
pnpm: new Set(["dlx", "exec"]),
npm: new Set(["exec"]),
yarn: new Set(["dlx", "exec", "workspace", "workspaces"]),
};
const lifecycleBooleanOptions: Readonly<
Record<PackageManager, ReadonlySet<string>>
> = {
pnpm: new Set([
"--dry-run", "--force", "--frozen-lockfile", "--lockfile-only",
"--no-optional", "--prefer-frozen-lockfile", "--recursive",
"--workspace-root", "-D", "-P", "-r", "-w",
]),
npm: new Set([
"--audit", "--dry-run", "--force", "--foreground-scripts", "--fund",
"--package-lock-only",
]),
yarn: new Set([
"--check-cache", "--frozen-lockfile", "--ignore-engines",
"--ignore-optional", "--immutable", "--immutable-cache", "--inline-builds",
"--no-lockfile", "--non-interactive", "--pure-lockfile",
]),
};
const lifecycleOptionsWithValue: Readonly<
Record<PackageManager, ReadonlySet<string>>
> = {
pnpm: new Set(["--child-concurrency", "--modules-dir", "--reporter"]),
npm: new Set(["--include", "--install-strategy", "--omit"]),
yarn: new Set(["--mode", "--modules-folder", "--production"]),
};
const knownBuiltinCommands: Readonly<Record<PackageManager, ReadonlySet<string>>> = {
pnpm: new Set([
"audit", "config", "deploy", "dlx", "exec", "fetch", "help", "list", "ls",
"outdated", "root", "server", "setup", "store", "view", "why",
]),
npm: new Set([
"access", "audit", "bugs", "cache", "completion", "config", "diff", "docs",
"doctor", "exec", "explore", "fund", "help", "help-search", "hook", "init",
"list", "login", "logout", "ls", "org", "outdated", "owner", "ping", "pkg",
"prefix", "profile", "query", "repo", "root", "search", "star", "stars",
"team", "token", "unstar", "version", "view", "whoami",
]),
yarn: new Set([
"cache", "config", "constraints", "dedupe", "dlx", "exec", "help", "info",
"npm", "plugin", "set", "stage", "version", "why",
]),
};
const exactBareSafeBuiltinCommands: Readonly<
Record<PackageManager, ReadonlySet<string>>
> = {
pnpm: new Set(["audit"]),
npm: new Set(["audit"]),
yarn: new Set(),
};
const npmImplicitScripts = new Set(["restart", "start", "stop", "test"]);
export function validatePackageScriptGraph(
scripts: Readonly<Record<string, string>>,
entryScript: string,
): string[] {
const failures: string[] = [];
const visiting = new Set<string>();
const visited = new Set<string>();
const stack: string[] = [];
const visit = (scriptName: string): void => {
if (visiting.has(scriptName)) {
const start = stack.indexOf(scriptName);
failures.push(`package script cycle: ${[...stack.slice(start), scriptName].join(" -> ")}`);
return;
}
if (visited.has(scriptName)) return;
const command = scripts[scriptName];
if (command === undefined) {
failures.push(`package script missing: ${scriptName}`);
return;
}
visiting.add(scriptName);
stack.push(scriptName);
if (/\bscripts\/run-ci-gate(?:\.[cm]?[jt]s)?\b/u.test(command)) {
failures.push(`${scriptName} must not invoke the CI gate runner`);
}
const parsed = parseManagerCommands(command, scripts);
if (parsed.unsupportedManagerSyntax) {
failures.push(`package script manager invocation is not safely parseable: ${scriptName}`);
}
for (const dependency of parsed.dependencies) {
if (dependency === "ci:gate") {
failures.push(`${scriptName} must not invoke ci:gate`);
} else if (!(dependency in scripts)) {
failures.push(`package script missing: ${scriptName} -> ${dependency}`);
} else {
visit(dependency);
}
}
stack.pop();
visiting.delete(scriptName);
visited.add(scriptName);
};
visit(entryScript);
return [...new Set(failures)];
}
export function validateInstallScriptPolicy(
scripts: Readonly<Record<string, string>>,
entryScripts: readonly string[],
): string[] {
const failures: string[] = [];
const visited = new Set<string>();
const visit = (scriptName: string): void => {
if (visited.has(scriptName)) return;
visited.add(scriptName);
const command = scripts[scriptName];
if (command === undefined) {
failures.push(`package script missing: ${scriptName}`);
return;
}
const parsed = parseManagerCommands(command, scripts);
if (parsed.unsafeLifecycle || parsed.unsupportedManagerSyntax) {
failures.push(
`install-bearing package script must use --ignore-scripts: ${scriptName}`,
);
}
for (const dependency of parsed.dependencies) {
if (dependency !== "ci:gate") visit(dependency);
}
};
for (const entryScript of entryScripts) visit(entryScript);
return [...new Set(failures)];
}
export function validateNpmScopeEnvironment(
environment: Readonly<Record<string, string | undefined>>,
): string[] {
return Object.keys(environment)
.filter((name) => npmDispatchScopeEnvironmentNames.has(name.toLowerCase()))
.map((name) => `npm scope environment is not allowed: ${name}`);
}
function parseManagerCommands(
command: string,
scripts: Readonly<Record<string, string>>,
): ManagerParseResult {
const tokenized = tokenizeShellSegments(command);
const dependencies: string[] = [];
let unsafeLifecycle = false;
let unsupportedManagerSyntax = false;
if (!tokenized) {
return Object.freeze({
dependencies: Object.freeze([]),
unsafeLifecycle: false,
unsupportedManagerSyntax: containsManagerReference(command),
});
}
unsupportedManagerSyntax ||= tokenized.unsupportedControl && containsManagerReference(command);
const npmScopeEnvironmentState: ShellNpmScopeEnvironmentState = {
autoExport: false,
forbidden: false,
uncertain: false,
};
for (const segment of tokenized.segments) {
const { tokens, expansionTokens } = segment;
updateShellNpmScopeEnvironmentState(segment, npmScopeEnvironmentState);
for (let index = 0; index < tokens.length; index += 1) {
const token = tokens[index]!;
if (token === "corepack") {
if (hasUnsafeManagerCommandPrefix(tokens, expansionTokens, index)) {
unsupportedManagerSyntax = true;
break;
}
const wrapped = tokens[index + 1];
if (!wrapped || !isPackageManager(wrapped)) {
unsupportedManagerSyntax = true;
break;
}
const parsed = parseManagerInvocation(
wrapped,
tokens,
expansionTokens,
index + 2,
scripts,
hasUnsafeNpmScopeEnvironment(
npmScopeEnvironmentState,
tokens,
expansionTokens,
index,
),
);
dependencies.push(...parsed.dependencies);
unsafeLifecycle ||= parsed.unsafeLifecycle;
unsupportedManagerSyntax ||= parsed.unsupportedManagerSyntax;
break;
}
if (isPackageManager(token)) {
if (hasUnsafeManagerCommandPrefix(tokens, expansionTokens, index)) {
unsupportedManagerSyntax = true;
break;
}
const parsed = parseManagerInvocation(
token,
tokens,
expansionTokens,
index + 1,
scripts,
hasUnsafeNpmScopeEnvironment(
npmScopeEnvironmentState,
tokens,
expansionTokens,
index,
),
);
dependencies.push(...parsed.dependencies);
unsafeLifecycle ||= parsed.unsafeLifecycle;
unsupportedManagerSyntax ||= parsed.unsupportedManagerSyntax;
break;
}
if (containsManagerReference(token)) {
unsupportedManagerSyntax = true;
break;
}
}
}
return Object.freeze({
dependencies: Object.freeze([...new Set(dependencies)]),
unsafeLifecycle,
unsupportedManagerSyntax,
});
}
function parseManagerInvocation(
manager: PackageManager,
tokens: readonly string[],
expansionTokens: readonly boolean[],
start: number,
scripts: Readonly<Record<string, string>>,
hasNpmScopeEnvironment: boolean,
): ManagerParseResult {
let cursor = start;
let changesManifestScope = false;
let hasNpmConfigAuthority = false;
let consumedManagerSyntax = false;
const suppression: SuppressionState = {
effective: undefined,
contradictory: false,
malformed: false,
};
while (cursor < tokens.length && tokens[cursor]!.startsWith("-")) {
consumedManagerSyntax = true;
const option = tokens[cursor]!;
const parsedSuppression = consumeSuppressionOption(
manager,
tokens,
cursor,
suppression,
);
if (parsedSuppression.recognized) {
if (parsedSuppression.unsupported) return unsupportedResult();
cursor = parsedSuppression.nextIndex;
continue;
}
const equals = option.indexOf("=");
const name = equals < 0 ? option : option.slice(0, equals);
if (managerOptionsWithValue.has(name)) {
changesManifestScope ||= manifestScopeOptions[manager].has(name);
hasNpmConfigAuthority ||=
manager === "npm" && npmIndirectConfigAuthorityOptions.has(name);
if (equals >= 0) {
if (option.slice(equals + 1).length === 0) return unsupportedResult();
} else {
cursor += 1;
if (cursor >= tokens.length || tokens[cursor]!.startsWith("-")) {
return unsupportedResult();
}
}
} else if (managerBooleanOptions.has(name)) {
if (equals >= 0 && !/^(?:true|false)$/u.test(option.slice(equals + 1))) {
return unsupportedResult();
}
} else if (option !== "--") {
return unsupportedResult();
}
cursor += 1;
}
const subcommand = tokens[cursor];
if (!subcommand) return unsupportedResult();
if (manager === "npm" && (hasNpmScopeEnvironment || hasNpmConfigAuthority)) {
return unsupportedResult();
}
const argumentsAfterCommand = tokens.slice(cursor + 1);
if (subcommand === "run" || subcommand === "run-script") {
const dependency = argumentsAfterCommand[0];
if (!dependency || dependency.startsWith("-")) return unsupportedResult();
if (changesManifestScope) return unsupportedResult();
if (
manager === "npm" &&
(expansionTokens.slice(start, cursor + 2).some(Boolean) ||
!areNpmScriptDispatchArgumentsSupported(
argumentsAfterCommand.slice(1),
expansionTokens.slice(cursor + 2),
suppression,
))
) {
return unsupportedResult();
}
return manager === "npm"
? npmScriptDependencyResult(dependency, scripts, suppression)
: dependencyResult(dependency);
}
const canonicalSubcommand = managerBuiltinAliases[manager].get(subcommand) ?? subcommand;
if (unsupportedBuiltinDispatchers[manager].has(canonicalSubcommand)) {
return unsupportedResult();
}
if (lifecycleMutationCommands.has(canonicalSubcommand)) {
const lifecycleArgumentsSupported = parseLifecycleArguments(
manager,
argumentsAfterCommand,
suppression,
);
return Object.freeze({
dependencies: Object.freeze([]),
unsafeLifecycle:
!lifecycleArgumentsSupported || !hasEffectiveLifecycleSuppression(suppression),
unsupportedManagerSyntax: !lifecycleArgumentsSupported,
});
}
if (knownBuiltinCommands[manager].has(canonicalSubcommand)) {
return exactBareSafeBuiltinCommands[manager].has(canonicalSubcommand) &&
!consumedManagerSyntax &&
argumentsAfterCommand.length === 0
? emptyResult()
: unsupportedResult();
}
const isKnownRootScript = Object.prototype.hasOwnProperty.call(scripts, subcommand);
const supportsImplicit = /^[A-Za-z0-9:_-]+$/u.test(subcommand) && (
((manager === "pnpm" || manager === "yarn") && isKnownRootScript) ||
(manager === "npm" && npmImplicitScripts.has(subcommand))
);
if (supportsImplicit) {
if (changesManifestScope) return unsupportedResult();
if (
manager === "npm" &&
(expansionTokens.slice(start, cursor + 1).some(Boolean) ||
!areNpmScriptDispatchArgumentsSupported(
argumentsAfterCommand,
expansionTokens.slice(cursor + 1),
suppression,
))
) {
return unsupportedResult();
}
return manager === "npm"
? npmScriptDependencyResult(subcommand, scripts, suppression)
: dependencyResult(subcommand);
}
return unsupportedResult();
}
function areNpmScriptDispatchArgumentsSupported(
tokens: readonly string[],
expansionTokens: readonly boolean[],
suppression: SuppressionState,
): boolean {
let index = 0;
while (index < tokens.length) {
const token = tokens[index]!;
if (token === "--") return true;
if (expansionTokens[index]) return false;
if (!token.startsWith("-")) {
index += 1;
continue;
}
const parsedSuppression = consumeSuppressionOption(
"npm",
tokens,
index,
suppression,
);
if (parsedSuppression.recognized) {
if (parsedSuppression.unsupported) return false;
index = parsedSuppression.nextIndex;
continue;
}
const equals = token.indexOf("=");
const name = equals < 0 ? token : token.slice(0, equals);
const isShortWorkspaceOption = token === "-w" || /^-w(?:=)?.+/u.test(token);
if (npmScriptDispatchScopeOptions.has(name) || isShortWorkspaceOption) {
return false;
}
if (!npmScriptDispatchBooleanOptions.has(name)) return false;
if (equals >= 0 && !/^(?:true|false)$/u.test(token.slice(equals + 1))) {
return false;
}
index += 1;
}
return true;
}
function hasUnsafeNpmScopeEnvironment(
state: Readonly<ShellNpmScopeEnvironmentState>,
tokens: readonly string[],
expansionTokens: readonly boolean[],
commandIndex: number,
): boolean {
return state.forbidden || state.uncertain ||
hasUnsafeImmediateNpmScopeEnvironment(tokens, expansionTokens, commandIndex);
}
function hasUnsafeManagerCommandPrefix(
tokens: readonly string[],
expansionTokens: readonly boolean[],
commandIndex: number,
): boolean {
const prefix = parseShellCommandPrefix(tokens, expansionTokens);
if (prefix.uncertain) return true;
let cursor = prefix.commandIndex;
if (cursor === commandIndex) return false;
if (cursor > commandIndex ||
!isEnvironmentCommand(tokens[cursor], expansionTokens[cursor] ?? false)) {
return true;
}
return parseEnvironmentCommandPrefix(
tokens,
expansionTokens,
cursor,
commandIndex,
).uncertain;
}
function hasUnsafeImmediateNpmScopeEnvironment(
tokens: readonly string[],
expansionTokens: readonly boolean[],
commandIndex: number,
): boolean {
const prefix = parseShellCommandPrefix(tokens, expansionTokens);
if (prefix.uncertain || prefix.assignments.some(
({ name }) => name !== null && isNpmScopeEnvironmentName(name),
)) return true;
let cursor = prefix.commandIndex;
if (cursor === commandIndex) return false;
if (!isEnvironmentCommand(tokens[cursor], expansionTokens[cursor] ?? false)) return false;
const environmentPrefix = parseEnvironmentCommandPrefix(
tokens,
expansionTokens,
cursor,
commandIndex,
);
return environmentPrefix.uncertain || environmentPrefix.assignmentNames.some(
(name) => isNpmScopeEnvironmentName(name),
);
}
function parseEnvironmentCommandPrefix(
tokens: readonly string[],
expansionTokens: readonly boolean[],
start: number,
commandIndex: number,
): EnvironmentCommandPrefix {
const assignmentNames: string[] = [];
let cursor = start + 1;
let uncertain = false;
while (cursor < commandIndex && tokens[cursor]!.startsWith("-")) {
const option = tokens[cursor]!;
if (expansionTokens[cursor]) uncertain = true;
if (option === "--") {
cursor += 1;
break;
}
if (option === "-i" || option === "--ignore-environment") {
cursor += 1;
continue;
}
if (option === "-u" || option === "--unset") {
cursor += 1;
if (cursor >= commandIndex || tokens[cursor]!.startsWith("-")) {
uncertain = true;
break;
}
uncertain ||= expansionTokens[cursor] ?? false;
cursor += 1;
continue;
}
if (/^--unset=.+/u.test(option)) {
cursor += 1;
continue;
}
uncertain = true;
cursor += 1;
}
while (cursor < commandIndex) {
const token = tokens[cursor]!;
if (hasDynamicAssignmentName(token, expansionTokens[cursor] ?? false)) {
uncertain = true;
}
const assignmentName = parseEnvironmentAssignmentName(token);
if (assignmentName) assignmentNames.push(assignmentName);
else uncertain = true;
cursor += 1;
}
return Object.freeze({
assignmentNames: Object.freeze(assignmentNames),
uncertain,
});
}
function parseShellCommandPrefix(
tokens: readonly string[],
expansionTokens: readonly boolean[],
): ShellCommandPrefix {
const assignments: Array<{
dynamicName: boolean;
name: string | null;
}> = [];
let cursor = 0;
let uncertain = false;
while (cursor < tokens.length) {
const token = tokens[cursor]!;
const name = parseAssignmentName(token);
const dynamicName = hasDynamicAssignmentName(
token,
expansionTokens[cursor] ?? false,
);
if (!name && !dynamicName) break;
assignments.push({ dynamicName, name });
uncertain ||= dynamicName;
cursor += 1;
}
while (cursor < tokens.length) {
const wrapper = tokens[cursor];
if (expansionTokens[cursor]) {
uncertain = true;
break;
}
if (wrapper !== "command" && wrapper !== "exec") break;
cursor += 1;
while (cursor < tokens.length && tokens[cursor]!.startsWith("-")) {
const option = tokens[cursor]!;
if (option === "--") {
cursor += 1;
break;
}
if (wrapper === "command" && option === "-p") {
cursor += 1;
continue;
}
uncertain = true;
cursor += 1;
if (wrapper === "exec" && option === "-a" && cursor < tokens.length) {
cursor += 1;
}
}
}
if (expansionTokens[cursor]) uncertain = true;
return Object.freeze({
assignments: Object.freeze(assignments.map((assignment) => Object.freeze(assignment))),
commandIndex: cursor,
uncertain,
});
}
function updateShellNpmScopeEnvironmentState(
segment: TokenizedShellSegment,
state: ShellNpmScopeEnvironmentState,
): void {
const { tokens, expansionTokens } = segment;
const prefix = parseShellCommandPrefix(tokens, expansionTokens);
state.uncertain ||= prefix.uncertain;
const command = tokens[prefix.commandIndex];
if (!command) {
for (const assignment of prefix.assignments) {
if (state.autoExport && assignment.name &&
isNpmScopeEnvironmentName(assignment.name)) {
state.forbidden = true;
}
}
return;
}
if (command === "eval" || command === "." || command === "source") {
state.uncertain = true;
return;
}
if (command === "unset" || command === "typeset" || command === "declare" ||
command === "local" || command === "readonly") {
state.uncertain = true;
return;
}
if (command === "set") {
if (tokens.slice(prefix.commandIndex + 1).includes("-a")) state.autoExport = true;
if (tokens.slice(prefix.commandIndex + 1).includes("+a")) {
state.autoExport = false;
state.uncertain = true;
}
return;
}
if (command === "export") {
let cursor = prefix.commandIndex + 1;
for (; cursor < tokens.length; cursor += 1) {
const token = tokens[cursor]!;
if (token === "--") continue;
if (token === "-n" || token.startsWith("-")) {
state.uncertain = true;
continue;
}
const assignmentName = parseAssignmentName(token);
const bareName = /^[A-Za-z_][A-Za-z0-9_]*$/u.test(token) ? token : null;
if (assignmentName || bareName) {
if (isNpmScopeEnvironmentName(assignmentName ?? bareName!)) {
state.forbidden = true;
}
} else if (hasDynamicAssignmentName(token, expansionTokens[cursor] ?? false) ||
expansionTokens[cursor]) {
state.uncertain = true;
}
}
return;
}
}
function isEnvironmentCommand(token: string | undefined, hasExpansion: boolean): boolean {
if (!token || hasExpansion) return false;
return token.split("/").at(-1) === "env";
}
function parseEnvironmentAssignmentName(token: string): string | null {
const equals = token.indexOf("=");
return equals > 0 ? token.slice(0, equals) : null;
}
function isNpmScopeEnvironmentName(name: string): boolean {
return npmDispatchScopeEnvironmentNames.has(name.toLowerCase());
}
function hasDynamicAssignmentName(token: string, hasExpansion: boolean): boolean {
const equals = token.indexOf("=");
return hasExpansion && equals > 0 && parseAssignmentName(token) === null;
}
function parseAssignmentName(token: string): string | null {
return /^([A-Za-z_][A-Za-z0-9_]*)=/u.exec(token)?.[1] ?? null;
}
function parseLifecycleArguments(
manager: PackageManager,
tokens: readonly string[],
suppression: SuppressionState,
): boolean {
let cursor = 0;
while (cursor < tokens.length) {
const token = tokens[cursor]!;
if (!token.startsWith("-")) {
cursor += 1;
continue;
}
const parsedSuppression = consumeSuppressionOption(
manager,
tokens,
cursor,
suppression,
);
if (parsedSuppression.recognized) {
if (parsedSuppression.unsupported) return false;
cursor = parsedSuppression.nextIndex;
continue;
}
const parsedOption = consumeAllowedLifecycleOption(manager, tokens, cursor);
if (parsedOption === null) return false;
cursor = parsedOption;
}
return true;
}
function consumeSuppressionOption(
manager: PackageManager,
tokens: readonly string[],
index: number,
state: SuppressionState,
): Readonly<{ recognized: boolean; unsupported: boolean; nextIndex: number }> {
const token = tokens[index]!;
if (token === "--no-ignore-scripts") {
recordSuppression(state, false);
return { recognized: true, unsupported: false, nextIndex: index + 1 };
}
const equalsForms = ["--ignore-scripts=", "--config.ignore-scripts="] as const;
for (const prefix of equalsForms) {
if (!token.startsWith(prefix)) continue;
if (prefix.startsWith("--config.") && manager !== "pnpm") {
state.malformed = true;
return { recognized: true, unsupported: true, nextIndex: index + 1 };
}
const raw = token.slice(prefix.length);
if (raw !== "true" && raw !== "false") {
state.malformed = true;
return { recognized: true, unsupported: true, nextIndex: index + 1 };
}
recordSuppression(state, raw === "true");
return { recognized: true, unsupported: false, nextIndex: index + 1 };
}
if (token !== "--ignore-scripts" && token !== "--config.ignore-scripts") {
return { recognized: false, unsupported: false, nextIndex: index };
}
if (token === "--config.ignore-scripts" && manager !== "pnpm") {
state.malformed = true;
return { recognized: true, unsupported: true, nextIndex: index + 1 };
}
const next = tokens[index + 1];
if (next === "true" || next === "false") {
const supportsSplitValue = manager === "npm" || manager === "pnpm";
if (!supportsSplitValue) {
state.malformed = true;
return { recognized: true, unsupported: true, nextIndex: index + 2 };
}
recordSuppression(state, next === "true");
return { recognized: true, unsupported: false, nextIndex: index + 2 };
}
recordSuppression(state, true);
return { recognized: true, unsupported: false, nextIndex: index + 1 };
}
function recordSuppression(
state: SuppressionState,
value: boolean,
): void {
if (state.effective !== undefined && state.effective !== value) {
state.contradictory = true;
}
state.effective = value;
}
function hasEffectiveLifecycleSuppression(state: SuppressionState): boolean {
return state.effective === true && !state.contradictory && !state.malformed;
}
function consumeAllowedLifecycleOption(
manager: PackageManager,
tokens: readonly string[],
index: number,
): number | null {
const option = tokens[index]!;
const equals = option.indexOf("=");
const name = equals < 0 ? option : option.slice(0, equals);
const booleanOption =
managerBooleanOptions.has(name) || lifecycleBooleanOptions[manager].has(name);
if (booleanOption) {
if (equals >= 0 && !/^(?:true|false)$/u.test(option.slice(equals + 1))) return null;
return index + 1;
}
const valuedOption =
managerOptionsWithValue.has(name) || lifecycleOptionsWithValue[manager].has(name);
if (!valuedOption) return null;
if (equals >= 0) return option.slice(equals + 1).length > 0 ? index + 1 : null;
const value = tokens[index + 1];
if (!value || value.startsWith("-")) return null;
return index + 2;
}
function dependencyResult(dependency: string): ManagerParseResult {
return dependenciesResult([dependency]);
}
function npmScriptDependencyResult(
dependency: string,
scripts: Readonly<Record<string, string>>,
suppression: SuppressionState,
): ManagerParseResult {
if (hasEffectiveLifecycleSuppression(suppression)) {
return dependenciesResult([dependency]);
}
return dependenciesResult(
[`pre${dependency}`, dependency, `post${dependency}`]
.filter((scriptName) => scriptName === dependency || scriptName in scripts),
);
}
function dependenciesResult(dependencies: readonly string[]): ManagerParseResult {
return Object.freeze({
dependencies: Object.freeze([...dependencies]),
unsafeLifecycle: false,
unsupportedManagerSyntax: false,
});
}
function emptyResult(): ManagerParseResult {
return Object.freeze({
dependencies: Object.freeze([]),
unsafeLifecycle: false,
unsupportedManagerSyntax: false,
});
}
function unsupportedResult(): ManagerParseResult {
return Object.freeze({
dependencies: Object.freeze([]),
unsafeLifecycle: false,
unsupportedManagerSyntax: true,
});
}
function isPackageManager(value: string): value is PackageManager {
return managerNames.has(value as PackageManager);
}
function containsManagerReference(value: string): boolean {
return /(?:^|[^A-Za-z0-9_-])(?:corepack|pnpm|npm|yarn)(?:[^A-Za-z0-9_-]|$)/u
.test(value);
}
function tokenizeShellSegments(command: string): Readonly<{
segments: readonly TokenizedShellSegment[];
unsupportedControl: boolean;
}> | null {
const segments: Array<{ tokens: string[]; expansionTokens: boolean[] }> = [
{ tokens: [], expansionTokens: [] },
];
let token = "";
let tokenHasExpansion = false;
let quote: "'" | '"' | null = null;
let escaping = false;
let unsupportedControl = false;
const pushToken = (): void => {
if (token.length > 0) {
segments.at(-1)!.tokens.push(token);
segments.at(-1)!.expansionTokens.push(tokenHasExpansion);
}
token = "";
tokenHasExpansion = false;
};
const pushSegment = (): void => {
pushToken();
if (segments.at(-1)!.tokens.length > 0) {
segments.push({ tokens: [], expansionTokens: [] });
}
};
for (let index = 0; index < command.length; index += 1) {
const character = command[index]!;
if (escaping) {
token += character;
escaping = false;
continue;
}
if (character === "\\" && quote !== "'") {
escaping = true;
continue;
}
if (quote) {
if (character === quote) quote = null;
else {
if (quote === '"' && character === "$") tokenHasExpansion = true;
token += character;
}
continue;
}
if (character === "'" || character === '"') {
quote = character;
continue;
}
if (character === "`" || (character === "$" && command[index + 1] === "(")) {
unsupportedControl = true;
if (character === "$") tokenHasExpansion = true;
token += character;
continue;
}
if (character === "$" || character === "*" || character === "?" || character === "[") {
tokenHasExpansion = true;
}
if (character === "#") {
unsupportedControl = true;
pushToken();
while (
index + 1 < command.length &&
command[index + 1] !== "\n" &&
command[index + 1] !== "\r"
) {
index += 1;
}
continue;
}
if (character === "<" || character === ">" || character === "(" || character === ")") {
unsupportedControl = true;
token += character;
continue;
}
if (/\s/u.test(character)) {
pushToken();
if (character === "\n" || character === "\r") pushSegment();
continue;
}
if (character === ";" || character === "|" || character === "&") {
pushSegment();
if (command[index + 1] === character) index += 1;
continue;
}
token += character;
}
if (quote || escaping) return null;
pushToken();
return Object.freeze({
segments: Object.freeze(
segments
.filter((segment) => segment.tokens.length > 0)
.map((segment) => Object.freeze({
tokens: Object.freeze(segment.tokens),
expansionTokens: Object.freeze(segment.expansionTokens),
})),
),
unsupportedControl,
});
}
+709
View File
@@ -0,0 +1,709 @@
import {
createHash,
createPublicKey,
randomBytes as cryptoRandomBytes,
} from "node:crypto";
import { constants } from "node:fs";
import {
lstat,
mkdir,
open,
readdir,
rm,
rmdir,
stat,
} from "node:fs/promises";
import path from "node:path";
import {
PROMOTED_FILE_NAMES,
type PromotedFileName,
} from "../contracts/promotion-artifacts.ts";
import {
evaluatePromotionEvidence,
assertDistinctProviderTrust,
providerPublicKeyFingerprint,
providerVerificationArtifactSchema,
PROMOTION_VERIFIER_ID,
PROMOTION_VERIFIER_VERSION,
provenanceProviderAttestationSchema,
trustPolicySha256,
vulnerabilityProviderReportSchema,
type ProviderTrust,
} from "./provider-evidence.ts";
import { verifyExactPromotionBundle } from "./exact-promotion-bundle.ts";
import {
captureCiCandidateArchive,
withVerifiedCapturedCandidate,
} from "./ci-candidate-archive.ts";
import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts";
import { readBoundedRegularFile } from "./ci-artifact-validator.ts";
export type StagedFile = Readonly<{
name: PromotedFileName;
bytes: Buffer;
sha256: string;
}>;
export type FinalizedPromotion = Readonly<{
stagingRoot: string;
cleanupToken: string;
runnerTempIdentity: Readonly<{ dev: number; ino: number }>;
stagingIdentity: Readonly<{ dev: number; ino: number }>;
files: readonly Readonly<{ name: PromotedFileName; sha256: string }>[];
}>;
export async function finalizeVerifiedPromotion(input: Readonly<{
repositoryRoot: string;
archivePath: string;
expectedArchiveSha256: string;
vulnerabilityReportPath: string;
provenanceAttestationPath: string;
vulnerabilityPublicKeyPath: string;
vulnerabilityKeyId: string;
provenancePublicKeyPath: string;
provenanceKeyId: string;
expectedRun: Readonly<{ id: string; attempt: number; sourceRevision: string }>;
vulnerabilityInvocationNonce: string;
provenanceInvocationNonce: string;
runnerTempRoot: string;
}>, dependencies: Readonly<{
captureArchive?: typeof captureCiCandidateArchive;
nowEpochMs?: () => number;
randomBytes?: (bytes: number) => Buffer;
afterCapture?: () => Promise<void>;
beforePublish?: () => Promise<void>;
afterStagingWrite?: () => Promise<void>;
afterFileWrite?: (name: PromotedFileName) => Promise<void>;
beforeSeal?: () => Promise<void>;
afterMkdirBeforeOpen?: (stagingRoot: string) => Promise<void>;
}> = {}): Promise<FinalizedPromotion> {
const root = path.resolve(input.repositoryRoot);
const capturedArchive = await (dependencies.captureArchive ?? captureCiCandidateArchive)({
archivePath: input.archivePath,
expectedSha256: input.expectedArchiveSha256,
});
const [vulnerabilityBytes, provenanceBytes, vulnerabilityKeyBytes, provenanceKeyBytes] =
await Promise.all([
capture(root, input.vulnerabilityReportPath, 16_777_216),
capture(root, input.provenanceAttestationPath, 16_777_216),
capture(root, input.vulnerabilityPublicKeyPath, 1_048_576),
capture(root, input.provenancePublicKeyPath, 1_048_576),
]);
await dependencies.afterCapture?.();
const vulnerabilityTrust = capturedTrust(
input.vulnerabilityKeyId,
vulnerabilityKeyBytes,
);
const provenanceTrust = capturedTrust(
input.provenanceKeyId,
provenanceKeyBytes,
);
assertDistinctProviderTrust({ vulnerabilityTrust, provenanceTrust });
const vulnerabilityReport = vulnerabilityProviderReportSchema.parse(
parseJson(vulnerabilityBytes),
);
const provenanceAttestation = provenanceProviderAttestationSchema.parse(
parseJson(provenanceBytes),
);
const nowEpochMs = dependencies.nowEpochMs ?? Date.now;
const generated = await withVerifiedCapturedCandidate({
captured: capturedArchive,
verify: async ({ extractionRoot, manifest }) => {
const local = await verifyArchivedLocalEvidence({
extractionRoot,
expectedManifest: manifest,
});
if (local.status !== "PASS" || !local.identity) {
throw new Error(
`captured local evidence failed final verification: ${local.failures.join(", ")}`,
);
}
if (local.identity.sourceRevision !== input.expectedRun.sourceRevision) {
throw new Error("captured source revision differs from expected promotion revision");
}
const expected = {
run: { id: input.expectedRun.id, attempt: input.expectedRun.attempt },
source: {
revision: local.identity.sourceRevision,
sourceSetSha256: local.identity.sourceSetSha256,
},
candidate: {
archiveSha256: capturedArchive.archiveSha256,
bundleSha256: manifest.bundleSha256,
distSha256: manifest.distSha256,
lockfileSha256: manifest.lockfileSha256,
},
secretScanAttestation: {
status: "PASS" as const,
localEvidenceAssessmentSha256: local.identity.assessmentSha256,
sourceSetSha256: local.identity.sourceSetSha256,
policySha256: local.identity.secretScan.policySha256,
sarifSha256: local.identity.secretScan.sarifSha256,
scanInputSha256: local.identity.secretScan.scanInputSha256,
},
vulnerabilityInvocationNonce: input.vulnerabilityInvocationNonce,
provenanceInvocationNonce: input.provenanceInvocationNonce,
} as const;
const reevaluated = evaluatePromotionEvidence({
expected,
localStatus: local.status,
vulnerabilityReport,
provenanceAttestation,
vulnerabilityTrust,
provenanceTrust,
nowEpochMs,
});
if (reevaluated.status !== "PASS") {
throw new Error(
`captured provider evidence failed trusted revalidation: ${reevaluated.failures.join(", ")}`,
);
}
const providerEvidence = {
vulnerabilityReportSha256: sha256(vulnerabilityBytes),
provenanceAttestationSha256: sha256(provenanceBytes),
vulnerabilityInvocationNonce: input.vulnerabilityInvocationNonce,
provenanceInvocationNonce: input.provenanceInvocationNonce,
vulnerabilityKeyId: vulnerabilityTrust.keyId,
vulnerabilityKeyFingerprint: vulnerabilityTrust.publicKeyFingerprint,
provenanceKeyId: provenanceTrust.keyId,
provenanceKeyFingerprint: provenanceTrust.publicKeyFingerprint,
secretScanAttestation: expected.secretScanAttestation,
} as const;
const trustDigest = trustPolicySha256({ vulnerabilityTrust, provenanceTrust });
const verifiedAt = new Date(nowEpochMs()).toISOString();
const common = {
schemaVersion: 3 as const,
verifiedAt,
status: "PASS" as const,
verifier: {
id: PROMOTION_VERIFIER_ID,
version: PROMOTION_VERIFIER_VERSION,
},
run: expected.run,
source: expected.source,
candidate: expected.candidate,
providerEvidence,
trustPolicySha256: trustDigest,
failures: [] as const,
};
const providerRecord = providerVerificationArtifactSchema.parse({
...common,
artifactType: "provider-verification",
vulnerabilityStatus: reevaluated.vulnerabilityStatus,
provenanceAttestationStatus: reevaluated.provenanceAttestationStatus,
});
const providerRecordBytes = canonicalJsonBytes(providerRecord);
const promotionRecord = providerVerificationArtifactSchema.parse({
...common,
artifactType: "promotion-verification",
localEvidenceStatus: local.status,
localEvidenceAssessmentSha256: local.identity.assessmentSha256,
providerVerificationSha256: sha256(providerRecordBytes),
});
return Object.freeze({
providerRecordBytes,
promotionRecordBytes: canonicalJsonBytes(promotionRecord),
exactExpected: Object.freeze({
run: expected.run,
sourceRevision: expected.source.revision,
sourceSetSha256: expected.source.sourceSetSha256,
archiveSha256: expected.candidate.archiveSha256,
bundleSha256: expected.candidate.bundleSha256,
distSha256: expected.candidate.distSha256,
lockfileSha256: expected.candidate.lockfileSha256,
}),
});
},
});
const stagedFiles: readonly StagedFile[] = Object.freeze([
staged("release-candidate.tar.gz", capturedArchive.bytes),
staged("vulnerability-report.json", vulnerabilityBytes),
staged("provenance-attestation.json", provenanceBytes),
staged("provider-verification.json", generated.providerRecordBytes),
staged("promotion-verification.json", generated.promotionRecordBytes),
]);
if (
JSON.stringify(stagedFiles.map(({ name }) => name)) !==
JSON.stringify(PROMOTED_FILE_NAMES)
) {
throw new Error("promotion exact-five canonical file order drift");
}
await dependencies.beforePublish?.();
await verifyExactPromotionBundle(
Object.fromEntries(stagedFiles.map(({ name, bytes }) => [name, bytes])),
{
vulnerabilityTrust,
provenanceTrust,
expected: generated.exactExpected,
nowEpochMs,
},
);
return publishPrivatePromotionStaging(
input.runnerTempRoot,
input.expectedRun,
stagedFiles,
dependencies.randomBytes ?? cryptoRandomBytes,
dependencies.afterStagingWrite,
dependencies.afterFileWrite,
async (capturedFiles) => {
await verifyExactPromotionBundle(
capturedFiles,
{
vulnerabilityTrust,
provenanceTrust,
expected: generated.exactExpected,
nowEpochMs,
},
);
},
dependencies.afterMkdirBeforeOpen,
dependencies.beforeSeal,
);
}
export const stageVerifiedPromotion = finalizeVerifiedPromotion;
export async function cleanupFinalizedPromotion(input: Readonly<{
runnerTempRoot: string;
stagingRoot: string;
cleanupToken: string;
runnerTempIdentity: Readonly<{ dev: number; ino: number }>;
stagingIdentity: Readonly<{ dev: number; ino: number }>;
}>, dependencies: Readonly<{
beforeRemove?: () => Promise<void>;
}> = {}): Promise<void> {
const parent = path.resolve(input.runnerTempRoot);
const expected = path.join(parent, input.cleanupToken);
if (
!/^[A-Za-z0-9._-]+-[a-f0-9]{32}$/u.test(input.cleanupToken) ||
path.resolve(input.stagingRoot) !== expected ||
!Number.isSafeInteger(input.runnerTempIdentity.dev) ||
input.runnerTempIdentity.dev <= 0 ||
!Number.isSafeInteger(input.runnerTempIdentity.ino) ||
input.runnerTempIdentity.ino <= 0
|| !Number.isSafeInteger(input.stagingIdentity.dev)
|| input.stagingIdentity.dev <= 0
|| !Number.isSafeInteger(input.stagingIdentity.ino)
|| input.stagingIdentity.ino <= 0
) {
throw new TypeError("promotion cleanup root/token mismatch");
}
const parentHandle = await open(
parent,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
try {
const openedParent = await parentHandle.stat();
assertRunnerTempIdentity(openedParent, input.runnerTempIdentity);
const descriptorRoot = `/proc/self/fd/${parentHandle.fd}`;
const descriptorMetadata = await stat(descriptorRoot);
if (!descriptorMetadata.isDirectory()) {
throw new Error("descriptor-relative cleanup is unavailable");
}
const descriptorExpected = path.join(descriptorRoot, input.cleanupToken);
let metadata;
try {
metadata = await lstat(descriptorExpected);
} catch (error) {
if (hasErrorCode(error, "ENOENT")) return;
throw error;
}
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
throw new TypeError("promotion cleanup leaf is unsafe");
}
assertStagingIdentity(metadata, input.stagingIdentity);
const stagingHandle = await open(
descriptorExpected,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
try {
const stagingDescriptorRoot = `/proc/self/fd/${stagingHandle.fd}`;
assertStagingIdentity(await stat(stagingDescriptorRoot), input.stagingIdentity);
const names = (await readdir(stagingDescriptorRoot)).sort(asciiCompare);
if (
JSON.stringify(names) !==
JSON.stringify([...PROMOTED_FILE_NAMES].sort(asciiCompare))
) {
throw new Error("promotion cleanup leaf does not contain the exact five files");
}
await dependencies.beforeRemove?.();
const visibleParent = await lstat(parent);
assertRunnerTempIdentity(visibleParent, input.runnerTempIdentity);
for (const name of PROMOTED_FILE_NAMES) {
await rm(path.join(stagingDescriptorRoot, name), { force: false });
}
assertStagingIdentity(await stat(stagingDescriptorRoot), input.stagingIdentity);
assertStagingIdentity(await lstat(descriptorExpected), input.stagingIdentity);
await rmdir(descriptorExpected);
} finally {
await stagingHandle.close();
}
const afterParent = await lstat(parent);
assertRunnerTempIdentity(afterParent, input.runnerTempIdentity);
} finally {
await parentHandle.close();
}
}
export async function publishPrivatePromotionStaging(
runnerTempRoot: string,
run: Readonly<{ id: string; attempt: number }>,
files: readonly StagedFile[],
randomBytes: (bytes: number) => Buffer,
afterStagingWrite?: () => Promise<void>,
afterFileWrite?: (name: PromotedFileName) => Promise<void>,
sealStagedFiles?: (files: Readonly<Record<PromotedFileName, Buffer>>) => Promise<void>,
afterMkdirBeforeOpen?: (stagingRoot: string) => Promise<void>,
beforeSeal?: () => Promise<void>,
): Promise<FinalizedPromotion> {
if (
JSON.stringify(files.map(({ name }) => name)) !==
JSON.stringify(PROMOTED_FILE_NAMES) ||
files.some(
({ bytes, sha256: digest }) =>
!Buffer.isBuffer(bytes) ||
!/^[a-f0-9]{64}$/u.test(digest) ||
sha256(bytes) !== digest,
)
) {
throw new TypeError("private promotion staging requires the canonical exact-five bytes");
}
const parentPath = path.resolve(runnerTempRoot);
const before = await lstat(parentPath);
if (!before.isDirectory() || before.isSymbolicLink()) {
throw new TypeError("runner temporary root must be a real directory");
}
const parentHandle = await open(
parentPath,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
const tokenBytes = randomBytes(16);
if (tokenBytes.byteLength !== 16) {
await parentHandle.close();
throw new TypeError("promotion staging nonce must contain exactly 128 random bits");
}
const safeRun = run.id.replaceAll(/[^A-Za-z0-9._-]/gu, "_").slice(0, 64) || "run";
const cleanupToken = `promotion-${safeRun}-${run.attempt}-${tokenBytes.toString("hex")}`;
const descriptorRoot = `/proc/self/fd/${parentHandle.fd}`;
const descriptorStaging = path.join(descriptorRoot, cleanupToken);
const visibleStaging = path.join(parentPath, cleanupToken);
let ownsStaging = false;
let stagingHandle: Awaited<ReturnType<typeof open>> | undefined;
let createdStagingIdentity: Readonly<{ dev: number; ino: number }> | undefined;
let stagingIdentity: Readonly<{ dev: number; ino: number }> | undefined;
let openedIdentityVerified = false;
const cleanup = async (primaryFailure?: unknown): Promise<void> => {
const cleanupFailures: unknown[] = [];
const attemptCleanup = async (operation: () => Promise<void>): Promise<void> => {
try {
await operation();
} catch (error) {
cleanupFailures.push(error);
}
};
if (ownsStaging && openedIdentityVerified && stagingHandle && stagingIdentity) {
const ownedIdentity = stagingIdentity;
const stagingDescriptorRoot = `/proc/self/fd/${stagingHandle.fd}`;
const removals = await Promise.allSettled(
files.map(({ name }) => rm(path.join(stagingDescriptorRoot, name), { force: true })),
);
cleanupFailures.push(
...removals.flatMap((result) =>
result.status === "rejected" ? [result.reason] : [],
),
);
await attemptCleanup(async () => {
let visible;
try {
visible = await lstat(descriptorStaging);
} catch (error) {
if (hasErrorCode(error, "ENOENT")) return;
throw error;
}
if (
visible.isDirectory() &&
!visible.isSymbolicLink() &&
visible.dev === ownedIdentity.dev &&
visible.ino === ownedIdentity.ino
) {
await rmdir(descriptorStaging);
}
});
}
if (stagingHandle) {
const ownedHandle = stagingHandle;
await attemptCleanup(async () => ownedHandle.close());
}
await attemptCleanup(async () => parentHandle.close());
if (cleanupFailures.length > 0) {
throw new AggregateError(
primaryFailure === undefined
? cleanupFailures
: [primaryFailure, ...cleanupFailures],
primaryFailure instanceof Error
? `${primaryFailure.message}; promotion staging cleanup also failed`
: "promotion staging cleanup failed",
{ cause: cleanupFailures.at(-1) },
);
}
};
let finalizedPromotion: FinalizedPromotion;
try {
const procMetadata = await stat(descriptorRoot);
if (!procMetadata.isDirectory()) throw new Error("descriptor-relative staging is unavailable");
await mkdir(descriptorStaging, { mode: 0o700 });
ownsStaging = true;
const createdStaging = await lstat(descriptorStaging);
if (!createdStaging.isDirectory() || createdStaging.isSymbolicLink()) {
throw new Error("created promotion staging leaf is unsafe");
}
createdStagingIdentity = Object.freeze({
dev: createdStaging.dev,
ino: createdStaging.ino,
});
await afterMkdirBeforeOpen?.(visibleStaging);
const openedHandle = await open(
descriptorStaging,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
let openedStaging;
try {
openedStaging = await openedHandle.stat();
if (!openedStaging.isDirectory()) {
throw new Error("promotion staging descriptor is not a directory");
}
if (
openedStaging.dev !== createdStagingIdentity.dev ||
openedStaging.ino !== createdStagingIdentity.ino
) {
throw new Error("promotion staging leaf identity changed between mkdir and open");
}
openedIdentityVerified = true;
} catch (error) {
try {
await openedHandle.close();
} catch (closeError) {
throw new AggregateError(
[error, closeError],
error instanceof Error
? `${error.message}; rejected staging descriptor close also failed`
: "rejected staging descriptor and close both failed",
{ cause: closeError },
);
}
throw error;
}
stagingHandle = openedHandle;
await stagingHandle.chmod(0o700);
stagingIdentity = Object.freeze({ dev: openedStaging.dev, ino: openedStaging.ino });
const stagingDescriptorRoot = `/proc/self/fd/${stagingHandle.fd}`;
assertStagingIdentity(await stat(stagingDescriptorRoot), stagingIdentity);
for (const file of files) {
const handle = await open(
path.join(stagingDescriptorRoot, file.name),
constants.O_WRONLY |
constants.O_CREAT |
constants.O_EXCL |
constants.O_NOFOLLOW,
0o400,
);
try {
await handle.chmod(0o400);
await handle.writeFile(file.bytes);
await handle.sync();
} finally {
await handle.close();
}
await afterFileWrite?.(file.name);
}
await syncHandle(stagingHandle);
await syncHandle(parentHandle);
await afterStagingWrite?.();
await beforeSeal?.();
const capturedFiles = await captureStagedFiles(stagingHandle, files);
await sealStagedFiles?.(capturedFiles);
const after = await lstat(parentPath);
if (
after.dev !== before.dev ||
after.ino !== before.ino ||
after.isSymbolicLink() ||
!after.isDirectory()
) {
throw new Error("runner temporary parent identity changed during staging");
}
const visible = await lstat(visibleStaging);
if (!visible.isDirectory() || visible.isSymbolicLink()) {
throw new Error("promotion staging visibility identity mismatch");
}
assertStagingIdentity(visible, stagingIdentity);
ownsStaging = false;
finalizedPromotion = Object.freeze({
stagingRoot: visibleStaging,
cleanupToken,
runnerTempIdentity: Object.freeze({ dev: before.dev, ino: before.ino }),
stagingIdentity,
files: Object.freeze(
files.map(({ name, sha256: digest }) => Object.freeze({ name, sha256: digest })),
),
});
} catch (error) {
await cleanup(error);
throw error;
}
await cleanup();
return finalizedPromotion;
}
async function captureStagedFiles(
stagingHandle: Awaited<ReturnType<typeof open>>,
declaredFiles: readonly StagedFile[],
): Promise<Readonly<Record<PromotedFileName, Buffer>>> {
const descriptorRoot = `/proc/self/fd/${stagingHandle.fd}`;
const names = (await readdir(descriptorRoot)).sort(asciiCompare);
if (
JSON.stringify(names) !==
JSON.stringify([...PROMOTED_FILE_NAMES].sort(asciiCompare))
) {
throw new Error("staged promotion seal requires exactly the canonical five files");
}
const declared = new Map(declaredFiles.map((file) => [file.name, file] as const));
const captured = {} as Record<PromotedFileName, Buffer>;
for (const name of PROMOTED_FILE_NAMES) {
const expected = declared.get(name)!;
const handle = await open(
path.join(descriptorRoot, name),
constants.O_RDONLY | constants.O_NOFOLLOW,
);
try {
const before = await handle.stat();
const maxBytes = name === "release-candidate.tar.gz" ? 268_435_456 : 16_777_216;
if (
!before.isFile() ||
before.nlink !== 1 ||
(before.mode & 0o777) !== 0o400 ||
before.size <= 0 ||
before.size > maxBytes
) {
throw new Error(
`staged promotion file must be regular, single-link, bounded, and mode 0400: ${name}`,
);
}
const bytes = await handle.readFile();
const after = await handle.stat();
if (
after.dev !== before.dev ||
after.ino !== before.ino ||
after.size !== before.size ||
after.nlink !== 1 ||
(after.mode & 0o777) !== 0o400 ||
bytes.byteLength !== before.size
) {
throw new Error(`staged promotion file inode or size changed during seal: ${name}`);
}
if (sha256(bytes) !== expected.sha256) {
throw new Error(`staged promotion file digest mismatch during seal: ${name}`);
}
captured[name] = bytes;
} finally {
await handle.close();
}
}
return Object.freeze(captured);
}
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
function assertStagingIdentity(
metadata: Readonly<{
dev: number;
ino: number;
isDirectory: () => boolean;
isSymbolicLink?: () => boolean;
}>,
expected: Readonly<{ dev: number; ino: number }>,
): void {
if (
metadata.dev !== expected.dev ||
metadata.ino !== expected.ino ||
!metadata.isDirectory() ||
metadata.isSymbolicLink?.()
) {
throw new Error("promotion staging leaf identity changed");
}
}
function assertRunnerTempIdentity(
metadata: Readonly<{ dev: number; ino: number; isDirectory: () => boolean; isSymbolicLink?: () => boolean }>,
expected: Readonly<{ dev: number; ino: number }>,
): void {
if (
metadata.dev !== expected.dev ||
metadata.ino !== expected.ino ||
!metadata.isDirectory() ||
metadata.isSymbolicLink?.()
) {
throw new Error("runner temporary parent identity changed during cleanup");
}
}
function capturedTrust(keyId: string, bytes: Buffer): ProviderTrust {
const publicKey = createPublicKey(
new TextDecoder("utf-8", { fatal: true }).decode(bytes),
);
return Object.freeze({
keyId,
publicKey,
publicKeyFingerprint: providerPublicKeyFingerprint(publicKey),
});
}
async function capture(root: string, configuredPath: string, maxBytes: number): Promise<Buffer> {
const absolute = path.resolve(root, configuredPath);
const relative = path.relative(root, absolute);
const outside =
relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
return readBoundedRegularFile({
root: outside ? path.dirname(absolute) : root,
relativePath: outside ? path.basename(absolute) : relative.replaceAll(path.sep, "/"),
maxBytes,
});
}
function staged(name: PromotedFileName, bytes: Buffer): StagedFile {
return Object.freeze({ name, bytes, sha256: sha256(bytes) });
}
function canonicalJsonBytes(value: unknown): Buffer {
return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function parseJson(bytes: Buffer): unknown {
try {
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown;
} catch {
throw new TypeError("captured provider evidence is not valid UTF-8 JSON");
}
}
function sha256(bytes: Buffer): string {
return createHash("sha256").update(bytes).digest("hex");
}
async function syncHandle(handle: Awaited<ReturnType<typeof open>>): Promise<void> {
try {
await handle.sync();
} catch (error) {
if (!hasErrorCode(error, "EINVAL") && !hasErrorCode(error, "ENOTSUP")) throw error;
}
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+169
View File
@@ -0,0 +1,169 @@
export type ProviderKind = "vulnerability" | "provenance";
const MEMORY_MAX = 1_073_741_824;
const TASKS_MAX = 64;
const STOP_TIMEOUT_MS = 5_000;
const RUNTIME_GRACE_MS = 10_000;
const UNIT_NAME = /^ca-provider-(?:vulnerability|provenance)-[1-9][0-9]*-[0-9a-f]{24}\.scope$/u;
const UNIT_NONCE = /^[0-9a-f]{24}$/u;
const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
const ENFORCEMENT_GATE = [
'cgroup_path=""',
"while IFS=: read -r hierarchy controllers candidate; do",
' if [ "$hierarchy" = 0 ] && [ -z "$controllers" ]; then cgroup_path=$candidate; fi',
"done < /proc/self/cgroup",
'if [ -z "$cgroup_path" ]; then',
" printf '%s\\n' 'provider cgroup enforcement failed: unified cgroup v2 membership is required' >&2",
" exit 125",
"fi",
'case "$cgroup_path" in',
' */"$0") ;;',
" *)",
" printf '%s\\n' 'provider cgroup enforcement failed: unit membership is invalid' >&2",
" exit 125",
" ;;",
"esac",
"cgroup_root=/sys/fs/cgroup$cgroup_path",
"require_cgroup_value() {",
' actual=$(/bin/cat "$cgroup_root/$1") || {',
" printf 'provider cgroup enforcement failed: cannot read %s\\n' \"$1\" >&2",
" exit 125",
" }",
' if [ "$actual" != "$2" ]; then',
" printf 'provider cgroup enforcement failed: %s is %s, expected %s\\n' \"$1\" \"$actual\" \"$2\" >&2",
" exit 125",
" fi",
"}",
`require_cgroup_value memory.max ${MEMORY_MAX}`,
"require_cgroup_value memory.swap.max 0",
`require_cgroup_value pids.max ${TASKS_MAX}`,
"require_cgroup_value cpu.max '100000 100000'",
'exec "$@"',
].join("\n");
export function formatProviderCgroupUnitName(
kind: ProviderKind,
supervisorPid: number,
nonce: string,
): string {
if (!Number.isSafeInteger(supervisorPid) || supervisorPid <= 0 || !UNIT_NONCE.test(nonce)) {
throw new TypeError("provider cgroup unit identity is invalid");
}
const unit = `ca-provider-${kind}-${supervisorPid}-${nonce}.scope`;
assertUnit(unit);
return unit;
}
export function systemdRunProviderArguments(
unit: string,
timeoutMs: number,
cpuSeconds: number,
nodeExecutable: string,
wrapperScript: string,
reportPath: string,
reportDev: number,
reportIno: number,
): string[] {
assertUnit(unit);
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > Number.MAX_SAFE_INTEGER - RUNTIME_GRACE_MS) {
throw new TypeError("provider cgroup runtime is invalid");
}
if (!Number.isSafeInteger(cpuSeconds) || cpuSeconds <= 0) {
throw new TypeError("provider cgroup CPU limit is invalid");
}
if (
!nodeExecutable.startsWith("/") || !wrapperScript.startsWith("/") ||
!reportPath.startsWith("/") || reportPath.includes("\0") ||
!Number.isSafeInteger(reportDev) || reportDev <= 0 ||
!Number.isSafeInteger(reportIno) || reportIno <= 0
) {
throw new TypeError("provider scope wrapper path is invalid");
}
return [
"--user",
"--scope",
"--collect",
"--quiet",
"--expand-environment=no",
`--unit=${unit}`,
`--property=MemoryMax=${MEMORY_MAX}`,
"--property=MemorySwapMax=0",
`--property=TasksMax=${TASKS_MAX}`,
"--property=CPUQuota=100%",
"--property=CPUQuotaPeriodSec=100ms",
"--property=KillMode=control-group",
"--property=SendSIGKILL=yes",
`--property=TimeoutStopSec=${STOP_TIMEOUT_MS}ms`,
`--property=RuntimeMaxSec=${timeoutMs + RUNTIME_GRACE_MS}ms`,
"--",
"/bin/sh",
"-eu",
"-c",
ENFORCEMENT_GATE,
unit,
nodeExecutable,
wrapperScript,
String(cpuSeconds),
reportPath,
String(reportDev),
String(reportIno),
];
}
export type ProviderScopeFrame = Readonly<{
bwrapInput: Buffer;
reportPath: string;
reportDev: number;
reportIno: number;
}>;
export function encodeProviderScopeFrame(input: ProviderScopeFrame): Buffer {
if (
!Buffer.isBuffer(input.bwrapInput) || input.bwrapInput.byteLength === 0 ||
!input.reportPath.startsWith("/") || input.reportPath.includes("\0") ||
!Number.isSafeInteger(input.reportDev) || input.reportDev <= 0 ||
!Number.isSafeInteger(input.reportIno) || input.reportIno <= 0
) {
throw new TypeError("provider scope frame is invalid");
}
const payload = Buffer.from(JSON.stringify({
bwrapInputBase64: input.bwrapInput.toString("base64"),
reportPath: input.reportPath,
reportDev: input.reportDev,
reportIno: input.reportIno,
}));
const frame = Buffer.allocUnsafe(4 + payload.byteLength);
frame.writeUInt32BE(payload.byteLength, 0);
payload.copy(frame, 4);
return frame;
}
export function encodeProviderBwrapInput(
arguments_: readonly string[],
environment: Readonly<Record<string, string | undefined>>,
): Buffer {
if (arguments_.some((argument) => argument.includes("\0"))) {
throw new TypeError("provider bwrap argument is invalid");
}
const entries = Object.entries(environment).sort(([left], [right]) =>
left < right ? -1 : left > right ? 1 : 0,
);
if (entries.some(([name, value]) =>
!ENVIRONMENT_NAME.test(name) || value === undefined || value.includes("\0")
)) {
throw new TypeError("provider bwrap environment is invalid");
}
const input = ["--clearenv"];
for (const [name, value] of entries) input.push("--setenv", name, value ?? "");
input.push(...arguments_);
return Buffer.from(`${input.join("\0")}\0`);
}
export function systemctlKillProviderArguments(unit: string): string[] {
assertUnit(unit);
return ["--user", "kill", "--kill-whom=all", "--signal=SIGKILL", unit];
}
function assertUnit(unit: string): void {
if (!UNIT_NAME.test(unit)) throw new TypeError("provider cgroup unit name is invalid");
}
+494
View File
@@ -0,0 +1,494 @@
import { createHash, verify, type KeyObject } from "node:crypto";
import { z } from "zod";
import {
canonicalizeSupplyChainValue,
supplyChainDigest,
} from "./supply-chain.ts";
export const PROVIDER_FUTURE_SKEW_MS = 5 * 60 * 1_000;
export const PROVIDER_MAX_LIFETIME_MS = 2 * 60 * 60 * 1_000;
export const PROMOTION_VERIFIER_ID =
"clean-architecture-frontend-template/promotion-verifier";
export const PROMOTION_VERIFIER_VERSION = "3";
const sha256 = z.string().regex(/^[a-f0-9]{64}$/u);
const fingerprint = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
const revision = z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u);
const nonce = z.string().regex(/^[a-f0-9]{64}$/u);
const nonEmptyString = z.string().min(1);
const timestamp = z
.string()
.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u)
.refine((value) => new Date(value).toISOString() === value);
const runSchema = z
.object({ id: z.string().min(1).max(128), attempt: z.int().min(1).max(1_000) })
.strict();
const sourceSchema = z
.object({ revision, sourceSetSha256: sha256 })
.strict();
const candidateSchema = z
.object({
archiveSha256: sha256,
bundleSha256: sha256,
distSha256: sha256,
lockfileSha256: sha256,
})
.strict();
const providerRunSchema = runSchema.extend({ invocationNonce: nonce }).strict();
export const secretScanAttestationSchema = z
.object({
status: z.literal("PASS"),
localEvidenceAssessmentSha256: sha256,
sourceSetSha256: sha256,
policySha256: sha256,
sarifSha256: sha256,
scanInputSha256: sha256,
})
.strict();
const signatureSchema = z
.object({
algorithm: z.literal("Ed25519"),
keyId: nonEmptyString,
publicKeyFingerprint: fingerprint,
value: z.string().regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u),
})
.strict();
const providerCommon = {
schemaVersion: z.literal(2),
provider: nonEmptyString,
issuedAt: timestamp,
expiresAt: timestamp,
run: providerRunSchema,
source: sourceSchema,
candidate: candidateSchema,
signature: signatureSchema,
} as const;
export const vulnerabilityProviderReportSchema = z
.object({
...providerCommon,
evidenceType: z.literal("vulnerability-report"),
secretScanAttestation: secretScanAttestationSchema,
findings: z.array(z.record(z.string(), z.json())),
})
.strict();
export const provenanceProviderAttestationSchema = z
.object({
...providerCommon,
evidenceType: z.literal("provenance-attestation"),
signer: nonEmptyString,
subject: z
.object({ name: z.literal("dist"), digest: z.object({ sha256 }).strict() })
.strict(),
})
.strict();
const verificationCommon = {
schemaVersion: z.literal(3),
verifiedAt: timestamp,
status: z.enum(["PASS", "FAIL_UNVERIFIED"]),
verifier: z
.object({ id: nonEmptyString, version: nonEmptyString })
.strict(),
run: runSchema,
source: sourceSchema,
candidate: candidateSchema,
providerEvidence: z
.object({
vulnerabilityReportSha256: sha256,
provenanceAttestationSha256: sha256,
vulnerabilityInvocationNonce: nonce,
provenanceInvocationNonce: nonce,
vulnerabilityKeyId: nonEmptyString,
vulnerabilityKeyFingerprint: fingerprint,
provenanceKeyId: nonEmptyString,
provenanceKeyFingerprint: fingerprint,
secretScanAttestation: secretScanAttestationSchema,
})
.strict(),
trustPolicySha256: sha256,
failures: z.array(z.string()),
} as const;
const providerVerificationV3Schema = z
.object({
...verificationCommon,
artifactType: z.literal("provider-verification"),
vulnerabilityStatus: z.enum(["PASS", "FAIL_UNVERIFIED"]),
provenanceAttestationStatus: z.enum(["PASS", "FAIL_UNVERIFIED"]),
})
.strict();
const promotionVerificationV3Schema = z
.object({
...verificationCommon,
artifactType: z.literal("promotion-verification"),
localEvidenceStatus: z.enum(["PASS", "FAIL"]),
localEvidenceAssessmentSha256: sha256,
providerVerificationSha256: sha256,
})
.strict();
export const providerVerificationArtifactSchema = z
.discriminatedUnion("artifactType", [
providerVerificationV3Schema,
promotionVerificationV3Schema,
])
.superRefine((record, context) => {
const subordinatePass =
record.artifactType === "provider-verification"
? record.vulnerabilityStatus === "PASS" &&
record.provenanceAttestationStatus === "PASS"
: record.localEvidenceStatus === "PASS";
const coherentPass = subordinatePass && record.failures.length === 0;
if ((record.status === "PASS") !== coherentPass) {
context.addIssue({
code: "custom",
path: ["status"],
message: "verification PASS must agree with subordinate statuses and failures",
});
}
if (record.status === "FAIL_UNVERIFIED" && record.failures.length === 0) {
context.addIssue({
code: "custom",
path: ["failures"],
message: "failed verification requires a failure diagnostic",
});
}
});
export type ProviderVerificationArtifactType = z.infer<
typeof providerVerificationArtifactSchema
>["artifactType"];
export type ProviderTrust = Readonly<{
keyId: string;
publicKey: KeyObject;
publicKeyFingerprint: string;
}>;
export type ExpectedPromotionContext = Readonly<{
run: Readonly<{ id: string; attempt: number }>;
source: Readonly<{ revision: string; sourceSetSha256: string }>;
candidate: Readonly<{
archiveSha256: string;
bundleSha256: string;
distSha256: string;
lockfileSha256: string;
}>;
vulnerabilityInvocationNonce: string;
provenanceInvocationNonce: string;
secretScanAttestation: z.infer<typeof secretScanAttestationSchema>;
}>;
export type PromotionEvidenceResult = Readonly<{
status: "PASS" | "FAIL_UNVERIFIED";
vulnerabilityStatus: "PASS" | "FAIL_UNVERIFIED";
provenanceAttestationStatus: "PASS" | "FAIL_UNVERIFIED";
failures: readonly string[];
}>;
export function validateProviderEvidence(input: Readonly<{
kind: "vulnerability" | "provenance";
value: unknown;
expected: ExpectedPromotionContext;
trust: ProviderTrust | null;
nowEpochMs?: () => number;
}>): Readonly<{
evidence: unknown | null;
status: "PASS" | "FAIL_UNVERIFIED";
failures: readonly string[];
}> {
const failures: string[] = [];
const now = (input.nowEpochMs ?? Date.now)();
if (input.kind === "vulnerability") {
const parsed = vulnerabilityProviderReportSchema.safeParse(input.value);
if (!parsed.success) {
return Object.freeze({
evidence: null,
status: "FAIL_UNVERIFIED",
failures: Object.freeze([
"external vulnerability provider report is missing or invalid",
]),
});
}
validateCommonContext(
"vulnerability report",
parsed.data,
input.expected,
input.expected.vulnerabilityInvocationNonce,
input.trust,
now,
failures,
);
if (
JSON.stringify(parsed.data.secretScanAttestation) !==
JSON.stringify(input.expected.secretScanAttestation)
) {
failures.push("vulnerability report secret scan attestation mismatch");
}
if (parsed.data.findings.length > 0) {
failures.push("vulnerability report contains findings");
}
return Object.freeze({
evidence: parsed.data,
status: failures.length === 0 ? "PASS" : "FAIL_UNVERIFIED",
failures: Object.freeze(failures),
});
}
const parsed = provenanceProviderAttestationSchema.safeParse(input.value);
if (!parsed.success) {
return Object.freeze({
evidence: null,
status: "FAIL_UNVERIFIED",
failures: Object.freeze([
"external signed provenance attestation is missing or invalid",
]),
});
}
validateCommonContext(
"provenance attestation",
parsed.data,
input.expected,
input.expected.provenanceInvocationNonce,
input.trust,
now,
failures,
);
if (parsed.data.subject.digest.sha256 !== input.expected.candidate.distSha256) {
failures.push("provenance attestation subject dist digest mismatch");
}
return Object.freeze({
evidence: parsed.data,
status: failures.length === 0 ? "PASS" : "FAIL_UNVERIFIED",
failures: Object.freeze(failures),
});
}
export function providerEvidenceSignaturePayload(value: unknown): Buffer {
if (!isRecord(value)) return Buffer.from("null", "utf8");
const { signature: _signature, ...payload } = value;
return Buffer.from(JSON.stringify(canonicalizeSupplyChainValue(payload)), "utf8");
}
export function providerPublicKeyFingerprint(publicKey: KeyObject): string {
if (publicKey.asymmetricKeyType !== "ed25519") {
throw new TypeError("provider trust key must be Ed25519");
}
return `sha256:${createHash("sha256")
.update(publicKey.export({ type: "spki", format: "der" }))
.digest("hex")}`;
}
export function createTrustPolicy(input: Readonly<{
vulnerabilityTrust: ProviderTrust;
provenanceTrust: ProviderTrust;
}>) {
assertDistinctProviderTrust(input);
return Object.freeze({
algorithm: "Ed25519" as const,
vulnerability: Object.freeze({
keyId: input.vulnerabilityTrust.keyId,
publicKeyFingerprint: input.vulnerabilityTrust.publicKeyFingerprint,
}),
provenance: Object.freeze({
keyId: input.provenanceTrust.keyId,
publicKeyFingerprint: input.provenanceTrust.publicKeyFingerprint,
}),
issuedAtFutureSkewMs: PROVIDER_FUTURE_SKEW_MS,
maximumLifetimeMs: PROVIDER_MAX_LIFETIME_MS,
});
}
export function assertDistinctProviderTrust(input: Readonly<{
vulnerabilityTrust: ProviderTrust;
provenanceTrust: ProviderTrust;
}>): void {
const vulnerabilityFingerprint = providerPublicKeyFingerprint(
input.vulnerabilityTrust.publicKey,
);
const provenanceFingerprint = providerPublicKeyFingerprint(
input.provenanceTrust.publicKey,
);
if (
input.vulnerabilityTrust.keyId === input.provenanceTrust.keyId ||
vulnerabilityFingerprint === provenanceFingerprint ||
input.vulnerabilityTrust.publicKeyFingerprint ===
input.provenanceTrust.publicKeyFingerprint
) {
throw new TypeError("provider trust roles require distinct key identities and DER-SPKI fingerprints");
}
}
export function trustPolicySha256(input: Readonly<{
vulnerabilityTrust: ProviderTrust;
provenanceTrust: ProviderTrust;
}>): string {
return supplyChainDigest(createTrustPolicy(input));
}
export function evaluatePromotionEvidence(input: Readonly<{
expected: ExpectedPromotionContext;
localStatus: unknown;
vulnerabilityReport: unknown;
provenanceAttestation: unknown;
vulnerabilityTrust: ProviderTrust | null;
provenanceTrust: ProviderTrust | null;
nowEpochMs?: () => number;
}>): PromotionEvidenceResult {
const failures: string[] = [];
if (input.localStatus !== "PASS") {
failures.push("local supply-chain evidence is not PASS");
}
const now = (input.nowEpochMs ?? Date.now)();
let vulnerabilityStatus: "PASS" | "FAIL_UNVERIFIED" = "FAIL_UNVERIFIED";
let provenanceAttestationStatus: "PASS" | "FAIL_UNVERIFIED" = "FAIL_UNVERIFIED";
const vulnerability = vulnerabilityProviderReportSchema.safeParse(
input.vulnerabilityReport,
);
if (!vulnerability.success) {
failures.push("external vulnerability provider report is missing or invalid");
} else {
const before = failures.length;
validateCommonContext(
"vulnerability report",
vulnerability.data,
input.expected,
input.expected.vulnerabilityInvocationNonce,
input.vulnerabilityTrust,
now,
failures,
);
if (
JSON.stringify(vulnerability.data.secretScanAttestation) !==
JSON.stringify(input.expected.secretScanAttestation)
) {
failures.push("vulnerability report secret scan attestation mismatch");
}
if (vulnerability.data.findings.length > 0) {
failures.push("vulnerability report contains findings");
}
if (failures.length === before && input.localStatus === "PASS") {
vulnerabilityStatus = "PASS";
}
}
const provenance = provenanceProviderAttestationSchema.safeParse(
input.provenanceAttestation,
);
if (!provenance.success) {
failures.push("external signed provenance attestation is missing or invalid");
} else {
const before = failures.length;
validateCommonContext(
"provenance attestation",
provenance.data,
input.expected,
input.expected.provenanceInvocationNonce,
input.provenanceTrust,
now,
failures,
);
if (provenance.data.subject.digest.sha256 !== input.expected.candidate.distSha256) {
failures.push("provenance attestation subject dist digest mismatch");
}
if (failures.length === before && input.localStatus === "PASS") {
provenanceAttestationStatus = "PASS";
}
}
return Object.freeze({
status:
failures.length === 0 &&
vulnerabilityStatus === "PASS" &&
provenanceAttestationStatus === "PASS"
? "PASS"
: "FAIL_UNVERIFIED",
vulnerabilityStatus,
provenanceAttestationStatus,
failures: Object.freeze(failures),
});
}
function validateCommonContext(
label: "vulnerability report" | "provenance attestation",
evidence: z.infer<
| typeof vulnerabilityProviderReportSchema
| typeof provenanceProviderAttestationSchema
>,
expected: ExpectedPromotionContext,
expectedNonce: string,
trust: ProviderTrust | null,
now: number,
failures: string[],
): void {
if (
evidence.run.id !== expected.run.id ||
evidence.run.attempt !== expected.run.attempt
) {
failures.push(`${label} run identity mismatch`);
}
if (evidence.run.invocationNonce !== expectedNonce) {
failures.push(`${label} invocation nonce mismatch`);
}
if (
evidence.source.revision !== expected.source.revision ||
evidence.source.sourceSetSha256 !== expected.source.sourceSetSha256
) {
failures.push(`${label} source identity mismatch`);
}
if (JSON.stringify(evidence.candidate) !== JSON.stringify(expected.candidate)) {
failures.push(`${label} candidate identity mismatch`);
}
validateEvidenceTime(label, evidence.issuedAt, evidence.expiresAt, now, failures);
if (
!trust ||
evidence.signature.keyId !== trust.keyId ||
evidence.signature.publicKeyFingerprint !== trust.publicKeyFingerprint
) {
failures.push(`${label} trust identity mismatch`);
return;
}
try {
if (
providerPublicKeyFingerprint(trust.publicKey) !== trust.publicKeyFingerprint ||
!verify(
null,
providerEvidenceSignaturePayload(evidence),
trust.publicKey,
Buffer.from(evidence.signature.value, "base64"),
)
) {
failures.push(`${label} signature verification failed`);
}
} catch {
failures.push(`${label} signature verification failed`);
}
}
function validateEvidenceTime(
label: string,
issuedAt: string,
expiresAt: string,
now: number,
failures: string[],
): void {
const issued = Date.parse(issuedAt);
const expires = Date.parse(expiresAt);
if (issued > now + PROVIDER_FUTURE_SKEW_MS) {
failures.push(`${label} issuedAt exceeds allowed future skew`);
}
if (expires <= now) failures.push(`${label} is expired`);
if (expires <= issued) failures.push(`${label} validity window is not positive`);
if (expires - issued > PROVIDER_MAX_LIFETIME_MS) {
failures.push(`${label} validity window exceeds two hours`);
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
+763
View File
@@ -0,0 +1,763 @@
import { spawn } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";
import { constants } from "node:fs";
import { lstat, open, type FileHandle } from "node:fs/promises";
import path from "node:path";
import {
decodeProviderGuardianPublished,
decodeProviderGuardianReady,
encodeProviderGuardianCommit,
encodeProviderGuardianGuard,
encodeProviderGuardianPublish,
MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES,
MAX_PROVIDER_GUARDIAN_LEASE_MS,
MAX_PROVIDER_SEALED_BYTES,
providerGuardianRawStagingLeaf,
providerGuardianSealedTempLeaf,
type ProviderGuardianKind,
} from "./provider-guardian-protocol.ts";
import { cleanupOwnedProviderReport } from "./provider-raw-cleanup.ts";
const RESPONSE_TIMEOUT_MS = 5_000;
const CLOSE_TIMEOUT_MS = 5_000;
const MAX_CONTROL_OUTPUT_BYTES = 4_096;
type OwnedIdentity = Readonly<{ dev: number; ino: number }>;
type RecoveryAuthority = Readonly<{
rawDirectoryHandle: FileHandle;
evidenceDirectoryHandle: FileHandle;
rawStagingHandle: FileHandle;
sealedTempHandle: FileHandle;
rawIdentity: OwnedIdentity;
sealedIdentity: OwnedIdentity;
rawStagingPinnedPath: string;
rawPinnedPath: string;
sealedTempPinnedPath: string;
sealedPinnedPath: string;
}>;
export type ProviderGuardianLease = Readonly<{
pid: number;
rawPath: string;
rawIdentity: OwnedIdentity;
sealedPath: string;
sealedTempPath: string;
sealedIdentity: OwnedIdentity;
prematureExit: Promise<Error>;
publish(bytes: Buffer): Promise<void>;
commit(): Promise<void>;
abort(): Promise<void>;
}>;
export type StartProviderGuardianInput = Readonly<{
kind: ProviderGuardianKind;
workspaceRoot: string;
leaseMs: number;
guardianScript: string;
}>;
export type ProviderScopeGuardianLatch = Readonly<{
activeFailure: Promise<Error>;
close(): Promise<void>;
failure(): Error | undefined;
}>;
export function createProviderScopeGuardianLatch(
guardianExit: Promise<Error>,
): ProviderScopeGuardianLatch {
let active = true;
let closing: Promise<void> | undefined;
let observedFailure: Error | undefined;
let signalActiveFailure!: (error: Error) => void;
const activeFailure = new Promise<Error>((resolve) => { signalActiveFailure = resolve; });
void guardianExit.then((error) => {
observedFailure = error;
if (active) signalActiveFailure(error);
});
return Object.freeze({
activeFailure,
close: () => {
closing ??= Promise.resolve().then(() => { active = false; });
return closing;
},
failure: () => observedFailure,
});
}
export function assertProviderGuardianLeasePaths(
lease: Readonly<{ rawPath: string; sealedPath: string }>,
expected: Readonly<{ rawPath: string; sealedPath: string }>,
): void {
if (lease.rawPath !== expected.rawPath) {
throw new Error("provider guardian returned a noncanonical raw path");
}
if (lease.sealedPath !== expected.sealedPath) {
throw new Error("provider guardian returned a noncanonical sealed path");
}
}
type GuardianResult = Readonly<{
code: number | null;
error?: Error;
signal: NodeJS.Signals | null;
}>;
export async function startProviderGuardian(
input: StartProviderGuardianInput,
): Promise<ProviderGuardianLease> {
if (
(input.kind !== "vulnerability" && input.kind !== "provenance") ||
!path.isAbsolute(input.workspaceRoot) || !path.isAbsolute(input.guardianScript) ||
!Number.isSafeInteger(input.leaseMs) || input.leaseMs <= 0 ||
input.leaseMs > MAX_PROVIDER_GUARDIAN_LEASE_MS
) {
throw new TypeError("provider guardian client input is invalid");
}
const rawLeaf = input.kind === "vulnerability"
? "vulnerability-report.json"
: "provenance-attestation.json";
const evidenceRoot = path.resolve(input.workspaceRoot, "provider-evidence");
const rawDirectory = path.join(evidenceRoot, "untrusted");
const rawPath = path.join(evidenceRoot, "untrusted", rawLeaf);
const sealedPath = path.join(evidenceRoot, rawLeaf);
const nonce = randomBytes(32);
const rawStagingLeaf = providerGuardianRawStagingLeaf(input.kind, nonce);
const sealedTempLeaf = providerGuardianSealedTempLeaf(input.kind, nonce);
const sealedTempPath = path.join(evidenceRoot, sealedTempLeaf);
const recovery = await openRecoveryAuthority({
rawDirectory,
evidenceRoot,
rawLeaf,
rawStagingLeaf,
sealedLeaf: rawLeaf,
sealedTempLeaf,
});
let child: ReturnType<typeof spawn>;
try {
await assertRecoveryLeavesMissing(recovery);
child = spawn(process.execPath, [input.guardianScript], {
cwd: input.workspaceRoot,
env: {},
stdio: [
"pipe",
"pipe",
"pipe",
recovery.rawDirectoryHandle.fd,
recovery.evidenceDirectoryHandle.fd,
recovery.rawStagingHandle.fd,
recovery.sealedTempHandle.fd,
],
});
} catch (error) {
return await closeRecoveryAndThrow(recovery, error);
}
if (!child.pid || !child.stdin || !child.stdout || !child.stderr) {
child.kill("SIGKILL");
return await closeRecoveryAndThrow(
recovery,
new Error("provider guardian process pipes are unavailable"),
);
}
let state: "starting" | "guarding" | "publishing" | "published" |
"committing" | "aborting" | "terminated" = "starting";
let stderr = Buffer.alloc(0);
let inputError: Error | undefined;
child.stdin.once("error", (error) => { inputError = error; });
child.stderr.on("data", (chunk: Buffer | string) => {
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
if (stderr.byteLength < MAX_CONTROL_OUTPUT_BYTES) {
stderr = Buffer.concat([stderr, bytes.subarray(0, MAX_CONTROL_OUTPUT_BYTES - stderr.byteLength)]);
}
});
const completion = guardianCompletion(child);
let signalPrematureExit!: (error: Error) => void;
const prematureExit = new Promise<Error>((resolve) => { signalPrematureExit = resolve; });
void completion.then((result) => {
if (state === "guarding" || state === "publishing" || state === "published") {
signalPrematureExit(guardianCloseError(result, stderr));
}
});
let ready: ReturnType<typeof decodeProviderGuardianReady>;
try {
const readyResponse = waitForFrame(child.stdout, completion, "READY");
child.stdin.write(encodeProviderGuardianGuard({
kind: input.kind,
nonce,
deadlineEpochMs: Date.now() + input.leaseMs,
}));
ready = decodeProviderGuardianReady(await readyResponse, nonce);
if (
ready.sealedTempLeaf !== sealedTempLeaf ||
ready.rawDev !== recovery.rawIdentity.dev ||
ready.rawIno !== recovery.rawIdentity.ino ||
ready.sealedDev !== recovery.sealedIdentity.dev ||
ready.sealedIno !== recovery.sealedIdentity.ino
) {
throw new TypeError("provider guardian READY identity is invalid for its allocation");
}
await assertPinnedLeafIdentity(recovery.rawPinnedPath, {
dev: ready.rawDev,
ino: ready.rawIno,
}, 0o600);
await assertPinnedLeafIdentity(recovery.sealedTempPinnedPath, {
dev: ready.sealedDev,
ino: ready.sealedIno,
}, 0o600);
await assertPinnedLeafMissing(recovery.rawStagingPinnedPath);
if (child.exitCode !== null || child.signalCode !== null) {
throw guardianCloseError(await completion, stderr);
}
state = "guarding";
} catch (error) {
state = "aborting";
child.stdin.end();
const failures = [toError(error)];
try {
await waitForClose(completion, child);
} catch (closeError) {
failures.push(toError(closeError));
}
try {
await cleanupStartupRecovery(recovery);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
state = "terminated";
if (failures.length > 1) {
throw new AggregateError(failures, "provider guardian startup failed", { cause: error });
}
throw failures[0]!;
}
const rawIdentity = Object.freeze({ dev: ready.rawDev, ino: ready.rawIno });
const sealedIdentity = Object.freeze({ dev: ready.sealedDev, ino: ready.sealedIno });
const fallback = Object.freeze({
rawStagingPath: recovery.rawStagingPinnedPath,
rawPath: recovery.rawPinnedPath,
rawIdentity,
sealedPath: recovery.sealedPinnedPath,
sealedTempPath: recovery.sealedTempPinnedPath,
sealedIdentity,
});
const publish = async (bytes: Buffer): Promise<void> => {
if (state !== "guarding") throw new Error("provider guardian lease is not ready to publish");
if (!Buffer.isBuffer(bytes) || bytes.byteLength <= 0 || bytes.byteLength > MAX_PROVIDER_SEALED_BYTES) {
throw new TypeError("provider guardian sealed bytes are invalid");
}
state = "publishing";
try {
await writePinnedSealedBytes(recovery.sealedTempHandle, sealedIdentity, bytes);
const publishedResponse = waitForFrame(child.stdout!, completion, "PUBLISHED");
child.stdin!.write(encodeProviderGuardianPublish({
nonce,
sealedDev: sealedIdentity.dev,
sealedIno: sealedIdentity.ino,
size: bytes.byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
}));
decodeProviderGuardianPublished(
await publishedResponse,
nonce,
sealedIdentity,
);
if (inputError) throw inputError;
state = "published";
} catch (error) {
state = "guarding";
throw error;
}
};
const commit = async (): Promise<void> => {
if (state !== "published") throw new Error("provider guardian lease is not ready to commit");
state = "committing";
child.stdin!.write(encodeProviderGuardianCommit(nonce));
child.stdin!.end();
let result: GuardianResult;
try {
result = await waitForClose(completion, child);
} catch (error) {
state = "terminated";
return await cleanupFallbackCloseAndThrow(fallback, recovery, error);
}
state = "terminated";
if (inputError) return await cleanupFallbackCloseAndThrow(fallback, recovery, inputError);
if (result.error || result.code !== 0 || result.signal !== null) {
return await cleanupFallbackCloseAndThrow(
fallback,
recovery,
guardianCloseError(result, stderr),
);
}
await closeRecoveryAuthority(recovery);
};
const abort = async (): Promise<void> => {
if (state !== "guarding" && state !== "published") {
throw new Error("provider guardian lease already terminated");
}
state = "aborting";
child.stdin!.end();
let closeError: unknown;
try {
await waitForClose(completion, child);
} catch (error) {
closeError = error;
}
state = "terminated";
if (closeError) return await cleanupFallbackCloseAndThrow(fallback, recovery, closeError);
await cleanupFallbackAndClose(fallback, recovery);
};
return Object.freeze({
pid: child.pid,
rawPath,
rawIdentity,
sealedPath,
sealedTempPath,
sealedIdentity,
prematureExit,
publish,
commit,
abort,
});
}
async function writePinnedSealedBytes(
handle: FileHandle,
identity: OwnedIdentity,
bytes: Buffer,
): Promise<void> {
assertPinnedMetadata(await handle.stat(), identity, 0o600, 0);
await handle.truncate(0);
await handle.writeFile(bytes);
await handle.chmod(0o400);
await handle.sync();
assertPinnedMetadata(await handle.stat(), identity, 0o400, bytes.byteLength);
}
function assertPinnedMetadata(
metadata: Awaited<ReturnType<Awaited<ReturnType<typeof open>>["stat"]>>,
identity: OwnedIdentity,
mode: number,
size: number,
): void {
if (
!metadata.isFile() || Number(metadata.dev) !== identity.dev ||
Number(metadata.ino) !== identity.ino || Number(metadata.nlink) !== 1 ||
(Number(metadata.mode) & 0o777) !== mode || Number(metadata.size) !== size
) {
throw new TypeError("provider guardian sealed temp identity changed");
}
}
function guardianCompletion(child: ReturnType<typeof spawn>): Promise<GuardianResult> {
return new Promise((resolve) => {
child.once("error", (error) => resolve({ code: null, error, signal: null }));
child.once("close", (code, signal) => resolve({ code, signal }));
});
}
async function waitForFrame(
stdout: NodeJS.ReadableStream,
completion: Promise<GuardianResult>,
label: string,
): Promise<Buffer> {
let timer: NodeJS.Timeout | undefined;
let pending = Buffer.alloc(0);
const response = new Promise<Buffer>((resolve, reject) => {
const onData = (chunk: Buffer | string): void => {
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
if (pending.byteLength > MAX_CONTROL_OUTPUT_BYTES) {
reject(new Error(`provider guardian ${label} output exceeded its bound`));
return;
}
if (pending.byteLength < 4) return;
const payloadBytes = pending.readUInt32BE(0);
if (payloadBytes <= 0 || payloadBytes > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
reject(new Error(`provider guardian ${label} frame length is invalid`));
return;
}
if (pending.byteLength < payloadBytes + 4) return;
if (pending.byteLength !== payloadBytes + 4) {
reject(new Error(`provider guardian ${label} output has trailing bytes`));
return;
}
resolve(pending.subarray(4));
};
stdout.on("data", onData);
});
try {
return await Promise.race([
response,
completion.then((result) => { throw guardianCloseError(result, Buffer.alloc(0)); }),
new Promise<never>((_resolve, reject) => {
timer = setTimeout(
() => reject(new Error(`provider guardian ${label} timed out`)),
RESPONSE_TIMEOUT_MS,
);
}),
]);
} finally {
if (timer) clearTimeout(timer);
stdout.removeAllListeners("data");
}
}
async function waitForClose(
completion: Promise<GuardianResult>,
child: ReturnType<typeof spawn>,
): Promise<GuardianResult> {
let timer: NodeJS.Timeout | undefined;
try {
return await Promise.race([
completion,
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error("provider guardian did not close within its bound"));
}, CLOSE_TIMEOUT_MS);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
type FallbackIdentity = Readonly<{
rawStagingPath: string;
rawPath: string;
rawIdentity: OwnedIdentity;
sealedPath: string;
sealedTempPath: string;
sealedIdentity: OwnedIdentity;
}>;
async function cleanupFallback(input: FallbackIdentity): Promise<void> {
const failures: Error[] = [];
for (const target of [
{ path: input.rawStagingPath, identity: input.rawIdentity },
{ path: input.rawPath, identity: input.rawIdentity },
{ path: input.sealedTempPath, identity: input.sealedIdentity },
{ path: input.sealedPath, identity: input.sealedIdentity },
]) {
try {
await cleanupOwnedProviderReport({
reportPath: target.path,
reportDev: target.identity.dev,
reportIno: target.identity.ino,
});
} catch (error) {
failures.push(toError(error));
}
}
if (failures.length > 0) {
throw new AggregateError(failures, "provider guardian fallback cleanup failed", {
cause: failures[0],
});
}
}
async function cleanupFallbackCloseAndThrow(
fallback: FallbackIdentity,
recovery: RecoveryAuthority,
primaryError: unknown,
): Promise<never> {
const failures = [toError(primaryError)];
try {
await cleanupFallback(fallback);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
if (failures.length > 1) {
throw new AggregateError(failures, "provider guardian failure and recovery failed", {
cause: failures[0],
});
}
throw failures[0]!;
}
async function cleanupFallbackAndClose(
fallback: FallbackIdentity,
recovery: RecoveryAuthority,
): Promise<void> {
const failures: Error[] = [];
try {
await cleanupFallback(fallback);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
if (failures.length > 0) {
throw new AggregateError(failures, "provider guardian abort recovery failed", {
cause: failures[0],
});
}
}
async function openRecoveryAuthority(input: Readonly<{
rawDirectory: string;
evidenceRoot: string;
rawLeaf: string;
rawStagingLeaf: string;
sealedLeaf: string;
sealedTempLeaf: string;
}>): Promise<RecoveryAuthority> {
let rawDirectoryHandle: FileHandle | undefined;
let evidenceDirectoryHandle: FileHandle | undefined;
let rawStagingHandle: FileHandle | undefined;
let sealedTempHandle: FileHandle | undefined;
let rawIdentity: OwnedIdentity | undefined;
let sealedIdentity: OwnedIdentity | undefined;
let rawStagingPinnedPath: string | undefined;
let rawPinnedPath: string | undefined;
let sealedTempPinnedPath: string | undefined;
let sealedPinnedPath: string | undefined;
try {
rawDirectoryHandle = await open(
input.rawDirectory,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
await assertPinnedDirectory(rawDirectoryHandle, input.rawDirectory, "raw");
evidenceDirectoryHandle = await open(
input.evidenceRoot,
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
);
await assertPinnedDirectory(evidenceDirectoryHandle, input.evidenceRoot, "evidence");
rawStagingPinnedPath =
`/proc/self/fd/${rawDirectoryHandle.fd}/${input.rawStagingLeaf}`;
rawPinnedPath = `/proc/self/fd/${rawDirectoryHandle.fd}/${input.rawLeaf}`;
sealedTempPinnedPath =
`/proc/self/fd/${evidenceDirectoryHandle.fd}/${input.sealedTempLeaf}`;
sealedPinnedPath = `/proc/self/fd/${evidenceDirectoryHandle.fd}/${input.sealedLeaf}`;
rawStagingHandle = await open(
rawStagingPinnedPath,
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
0o600,
);
const rawMetadata = await rawStagingHandle.stat();
rawIdentity = Object.freeze({ dev: rawMetadata.dev, ino: rawMetadata.ino });
assertAllocatedPrivateMetadata(rawMetadata, rawIdentity, "raw staging");
await assertPinnedLeafIdentity(rawStagingPinnedPath, rawIdentity, 0o600);
sealedTempHandle = await open(
sealedTempPinnedPath,
constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
0o600,
);
const sealedMetadata = await sealedTempHandle.stat();
sealedIdentity = Object.freeze({ dev: sealedMetadata.dev, ino: sealedMetadata.ino });
assertAllocatedPrivateMetadata(sealedMetadata, sealedIdentity, "sealed temp");
await assertPinnedLeafIdentity(sealedTempPinnedPath, sealedIdentity, 0o600);
return Object.freeze({
rawDirectoryHandle,
evidenceDirectoryHandle,
rawStagingHandle,
sealedTempHandle,
rawIdentity,
sealedIdentity,
rawStagingPinnedPath,
rawPinnedPath,
sealedTempPinnedPath,
sealedPinnedPath,
});
} catch (error) {
const failures = [toError(error)];
for (const target of [
{ path: rawStagingPinnedPath, identity: rawIdentity },
{ path: rawPinnedPath, identity: rawIdentity },
{ path: sealedTempPinnedPath, identity: sealedIdentity },
{ path: sealedPinnedPath, identity: sealedIdentity },
]) {
if (!target.path || !target.identity) continue;
try {
await cleanupOwnedProviderReport({
reportPath: target.path,
reportDev: target.identity.dev,
reportIno: target.identity.ino,
});
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
}
for (const handle of [
sealedTempHandle,
rawStagingHandle,
evidenceDirectoryHandle,
rawDirectoryHandle,
]) {
if (!handle) continue;
try { await handle.close(); } catch (closeError) { failures.push(toError(closeError)); }
}
if (failures.length > 1) {
throw new AggregateError(failures, "provider guardian recovery setup failed", {
cause: error,
});
}
throw failures[0]!;
}
}
function assertAllocatedPrivateMetadata(
metadata: Awaited<ReturnType<FileHandle["stat"]>>,
identity: OwnedIdentity,
label: string,
): void {
if (
!metadata.isFile() || Number(metadata.dev) !== identity.dev ||
Number(metadata.ino) !== identity.ino || Number(metadata.nlink) !== 1 ||
(Number(metadata.mode) & 0o777) !== 0o600 || Number(metadata.size) !== 0
) {
throw new TypeError(`provider guardian ${label} allocation is invalid`);
}
}
async function assertPinnedDirectory(
handle: FileHandle,
canonicalPath: string,
label: string,
): Promise<void> {
const [descriptorMetadata, pathMetadata] = await Promise.all([
handle.stat(),
lstat(canonicalPath),
]);
if (
!descriptorMetadata.isDirectory() || !pathMetadata.isDirectory() ||
pathMetadata.isSymbolicLink() || descriptorMetadata.dev !== pathMetadata.dev ||
descriptorMetadata.ino !== pathMetadata.ino
) {
throw new TypeError(`provider guardian ${label} recovery directory identity changed`);
}
}
async function assertRecoveryLeavesMissing(recovery: RecoveryAuthority): Promise<void> {
await assertPinnedLeafMissing(recovery.rawPinnedPath);
await assertPinnedLeafMissing(recovery.sealedPinnedPath);
assertAllocatedPrivateMetadata(
await recovery.rawStagingHandle.stat(),
recovery.rawIdentity,
"raw staging",
);
assertAllocatedPrivateMetadata(
await recovery.sealedTempHandle.stat(),
recovery.sealedIdentity,
"sealed temp",
);
await assertPinnedLeafIdentity(
recovery.rawStagingPinnedPath,
recovery.rawIdentity,
0o600,
);
await assertPinnedLeafIdentity(
recovery.sealedTempPinnedPath,
recovery.sealedIdentity,
0o600,
);
}
async function assertPinnedLeafMissing(target: string): Promise<void> {
try {
await lstat(target);
throw new Error("provider guardian transaction leaf already exists");
} catch (error) {
if (!hasErrorCode(error, "ENOENT")) throw error;
}
}
async function assertPinnedLeafIdentity(
target: string,
identity: OwnedIdentity,
mode: number,
): Promise<void> {
const metadata = await lstat(target);
if (
!metadata.isFile() || metadata.isSymbolicLink() || metadata.dev !== identity.dev ||
metadata.ino !== identity.ino || metadata.nlink !== 1 ||
(metadata.mode & 0o777) !== mode || metadata.size !== 0
) {
throw new TypeError("provider guardian READY identity changed");
}
}
async function cleanupStartupRecovery(recovery: RecoveryAuthority): Promise<void> {
await cleanupFallback({
rawStagingPath: recovery.rawStagingPinnedPath,
rawPath: recovery.rawPinnedPath,
rawIdentity: recovery.rawIdentity,
sealedTempPath: recovery.sealedTempPinnedPath,
sealedPath: recovery.sealedPinnedPath,
sealedIdentity: recovery.sealedIdentity,
});
}
async function closeRecoveryAuthority(recovery: RecoveryAuthority): Promise<void> {
const failures: Error[] = [];
for (const handle of [
recovery.rawStagingHandle,
recovery.sealedTempHandle,
recovery.rawDirectoryHandle,
recovery.evidenceDirectoryHandle,
]) {
try { await handle.close(); } catch (error) { failures.push(toError(error)); }
}
if (failures.length > 0) {
throw new AggregateError(failures, "provider guardian recovery directory close failed", {
cause: failures[0],
});
}
}
async function closeRecoveryAndThrow(
recovery: RecoveryAuthority,
primaryError: unknown,
): Promise<never> {
const failures = [toError(primaryError)];
try {
await cleanupStartupRecovery(recovery);
} catch (cleanupError) {
failures.push(toError(cleanupError));
}
try {
await closeRecoveryAuthority(recovery);
} catch (closeError) {
failures.push(toError(closeError));
}
if (failures.length > 1) {
throw new AggregateError(failures,
"provider guardian failure and recovery close failed", { cause: failures[0] });
}
throw failures[0]!;
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
function guardianCloseError(result: GuardianResult, stderr: Buffer): Error {
if (result.error) return result.error;
const detail = stderr.toString("utf8").trim();
return new Error(
`provider guardian failed: exit=${result.code ?? "none"}, signal=${result.signal ?? "none"}${detail ? `, output=${detail}` : ""}`,
);
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
+396
View File
@@ -0,0 +1,396 @@
import { timingSafeEqual } from "node:crypto";
export type ProviderGuardianKind = "vulnerability" | "provenance";
export const MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES = 4_092;
export const MAX_PROVIDER_GUARDIAN_LEASE_MS = 40 * 60 * 1_000;
export const MAX_PROVIDER_SEALED_BYTES = 8_388_608;
const V2_GUARD_KEYS = ["type", "version", "kind", "nonce", "deadlineEpochMs"] as const;
const READY_KEYS = [
"type", "version", "nonce", "rawDev", "rawIno",
"sealedTempLeaf", "sealedDev", "sealedIno",
] as const;
const PUBLISH_KEYS = [
"type", "version", "nonce", "sealedDev", "sealedIno", "size", "sha256",
] as const;
const PUBLISHED_KEYS = ["type", "version", "nonce", "sealedDev", "sealedIno"] as const;
const COMMIT_KEYS = ["type", "version", "nonce"] as const;
export type ProviderGuardianGuard = Readonly<{
kind: ProviderGuardianKind;
nonce: Buffer;
deadlineEpochMs: number;
}>;
export type ProviderGuardianReady = Readonly<{
nonce: Buffer;
rawDev: number;
rawIno: number;
sealedTempLeaf: string;
sealedDev: number;
sealedIno: number;
}>;
export type ProviderGuardianPublish = Readonly<{
nonce: Buffer;
sealedDev: number;
sealedIno: number;
size: number;
sha256: string;
}>;
export type ProviderGuardianPublished = Readonly<{
nonce: Buffer;
sealedDev: number;
sealedIno: number;
}>;
export function providerGuardianSealedTempLeaf(
kind: ProviderGuardianKind,
nonce: Buffer,
): string {
const rawLeaf = baseLeaf(kind);
assertV2Nonce(nonce);
return `.${rawLeaf}.guardian-${nonce.subarray(0, 16).toString("hex")}.tmp`;
}
export function providerGuardianRawStagingLeaf(
kind: ProviderGuardianKind,
nonce: Buffer,
): string {
const rawLeaf = baseLeaf(kind);
assertV2Nonce(nonce);
return `.${rawLeaf}.guardian-${nonce.subarray(0, 16).toString("hex")}.raw.tmp`;
}
export function encodeProviderGuardianGuard(input: ProviderGuardianGuard): Buffer {
assertV2Guard(input);
return prefixFrame(encodeV2GuardPayload(input));
}
export function decodeProviderGuardianGuard(
payload: Buffer,
options: Readonly<{ nowEpochMs: number; maxLeaseMs: number }>,
): ProviderGuardianGuard {
const value = parseRecord(payload, V2_GUARD_KEYS, "guard");
const guard: ProviderGuardianGuard = {
kind: value.kind as ProviderGuardianKind,
nonce: parseV2Nonce(value.nonce),
deadlineEpochMs: value.deadlineEpochMs as number,
};
if (value.type !== "guard" || value.version !== 2) {
throw new TypeError("provider guardian guard version is invalid");
}
assertV2Guard(guard);
if (
!Number.isSafeInteger(options.nowEpochMs) ||
!Number.isSafeInteger(options.maxLeaseMs) || options.maxLeaseMs <= 0 ||
guard.deadlineEpochMs <= options.nowEpochMs ||
guard.deadlineEpochMs > options.nowEpochMs + options.maxLeaseMs
) {
throw new TypeError("provider guardian guard deadline is invalid");
}
assertCanonical(payload, encodeV2GuardPayload(guard), "guard");
return guard;
}
export function encodeProviderGuardianReady(input: ProviderGuardianReady): Buffer {
assertReady(input);
return prefixFrame(encodeReadyPayload(input));
}
export function decodeProviderGuardianReady(
payload: Buffer,
expectedNonce: Buffer,
): ProviderGuardianReady {
assertV2Nonce(expectedNonce);
const value = parseRecord(payload, READY_KEYS, "READY");
const ready: ProviderGuardianReady = {
nonce: parseV2Nonce(value.nonce),
rawDev: value.rawDev as number,
rawIno: value.rawIno as number,
sealedTempLeaf: value.sealedTempLeaf as string,
sealedDev: value.sealedDev as number,
sealedIno: value.sealedIno as number,
};
if (value.type !== "ready" || value.version !== 2) {
throw new TypeError("provider guardian READY version is invalid");
}
assertReady(ready);
assertAuthenticatedNonce(ready.nonce, expectedNonce, "READY");
assertCanonical(payload, encodeReadyPayload(ready), "READY");
return ready;
}
export function encodeProviderGuardianPublish(input: ProviderGuardianPublish): Buffer {
assertPublish(input);
return prefixFrame(encodePublishPayload(input));
}
export function decodeProviderGuardianPublish(
payload: Buffer,
expectedNonce: Buffer,
): ProviderGuardianPublish {
assertV2Nonce(expectedNonce);
const value = parseRecord(payload, PUBLISH_KEYS, "publish");
const publish: ProviderGuardianPublish = {
nonce: parseV2Nonce(value.nonce),
sealedDev: value.sealedDev as number,
sealedIno: value.sealedIno as number,
size: value.size as number,
sha256: value.sha256 as string,
};
if (value.type !== "publish" || value.version !== 2) {
throw new TypeError("provider guardian publish version is invalid");
}
assertPublish(publish);
assertAuthenticatedNonce(publish.nonce, expectedNonce, "publish");
assertCanonical(payload, encodePublishPayload(publish), "publish");
return publish;
}
export function encodeProviderGuardianPublished(input: ProviderGuardianPublished): Buffer {
assertPublished(input);
return prefixFrame(encodePublishedPayload(input));
}
export function decodeProviderGuardianPublished(
payload: Buffer,
expectedNonce: Buffer,
expectedIdentity: Readonly<{ dev: number; ino: number }>,
): void {
assertV2Nonce(expectedNonce);
const value = parseRecord(payload, PUBLISHED_KEYS, "PUBLISHED");
const published: ProviderGuardianPublished = {
nonce: parseV2Nonce(value.nonce),
sealedDev: value.sealedDev as number,
sealedIno: value.sealedIno as number,
};
if (value.type !== "published" || value.version !== 2) {
throw new TypeError("provider guardian PUBLISHED version is invalid");
}
assertPublished(published);
assertAuthenticatedNonce(published.nonce, expectedNonce, "PUBLISHED");
if (published.sealedDev !== expectedIdentity.dev || published.sealedIno !== expectedIdentity.ino) {
throw new TypeError("provider guardian PUBLISHED identity is invalid");
}
assertCanonical(payload, encodePublishedPayload(published), "PUBLISHED");
}
function encodeV2GuardPayload(input: ProviderGuardianGuard): Buffer {
return Buffer.from(JSON.stringify({
type: "guard",
version: 2,
kind: input.kind,
nonce: input.nonce.toString("hex"),
deadlineEpochMs: input.deadlineEpochMs,
}));
}
export function encodeProviderGuardianCommit(nonce: Buffer): Buffer {
assertV2Nonce(nonce);
return prefixFrame(encodeCommitPayload(nonce));
}
export function decodeProviderGuardianCommit(payload: Buffer, expectedNonce: Buffer): void {
assertPayloadSize(payload);
assertV2Nonce(expectedNonce);
const decoded = decodeUtf8(payload);
let value: unknown;
try {
value = JSON.parse(decoded);
} catch {
throw new TypeError("provider guardian commit JSON is invalid");
}
if (!isRecord(value) || !hasExactKeys(value, COMMIT_KEYS)) {
throw new TypeError("provider guardian commit fields are invalid");
}
const nonce = parseV2Nonce(value.nonce);
if (
value.type !== "commit" || value.version !== 2 ||
nonce.byteLength !== expectedNonce.byteLength ||
!timingSafeEqual(nonce, expectedNonce)
) {
throw new TypeError("provider guardian commit authentication failed");
}
if (!payload.equals(encodeCommitPayload(nonce))) {
throw new TypeError("provider guardian commit is not canonical");
}
}
function encodeCommitPayload(nonce: Buffer): Buffer {
return Buffer.from(JSON.stringify({
type: "commit",
version: 2,
nonce: nonce.toString("hex"),
}));
}
function encodeReadyPayload(input: ProviderGuardianReady): Buffer {
return Buffer.from(JSON.stringify({
type: "ready",
version: 2,
nonce: input.nonce.toString("hex"),
rawDev: input.rawDev,
rawIno: input.rawIno,
sealedTempLeaf: input.sealedTempLeaf,
sealedDev: input.sealedDev,
sealedIno: input.sealedIno,
}));
}
function encodePublishPayload(input: ProviderGuardianPublish): Buffer {
return Buffer.from(JSON.stringify({
type: "publish",
version: 2,
nonce: input.nonce.toString("hex"),
sealedDev: input.sealedDev,
sealedIno: input.sealedIno,
size: input.size,
sha256: input.sha256,
}));
}
function encodePublishedPayload(input: ProviderGuardianPublished): Buffer {
return Buffer.from(JSON.stringify({
type: "published",
version: 2,
nonce: input.nonce.toString("hex"),
sealedDev: input.sealedDev,
sealedIno: input.sealedIno,
}));
}
function prefixFrame(payload: Buffer): Buffer {
if (payload.byteLength <= 0 || payload.byteLength > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
throw new TypeError("provider guardian frame size is invalid");
}
const frame = Buffer.allocUnsafe(payload.byteLength + 4);
frame.writeUInt32BE(payload.byteLength, 0);
payload.copy(frame, 4);
return frame;
}
function assertV2Guard(input: ProviderGuardianGuard): void {
if (
(input.kind !== "vulnerability" && input.kind !== "provenance") ||
!isV2Nonce(input.nonce) ||
!Number.isSafeInteger(input.deadlineEpochMs) || input.deadlineEpochMs <= 0
) {
throw new TypeError("provider guardian guard fields are invalid");
}
}
function baseLeaf(kind: ProviderGuardianKind): string {
if (kind === "vulnerability") return "vulnerability-report.json";
if (kind === "provenance") return "provenance-attestation.json";
throw new TypeError("provider guardian kind is invalid");
}
function assertReady(input: ProviderGuardianReady): void {
if (
!isV2Nonce(input.nonce) ||
!isIdentityPart(input.rawDev) || !isIdentityPart(input.rawIno) ||
typeof input.sealedTempLeaf !== "string" ||
!/^\.(?:vulnerability-report|provenance-attestation)\.json\.guardian-[0-9a-f]{32}\.tmp$/u
.test(input.sealedTempLeaf) ||
!isIdentityPart(input.sealedDev) || !isIdentityPart(input.sealedIno)
) {
throw new TypeError("provider guardian READY fields are invalid");
}
}
function assertPublish(input: ProviderGuardianPublish): void {
if (
!isV2Nonce(input.nonce) ||
!isIdentityPart(input.sealedDev) || !isIdentityPart(input.sealedIno) ||
!Number.isSafeInteger(input.size) || input.size <= 0 || input.size > MAX_PROVIDER_SEALED_BYTES ||
typeof input.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(input.sha256)
) {
throw new TypeError("provider guardian publish fields are invalid");
}
}
function assertPublished(input: ProviderGuardianPublished): void {
if (
!isV2Nonce(input.nonce) ||
!isIdentityPart(input.sealedDev) || !isIdentityPart(input.sealedIno)
) {
throw new TypeError("provider guardian PUBLISHED fields are invalid");
}
}
function assertV2Nonce(nonce: Buffer): void {
if (!isV2Nonce(nonce)) throw new TypeError("provider guardian nonce is invalid");
}
function isV2Nonce(nonce: Buffer): boolean {
return Buffer.isBuffer(nonce) && nonce.byteLength === 32;
}
function parseV2Nonce(value: unknown): Buffer {
return typeof value === "string" && /^[0-9a-f]{64}$/u.test(value)
? Buffer.from(value, "hex")
: Buffer.alloc(0);
}
function isIdentityPart(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) > 0;
}
function assertAuthenticatedNonce(received: Buffer, expected: Buffer, label: string): void {
if (received.byteLength !== expected.byteLength || !timingSafeEqual(received, expected)) {
throw new TypeError(`provider guardian ${label} authentication failed`);
}
}
function parseRecord(
payload: Buffer,
expectedKeys: readonly string[],
label: string,
): Record<string, unknown> {
assertPayloadSize(payload);
let value: unknown;
try {
value = JSON.parse(decodeUtf8(payload));
} catch (error) {
if (error instanceof TypeError && /provider guardian/u.test(error.message)) throw error;
throw new TypeError(`provider guardian ${label} JSON is invalid`, { cause: error });
}
if (!isRecord(value) || !hasExactKeys(value, expectedKeys)) {
throw new TypeError(`provider guardian ${label} fields are invalid`);
}
return value;
}
function assertCanonical(payload: Buffer, canonical: Buffer, label: string): void {
if (!payload.equals(canonical)) {
throw new TypeError(`provider guardian ${label} frame is not canonical`);
}
}
function assertPayloadSize(payload: Buffer): void {
if (!Buffer.isBuffer(payload) || payload.byteLength <= 0 || payload.byteLength > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
throw new TypeError("provider guardian frame size is invalid");
}
}
function decodeUtf8(payload: Buffer): string {
let decoded: string;
try {
decoded = new TextDecoder("utf-8", { fatal: true }).decode(payload);
} catch {
throw new TypeError("provider guardian frame UTF-8 is invalid");
}
if (decoded.includes("\0")) throw new TypeError("provider guardian frame contains NUL");
return decoded;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function hasExactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
const keys = Object.keys(value);
return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
}
+27
View File
@@ -0,0 +1,27 @@
export type ProviderOutputLimiter = Readonly<{
consume(chunk: Buffer | string): void;
bytes(): number;
}>;
export function createProviderOutputLimiter(
maxBytes: number,
onExceeded: () => void,
): ProviderOutputLimiter {
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || typeof onExceeded !== "function") {
throw new TypeError("provider output limiter input is invalid");
}
let observedBytes = 0;
let exceeded = false;
return Object.freeze({
consume: (chunk: Buffer | string) => {
if (exceeded) return;
const bytes = Buffer.isBuffer(chunk) ? chunk.byteLength : Buffer.byteLength(chunk);
observedBytes += bytes;
if (observedBytes > maxBytes) {
exceeded = true;
onExceeded();
}
},
bytes: () => observedBytes,
});
}
+103
View File
@@ -0,0 +1,103 @@
import { spawn, type ChildProcess } from "node:child_process";
export type ProviderProcessInput = Readonly<{
executable: string;
arguments: readonly string[];
environment: NodeJS.ProcessEnv;
timeoutMs: number;
}>;
type ProviderChild = Pick<ChildProcess, "kill" | "once" | "pid">;
export async function runProviderProcess(
input: ProviderProcessInput,
dependencies: Readonly<{
spawnChild?: (input: ProviderProcessInput) => ProviderChild;
setTimer?: (callback: () => void, milliseconds: number) => ReturnType<typeof setTimeout>;
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
killProcessGroup?: (child: ProviderChild) => void;
}> = {},
): Promise<void> {
if (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs <= 0) {
throw new TypeError("provider process timeout must be a positive integer");
}
const child = (dependencies.spawnChild ?? defaultSpawn)(input);
const setTimer = dependencies.setTimer ?? setTimeout;
const clearTimer = dependencies.clearTimer ?? clearTimeout;
const killProcessGroup = dependencies.killProcessGroup ?? defaultKillProcessGroup;
await new Promise<void>((resolve, reject) => {
let settled = false;
let timedOut = false;
const killFailures: unknown[] = [];
const settle = (error?: Error): void => {
if (settled) return;
settled = true;
clearTimer(timer);
error ? reject(error) : resolve();
};
const timer = setTimer(() => {
timedOut = true;
try {
killProcessGroup(child);
} catch (groupError) {
killFailures.push(groupError);
try {
child.kill("SIGKILL");
} catch (fallbackError) {
killFailures.push(fallbackError);
}
}
}, input.timeoutMs);
child.once("error", (error: Error) => {
if (!timedOut) settle(error);
});
child.once("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (timedOut) {
const timeoutError = new Error(
"sandboxed external provider command timed out after process close",
);
settle(
killFailures.length === 0
? timeoutError
: new AggregateError(
[timeoutError, ...killFailures],
"sandboxed external provider timed out and process-group kill failed before close",
{ cause: killFailures.at(-1) },
),
);
} else if (code === 0 && signal === null) {
settle();
} else {
settle(
new Error(
`sandboxed external provider failed: exit=${code ?? "none"}, signal=${signal ?? "none"}`,
),
);
}
});
});
}
function defaultSpawn(input: ProviderProcessInput): ProviderChild {
return spawn(input.executable, [...input.arguments], {
env: input.environment,
stdio: "inherit",
detached: true,
});
}
function defaultKillProcessGroup(child: ProviderChild): void {
if (child.pid && child.pid > 0) {
try {
process.kill(-child.pid, "SIGKILL");
return;
} catch (error) {
if (!hasErrorCode(error, "ESRCH")) throw error;
}
}
child.kill("SIGKILL");
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+40
View File
@@ -0,0 +1,40 @@
import { randomBytes } from "node:crypto";
import { lstat, rename, unlink } from "node:fs/promises";
export type ProviderRawIdentity = Readonly<{
reportPath: string;
reportDev: number;
reportIno: number;
}>;
export async function cleanupOwnedProviderReport(
identity: ProviderRawIdentity,
): Promise<boolean> {
try {
const metadata = await lstat(identity.reportPath);
if (!matchesReportIdentity(metadata, identity)) return false;
const quarantine = `${identity.reportPath}.parent-loss-${process.pid}-${randomBytes(16).toString("hex")}`;
await rename(identity.reportPath, quarantine);
const quarantinedMetadata = await lstat(quarantine);
if (!matchesReportIdentity(quarantinedMetadata, identity)) {
throw new Error("provider raw output identity changed during parent-loss cleanup");
}
await unlink(quarantine);
return true;
} catch (error) {
if (hasErrorCode(error, "ENOENT")) return false;
throw error;
}
}
function matchesReportIdentity(
metadata: Awaited<ReturnType<typeof lstat>>,
identity: ProviderRawIdentity,
): boolean {
return metadata.isFile() && !metadata.isSymbolicLink() &&
metadata.dev === identity.reportDev && metadata.ino === identity.reportIno;
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+641
View File
@@ -0,0 +1,641 @@
import { createHash } from "node:crypto";
import {
closeSync,
fstatSync,
fsyncSync,
lstatSync,
readlinkSync,
readSync,
type Stats,
writeSync,
createReadStream,
} from "node:fs";
import { link, lstat, unlink } from "node:fs/promises";
import path from "node:path";
import {
decodeProviderGuardianCommit,
decodeProviderGuardianGuard,
decodeProviderGuardianPublish,
encodeProviderGuardianPublished,
encodeProviderGuardianReady,
MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES,
MAX_PROVIDER_GUARDIAN_LEASE_MS,
providerGuardianRawStagingLeaf,
providerGuardianSealedTempLeaf,
type ProviderGuardianGuard,
type ProviderGuardianKind,
} from "./provider-guardian-protocol.ts";
import { cleanupOwnedProviderReport } from "./provider-raw-cleanup.ts";
type OwnedIdentity = Readonly<{ dev: number; ino: number }>;
type BootstrapAuthority = Readonly<{
kind: ProviderGuardianKind;
noncePrefix: string;
rawStagingLeaf: string;
rawStagingPath: string;
rawPath: string;
rawIdentity: OwnedIdentity;
sealedTempLeaf: string;
sealedTempPath: string;
sealedPath: string;
sealedIdentity: OwnedIdentity;
}>;
type BoundPrivateAuthority = Readonly<{
identity: OwnedIdentity;
leaf: string;
noncePrefix: string;
path: string;
stem: string;
}>;
type GuardianTransaction = Readonly<{ guard: ProviderGuardianGuard }>;
const RAW_DIRECTORY_FD = 3;
const EVIDENCE_DIRECTORY_FD = 4;
const RAW_STAGING_FD = 5;
const SEALED_TEMP_FD = 6;
const RAW_DIRECTORY_PATH = `/proc/self/fd/${RAW_DIRECTORY_FD}`;
const EVIDENCE_DIRECTORY_PATH = `/proc/self/fd/${EVIDENCE_DIRECTORY_FD}`;
const RAW_STAGING_FD_PATH = `/proc/self/fd/${RAW_STAGING_FD}`;
const SEALED_TEMP_FD_PATH = `/proc/self/fd/${SEALED_TEMP_FD}`;
const RAW_STAGING_PATTERN =
/^\.(vulnerability-report|provenance-attestation)\.json\.guardian-([0-9a-f]{32})\.raw\.tmp$/u;
const SEALED_TEMP_PATTERN =
/^\.(vulnerability-report|provenance-attestation)\.json\.guardian-([0-9a-f]{32})\.tmp$/u;
let privateFdsClosed = false;
const bootstrap = await initializeBootstrap();
let pending = Buffer.alloc(0);
let expectedBytes: number | undefined;
let state: "starting" | "guarding" | "published" | "commitPending" = "starting";
let transaction: GuardianTransaction | undefined;
let terminal = false;
let deadline: NodeJS.Timeout | undefined;
let operations = Promise.resolve();
const liveness = createReadStream("", { fd: 0, autoClose: false });
liveness.on("data", consumeChunk);
liveness.once("end", () => {
enqueue(async () => {
if (state === "commitPending" && pending.byteLength === 0 && expectedBytes === undefined) {
await succeedOnCommittedEof();
return;
}
if (state === "starting" && pending.byteLength > 0) {
await failClosed(126, "provider guardian frame is truncated");
return;
}
await failClosed(125, "provider guardian liveness EOF");
});
});
liveness.once("error", (error) => {
enqueue(async () => failClosed(125, `provider guardian liveness error: ${error.message}`));
});
async function initializeBootstrap(): Promise<BootstrapAuthority> {
const rawDirectory = path.resolve(process.cwd(), "provider-evidence/untrusted");
const evidenceDirectory = path.resolve(process.cwd(), "provider-evidence");
const failures: Error[] = [];
const rawDirectoryValid = captureInheritedDirectory(
RAW_DIRECTORY_FD,
rawDirectory,
"raw",
failures,
);
const evidenceDirectoryValid = captureInheritedDirectory(
EVIDENCE_DIRECTORY_FD,
evidenceDirectory,
"evidence",
failures,
);
const rawDescriptor = capturePrivateDescriptor(RAW_STAGING_FD, "raw staging", failures);
const sealedDescriptor = capturePrivateDescriptor(SEALED_TEMP_FD, "sealed temp", failures);
const rawAuthority = rawDescriptor && rawDirectoryValid
? capturePrivateAlias({
descriptorMetadata: rawDescriptor,
descriptorTarget: RAW_STAGING_FD_PATH,
expectedDirectory: rawDirectory,
descriptorDirectory: RAW_DIRECTORY_PATH,
grammar: RAW_STAGING_PATTERN,
label: "raw staging",
}, failures)
: undefined;
const sealedAuthority = sealedDescriptor && evidenceDirectoryValid
? capturePrivateAlias({
descriptorMetadata: sealedDescriptor,
descriptorTarget: SEALED_TEMP_FD_PATH,
expectedDirectory: evidenceDirectory,
descriptorDirectory: EVIDENCE_DIRECTORY_PATH,
grammar: SEALED_TEMP_PATTERN,
label: "sealed temp",
}, failures)
: undefined;
if (!rawAuthority || !sealedAuthority) {
return await failBootstrap(rawAuthority, sealedAuthority, failures);
}
try {
if (
rawAuthority.stem !== sealedAuthority.stem ||
rawAuthority.noncePrefix !== sealedAuthority.noncePrefix
) {
throw new TypeError("provider guardian inherited private aliases disagree");
}
const kind = providerKindFromStem(rawAuthority.stem);
const rawLeaf = kind === "vulnerability"
? "vulnerability-report.json"
: "provenance-attestation.json";
return Object.freeze({
kind,
noncePrefix: rawAuthority.noncePrefix,
rawStagingLeaf: rawAuthority.leaf,
rawStagingPath: rawAuthority.path,
rawPath: `${RAW_DIRECTORY_PATH}/${rawLeaf}`,
rawIdentity: rawAuthority.identity,
sealedTempLeaf: sealedAuthority.leaf,
sealedTempPath: sealedAuthority.path,
sealedPath: `${EVIDENCE_DIRECTORY_PATH}/${rawLeaf}`,
sealedIdentity: sealedAuthority.identity,
});
} catch (error) {
failures.push(toError(error));
return await failBootstrap(rawAuthority, sealedAuthority, failures);
}
}
function captureInheritedDirectory(
fd: number,
canonicalPath: string,
label: string,
failures: Error[],
): boolean {
try {
assertInheritedDirectory(fd, canonicalPath, label);
return true;
} catch (error) {
failures.push(toError(error));
return false;
}
}
function capturePrivateDescriptor(
fd: number,
label: string,
failures: Error[],
): Stats | undefined {
try {
return fstatSync(fd);
} catch (error) {
failures.push(new Error(`provider guardian inherited ${label} fd is invalid`, {
cause: error,
}));
return undefined;
}
}
function capturePrivateAlias(
input: Readonly<{
descriptorMetadata: Stats;
descriptorTarget: string;
expectedDirectory: string;
descriptorDirectory: string;
grammar: RegExp;
label: string;
}>,
failures: Error[],
): BoundPrivateAuthority | undefined {
try {
return bindPrivateAlias(input);
} catch (error) {
failures.push(toError(error));
return undefined;
}
}
function bindPrivateAlias(input: Readonly<{
descriptorMetadata: Stats;
descriptorTarget: string;
expectedDirectory: string;
descriptorDirectory: string;
grammar: RegExp;
label: string;
}>): BoundPrivateAuthority {
const descriptorTarget = readlinkSync(input.descriptorTarget);
if (path.dirname(descriptorTarget) !== input.expectedDirectory) {
throw new TypeError(
`provider guardian inherited ${input.label} alias is outside its directory`,
);
}
const leaf = path.basename(descriptorTarget);
const match = input.grammar.exec(leaf);
if (!match) {
throw new TypeError(`provider guardian inherited ${input.label} alias is invalid`);
}
const boundPath = `${input.descriptorDirectory}/${leaf}`;
const pathnameMetadata = lstatSync(boundPath);
assertPrivateMetadata(input.descriptorMetadata, pathnameMetadata, input.label);
return Object.freeze({
identity: Object.freeze({
dev: input.descriptorMetadata.dev,
ino: input.descriptorMetadata.ino,
}),
leaf,
noncePrefix: match[2]!,
path: boundPath,
stem: match[1]!,
});
}
async function failBootstrap(
rawAuthority: BoundPrivateAuthority | undefined,
sealedAuthority: BoundPrivateAuthority | undefined,
failures: Error[],
): Promise<never> {
for (const authority of [rawAuthority, sealedAuthority]) {
if (!authority) continue;
await cleanupOwnedPath(authority.path, authority.identity, failures);
}
closePrivateFds(failures);
writeAggregateDiagnostic("provider guardian bootstrap failed", failures);
process.exit(126);
}
function assertPrivateMetadata(
descriptorMetadata: Stats,
pathnameMetadata: Stats,
label: string,
): void {
if (
!descriptorMetadata.isFile() || !pathnameMetadata.isFile() ||
pathnameMetadata.isSymbolicLink() || descriptorMetadata.dev !== pathnameMetadata.dev ||
descriptorMetadata.ino !== pathnameMetadata.ino || descriptorMetadata.nlink !== 1 ||
pathnameMetadata.nlink !== 1 || (descriptorMetadata.mode & 0o777) !== 0o600 ||
(pathnameMetadata.mode & 0o777) !== 0o600 || descriptorMetadata.size !== 0 ||
pathnameMetadata.size !== 0
) {
throw new TypeError(`provider guardian inherited ${label} identity is invalid`);
}
}
function providerKindFromStem(stem: string): ProviderGuardianKind {
if (stem === "vulnerability-report") return "vulnerability";
if (stem === "provenance-attestation") return "provenance";
throw new TypeError("provider guardian inherited private kind is invalid");
}
function consumeChunk(chunk: Buffer | string): void {
if (terminal) return;
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
if (expectedBytes === undefined && pending.byteLength >= 4) {
expectedBytes = pending.readUInt32BE(0);
if (expectedBytes <= 0 || expectedBytes > MAX_PROVIDER_GUARDIAN_FRAME_PAYLOAD_BYTES) {
enqueue(async () => failClosed(126, "provider guardian frame length is invalid"));
return;
}
}
if (expectedBytes !== undefined && pending.byteLength === expectedBytes + 4) {
const payload = pending.subarray(4);
pending = Buffer.alloc(0);
expectedBytes = undefined;
enqueue(async () => handleFrame(payload));
} else if (expectedBytes !== undefined && pending.byteLength > expectedBytes + 4) {
enqueue(async () => failClosed(126, "provider guardian frame has trailing bytes"));
}
}
function enqueue(operation: () => Promise<void>): void {
operations = operations.then(operation).catch(async (error) => {
await failClosed(126, error instanceof Error ? error.message : String(error));
});
}
async function handleFrame(payload: Buffer): Promise<void> {
if (state === "starting") {
await establishTransaction(payload);
} else if (state === "guarding") {
await publishSealedArtifact(payload);
} else if (state === "published") {
await prepareCommit(payload);
} else {
await failClosed(126, "provider guardian received data after commit");
}
}
async function establishTransaction(payload: Buffer): Promise<void> {
const nowEpochMs = Date.now();
const guard = decodeProviderGuardianGuard(payload, {
nowEpochMs,
maxLeaseMs: MAX_PROVIDER_GUARDIAN_LEASE_MS,
});
if (
guard.kind !== bootstrap.kind ||
guard.nonce.subarray(0, 16).toString("hex") !== bootstrap.noncePrefix ||
providerGuardianRawStagingLeaf(guard.kind, guard.nonce) !== bootstrap.rawStagingLeaf ||
providerGuardianSealedTempLeaf(guard.kind, guard.nonce) !== bootstrap.sealedTempLeaf
) {
throw new TypeError("provider guardian guard does not match inherited private aliases");
}
assertBoundPrivateLeaf(
RAW_STAGING_FD,
bootstrap.rawStagingPath,
bootstrap.rawIdentity,
"raw staging",
);
assertBoundPrivateLeaf(
SEALED_TEMP_FD,
bootstrap.sealedTempPath,
bootstrap.sealedIdentity,
"sealed temp",
);
transaction = Object.freeze({ guard });
await link(bootstrap.rawStagingPath, bootstrap.rawPath);
assertOwnedPathMetadata(bootstrap.rawStagingPath, bootstrap.rawIdentity, 2, 0o600, 0,
"raw staging link");
assertOwnedPathMetadata(bootstrap.rawPath, bootstrap.rawIdentity, 2, 0o600, 0,
"canonical raw link");
await unlink(bootstrap.rawStagingPath);
fsyncSync(RAW_DIRECTORY_FD);
assertOwnedPathMetadata(bootstrap.rawPath, bootstrap.rawIdentity, 1, 0o600, 0,
"canonical raw");
assertBoundPrivateLeaf(
SEALED_TEMP_FD,
bootstrap.sealedTempPath,
bootstrap.sealedIdentity,
"sealed temp",
);
const remainingLeaseMs = guard.deadlineEpochMs - Date.now();
if (remainingLeaseMs <= 0) {
throw new TypeError("provider guardian guard deadline expired during startup");
}
deadline = setTimeout(() => {
enqueue(async () => failClosed(124, "provider guardian lease deadline expired", true));
}, remainingLeaseMs);
writeSync(1, encodeProviderGuardianReady({
nonce: guard.nonce,
rawDev: bootstrap.rawIdentity.dev,
rawIno: bootstrap.rawIdentity.ino,
sealedTempLeaf: bootstrap.sealedTempLeaf,
sealedDev: bootstrap.sealedIdentity.dev,
sealedIno: bootstrap.sealedIdentity.ino,
}));
state = "guarding";
}
function assertBoundPrivateLeaf(
fd: number,
target: string,
identity: OwnedIdentity,
label: string,
): void {
const descriptorMetadata = fstatSync(fd);
const pathnameMetadata = lstatSync(target);
assertOwnedMetadata(descriptorMetadata, identity, 1, 0o600, 0, label);
assertOwnedMetadata(pathnameMetadata, identity, 1, 0o600, 0, label);
if (pathnameMetadata.isSymbolicLink()) {
throw new TypeError(`provider guardian ${label} alias became symbolic`);
}
}
async function publishSealedArtifact(payload: Buffer): Promise<void> {
if (!transaction) {
throw new Error("provider guardian transaction identity is unavailable");
}
const publication = decodeProviderGuardianPublish(payload, transaction.guard.nonce);
if (
publication.sealedDev !== bootstrap.sealedIdentity.dev ||
publication.sealedIno !== bootstrap.sealedIdentity.ino
) {
throw new TypeError("provider guardian publish identity is invalid");
}
assertOwnedMetadata(
fstatSync(SEALED_TEMP_FD),
bootstrap.sealedIdentity,
1,
0o400,
publication.size,
"sealed publish descriptor",
);
const pathnameMetadata = await lstat(bootstrap.sealedTempPath);
assertOwnedMetadata(
pathnameMetadata,
bootstrap.sealedIdentity,
1,
0o400,
publication.size,
"sealed publish pathname",
);
if (pathnameMetadata.isSymbolicLink()) {
throw new TypeError("provider guardian sealed publish pathname became symbolic");
}
const actualSha256 = hashInheritedFile(SEALED_TEMP_FD, publication.size);
if (actualSha256 !== publication.sha256) {
throw new TypeError("provider guardian publish hash is invalid");
}
try {
await lstat(bootstrap.sealedPath);
throw new Error("provider guardian sealed output already exists");
} catch (error) {
if (!hasErrorCode(error, "ENOENT")) throw error;
}
await link(bootstrap.sealedTempPath, bootstrap.sealedPath);
await unlink(bootstrap.sealedTempPath);
fsyncSync(EVIDENCE_DIRECTORY_FD);
const finalMetadata = await lstat(bootstrap.sealedPath);
assertOwnedMetadata(
finalMetadata,
bootstrap.sealedIdentity,
1,
0o400,
publication.size,
"sealed final",
);
if (finalMetadata.isSymbolicLink()) {
throw new TypeError("provider guardian sealed final became symbolic");
}
state = "published";
writeSync(1, encodeProviderGuardianPublished({
nonce: transaction.guard.nonce,
sealedDev: bootstrap.sealedIdentity.dev,
sealedIno: bootstrap.sealedIdentity.ino,
}));
}
function assertOwnedPathMetadata(
target: string,
identity: OwnedIdentity,
expectedLinks: number,
expectedMode: number,
expectedSize: number,
label: string,
): void {
const metadata = lstatSync(target);
assertOwnedMetadata(metadata, identity, expectedLinks, expectedMode, expectedSize, label);
if (metadata.isSymbolicLink()) {
throw new TypeError(`provider guardian ${label} became symbolic`);
}
}
function assertOwnedMetadata(
metadata: Stats,
identity: OwnedIdentity,
expectedLinks: number,
expectedMode: number,
expectedSize: number,
label: string,
): void {
if (
!metadata.isFile() || metadata.dev !== identity.dev || metadata.ino !== identity.ino ||
metadata.nlink !== expectedLinks || (metadata.mode & 0o777) !== expectedMode ||
metadata.size !== expectedSize
) {
throw new TypeError(`provider guardian ${label} metadata is invalid`);
}
}
function hashInheritedFile(fd: number, size: number): string {
const digest = createHash("sha256");
const buffer = Buffer.allocUnsafe(Math.min(65_536, size));
let position = 0;
while (position < size) {
const requested = Math.min(buffer.byteLength, size - position);
const bytesRead = readSync(fd, buffer, 0, requested, position);
if (bytesRead <= 0) throw new Error("provider guardian sealed publish read was truncated");
digest.update(buffer.subarray(0, bytesRead));
position += bytesRead;
}
return digest.digest("hex");
}
async function prepareCommit(payload: Buffer): Promise<void> {
if (!transaction) throw new Error("provider guardian transaction identity is unavailable");
decodeProviderGuardianCommit(payload, transaction.guard.nonce);
const removedRaw = await cleanupOwnedProviderReport({
reportPath: bootstrap.rawPath,
reportDev: bootstrap.rawIdentity.dev,
reportIno: bootstrap.rawIdentity.ino,
});
if (!removedRaw) throw new Error("provider guardian raw output disappeared before commit");
state = "commitPending";
}
async function succeedOnCommittedEof(): Promise<void> {
const closeErrors: Error[] = [];
closePrivateFds(closeErrors);
if (closeErrors.length > 0) {
await failClosed(
126,
"provider guardian private descriptor close failed",
false,
closeErrors,
);
return;
}
terminal = true;
if (deadline) clearTimeout(deadline);
liveness.removeAllListeners();
liveness.destroy();
closeControlInputBestEffort();
process.exit(0);
}
async function failClosed(
exitCode: number,
message: string,
forceSignal = false,
priorErrors: readonly Error[] = [],
): Promise<void> {
if (terminal) return;
terminal = true;
if (deadline) clearTimeout(deadline);
liveness.removeAllListeners();
liveness.destroy();
const failures = [new Error(message), ...priorErrors];
await cleanupOwnedPath(bootstrap.rawStagingPath, bootstrap.rawIdentity, failures);
await cleanupOwnedPath(bootstrap.rawPath, bootstrap.rawIdentity, failures);
await cleanupOwnedPath(bootstrap.sealedTempPath, bootstrap.sealedIdentity, failures);
await cleanupOwnedPath(bootstrap.sealedPath, bootstrap.sealedIdentity, failures);
closePrivateFds(failures);
closeControlInputBestEffort();
writeAggregateDiagnostic("provider guardian failed", failures);
if (forceSignal) {
try {
process.kill(process.pid, "SIGKILL");
} finally {
process.exit(exitCode);
}
}
process.exit(exitCode);
}
async function cleanupOwnedPath(
target: string,
identity: OwnedIdentity,
errors: Error[],
): Promise<void> {
try {
await cleanupOwnedProviderReport({
reportPath: target,
reportDev: identity.dev,
reportIno: identity.ino,
});
} catch (error) {
errors.push(toError(error));
}
}
function closePrivateFds(errors: Error[]): void {
if (privateFdsClosed) return;
privateFdsClosed = true;
for (const fd of [RAW_STAGING_FD, SEALED_TEMP_FD]) {
try {
closeSync(fd);
} catch (error) {
errors.push(toError(error));
}
}
}
function closeControlInputBestEffort(): void {
try {
closeSync(0);
} catch {
// Terminal cleanup and the exit status must not depend on a diagnostic fd.
}
}
function writeAggregateDiagnostic(label: string, failures: readonly Error[]): void {
const aggregate = failures.length > 1
? new AggregateError(failures, label, { cause: failures[0] })
: failures[0];
const detail = aggregate instanceof AggregateError
? aggregate.errors.map((error) => toError(error).message).join("; ")
: aggregate?.message ?? label;
try {
writeSync(2, `${label}: ${detail}\n`);
} catch {
// A closed parent-side pipe must not convert fail-closed termination to exit 0.
}
}
function assertInheritedDirectory(fd: number, canonicalPath: string, label: string): void {
const descriptorMetadata = fstatSync(fd);
const pathMetadata = lstatSync(canonicalPath);
if (
!descriptorMetadata.isDirectory() || !pathMetadata.isDirectory() ||
pathMetadata.isSymbolicLink() || descriptorMetadata.dev !== pathMetadata.dev ||
descriptorMetadata.ino !== pathMetadata.ino
) {
throw new TypeError(`provider guardian inherited ${label} fd is not a directory`);
}
}
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+166
View File
@@ -0,0 +1,166 @@
import { spawn } from "node:child_process";
import { closeSync, createReadStream, writeSync } from "node:fs";
import { cleanupOwnedProviderReport } from "./provider-raw-cleanup.ts";
const MAX_FRAME_BYTES = 16_777_216;
const reportIdentity = parseReportIdentity(process.argv.slice(2));
let pending = Buffer.alloc(0);
let expectedBytes: number | undefined;
let provider: ReturnType<typeof spawn> | undefined;
let providerClosed = false;
let livenessLost = false;
const liveness = createReadStream("", { fd: 0, autoClose: false });
liveness.on("data", (chunk: Buffer | string) => {
if (provider) {
terminateForProtocolFailure("provider scope received trailing protocol bytes");
return;
}
pending = Buffer.concat([pending, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
if (expectedBytes === undefined && pending.byteLength >= 4) {
expectedBytes = pending.readUInt32BE(0);
if (expectedBytes <= 0 || expectedBytes > MAX_FRAME_BYTES) {
terminateForProtocolFailure("provider scope frame length is invalid");
return;
}
}
if (expectedBytes !== undefined && pending.byteLength === expectedBytes + 4) {
launchProvider(pending.subarray(4));
pending = Buffer.alloc(0);
} else if (expectedBytes !== undefined && pending.byteLength > expectedBytes + 4) {
terminateForProtocolFailure("provider scope frame has trailing bytes");
}
});
liveness.once("end", () => terminateForParentLoss());
liveness.once("error", () => terminateForParentLoss());
function launchProvider(payload: Buffer): void {
const frame = parseFrame(payload);
if (
frame.reportPath !== reportIdentity.reportPath ||
frame.reportDev !== reportIdentity.reportDev ||
frame.reportIno !== reportIdentity.reportIno
) {
throw new TypeError("provider scope frame identity does not match its launch identity");
}
const bwrapInput = Buffer.from(frame.bwrapInputBase64, "base64");
provider = spawn("/usr/bin/bwrap", ["--args", "0"], {
detached: true,
stdio: ["pipe", "inherit", "inherit"],
});
provider.stdin?.end(bwrapInput);
provider.once("error", (error) => finishProvider(frame, null, null, error));
provider.once("close", (code, signal) => finishProvider(frame, code, signal));
}
async function finishProvider(
frame: ReturnType<typeof parseFrame>,
code: number | null,
signal: NodeJS.Signals | null,
error?: Error,
): Promise<void> {
if (providerClosed) return;
providerClosed = true;
if (livenessLost) await cleanupOwnedProviderReport(frame);
closeLivenessInput();
if (error) {
writeSync(2, `${error.message}\n`);
process.exit(1);
}
if (signal) process.exit(128 + signalNumber(signal));
process.exit(code ?? 1);
}
function terminateForParentLoss(): void {
if (livenessLost) return;
livenessLost = true;
if (!provider || providerClosed) {
void cleanupAfterParentLossAndExit();
return;
}
try {
process.kill(-provider.pid!, "SIGKILL");
} catch (error) {
if (!hasErrorCode(error, "ESRCH")) throw error;
}
}
async function cleanupAfterParentLossAndExit(): Promise<void> {
try {
await cleanupOwnedProviderReport(reportIdentity);
} catch (error) {
writeSync(2, `${error instanceof Error ? error.message : String(error)}\n`);
}
closeLivenessInput();
process.exit(125);
}
function terminateForProtocolFailure(message: string): void {
writeSync(2, `${message}\n`);
terminateForParentLoss();
}
function closeLivenessInput(): void {
liveness.removeAllListeners();
liveness.destroy();
try {
closeSync(0);
} catch (error) {
if (!hasErrorCode(error, "EBADF")) throw error;
}
}
function parseFrame(payload: Buffer): Readonly<{
bwrapInputBase64: string;
reportPath: string;
reportDev: number;
reportIno: number;
}> {
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload)) as Record<string, unknown>;
if (
typeof value.bwrapInputBase64 !== "string" ||
typeof value.reportPath !== "string" || !value.reportPath.startsWith("/") ||
!Number.isSafeInteger(value.reportDev) || Number(value.reportDev) <= 0 ||
!Number.isSafeInteger(value.reportIno) || Number(value.reportIno) <= 0
) {
throw new TypeError("provider scope frame payload is invalid");
}
return {
bwrapInputBase64: value.bwrapInputBase64,
reportPath: value.reportPath,
reportDev: Number(value.reportDev),
reportIno: Number(value.reportIno),
};
}
function parseReportIdentity(arguments_: readonly string[]): Readonly<{
cpuSeconds: number;
reportPath: string;
reportDev: number;
reportIno: number;
}> {
const [cpuValue, reportPath, devValue, inoValue, ...trailing] = arguments_;
const cpuSeconds = Number(cpuValue);
const reportDev = Number(devValue);
const reportIno = Number(inoValue);
if (
trailing.length > 0 ||
!Number.isSafeInteger(cpuSeconds) || cpuSeconds <= 0 ||
typeof reportPath !== "string" || !reportPath.startsWith("/") || reportPath.includes("\0") ||
!Number.isSafeInteger(reportDev) || reportDev <= 0 ||
!Number.isSafeInteger(reportIno) || reportIno <= 0
) {
throw new TypeError("provider scope launch identity is invalid");
}
return { cpuSeconds, reportPath, reportDev, reportIno };
}
function signalNumber(signal: NodeJS.Signals): number {
return signal === "SIGKILL" ? 9 : signal === "SIGXCPU" ? 24 : 1;
}
function hasErrorCode(error: unknown, code: string): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
}
+163
View File
@@ -0,0 +1,163 @@
import { randomBytes as cryptoRandomBytes } from "node:crypto";
import {
captureCiCandidateArchive,
withVerifiedCapturedCandidate,
type CapturedCandidateArchive,
} from "./ci-candidate-archive.ts";
import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts";
import type { ExpectedPromotionContext, ProviderTrust } from "./provider-evidence.ts";
import { validateProviderUpload } from "./provider-upload-validator.ts";
export type ProviderInvocation = Readonly<{
candidateRoot: string;
environment: Readonly<Record<string, string>>;
}>;
export async function superviseProviderEvidence(input: Readonly<{
kind: "vulnerability" | "provenance";
archivePath: string;
expectedArchiveSha256: string;
expectedRun: Readonly<{ id: string; attempt: number; sourceRevision: string }>;
trust: ProviderTrust;
executeProvider: (invocation: ProviderInvocation) => Promise<void>;
captureReport: () => Promise<Buffer>;
}>, dependencies: Readonly<{
captureArchive?: typeof captureCiCandidateArchive;
withVerifiedCandidate?: typeof withVerifiedCapturedCandidate;
verifyLocalEvidence?: typeof verifyArchivedLocalEvidence;
validateUpload?: typeof validateProviderUpload;
randomBytes?: (bytes: number) => Buffer;
nowEpochMs?: () => number;
}> = {}): Promise<Readonly<{
evidence: unknown;
invocationNonce: string;
expectedContext: ExpectedPromotionContext;
}>> {
const captured = await (dependencies.captureArchive ?? captureCiCandidateArchive)({
archivePath: input.archivePath,
expectedSha256: input.expectedArchiveSha256,
});
const nonceBytes = (dependencies.randomBytes ?? cryptoRandomBytes)(32);
if (nonceBytes.byteLength !== 32) {
throw new TypeError("provider invocation nonce must contain exactly 32 bytes");
}
const invocationNonce = nonceBytes.toString("hex");
const nowEpochMs = dependencies.nowEpochMs ?? Date.now;
const result = await (dependencies.withVerifiedCandidate ?? withVerifiedCapturedCandidate)({
captured,
verify: async ({ extractionRoot, manifest }) => {
const local = await (dependencies.verifyLocalEvidence ?? verifyArchivedLocalEvidence)({
extractionRoot,
expectedManifest: manifest,
});
if (local.status !== "PASS" || !local.identity) {
throw new Error(
`provider candidate local assessment failed: ${local.failures.join("; ")}`,
);
}
if (local.identity.sourceRevision !== input.expectedRun.sourceRevision) {
throw new Error("provider candidate source revision mismatch");
}
const expectedContext: ExpectedPromotionContext = Object.freeze({
run: Object.freeze({ id: input.expectedRun.id, attempt: input.expectedRun.attempt }),
source: Object.freeze({
revision: local.identity.sourceRevision,
sourceSetSha256: local.identity.sourceSetSha256,
}),
candidate: Object.freeze({
archiveSha256: captured.archiveSha256,
bundleSha256: manifest.bundleSha256,
distSha256: manifest.distSha256,
lockfileSha256: manifest.lockfileSha256,
}),
secretScanAttestation: Object.freeze({
status: "PASS" as const,
localEvidenceAssessmentSha256: local.identity.assessmentSha256,
sourceSetSha256: local.identity.sourceSetSha256,
policySha256: local.identity.secretScan.policySha256,
sarifSha256: local.identity.secretScan.sarifSha256,
scanInputSha256: local.identity.secretScan.scanInputSha256,
}),
vulnerabilityInvocationNonce:
input.kind === "vulnerability" ? invocationNonce : "0".repeat(64),
provenanceInvocationNonce:
input.kind === "provenance" ? invocationNonce : "0".repeat(64),
});
const issuedNow = nowEpochMs();
const issuedAt = new Date(issuedNow).toISOString();
const expiresAt = new Date(issuedNow + 60 * 60 * 1_000).toISOString();
await input.executeProvider({
candidateRoot: extractionRoot,
environment: providerInvocationEnvironment({
kind: input.kind,
expectedContext,
invocationNonce,
issuedAt,
expiresAt,
trust: input.trust,
}),
});
const capturedReport = await input.captureReport();
const evidence = await (dependencies.validateUpload ?? validateProviderUpload)({
kind: input.kind,
verifiedManifest: manifest,
archiveSha256: captured.archiveSha256,
candidateRoot: extractionRoot,
capturedReport,
expectedContext,
trust: input.trust,
nowEpochMs,
});
return Object.freeze({ evidence, invocationNonce, expectedContext });
},
});
return result;
}
export function providerInvocationEnvironment(input: Readonly<{
kind: "vulnerability" | "provenance";
expectedContext: ExpectedPromotionContext;
invocationNonce: string;
issuedAt: string;
expiresAt: string;
trust: ProviderTrust;
}>): Readonly<Record<string, string>> {
return Object.freeze({
PROVIDER_EVIDENCE_SCHEMA_VERSION: "2",
PROVIDER_EVIDENCE_TYPE:
input.kind === "vulnerability"
? "vulnerability-report"
: "provenance-attestation",
PROVIDER_ISSUED_AT: input.issuedAt,
PROVIDER_EXPIRES_AT: input.expiresAt,
PROVIDER_INVOCATION_NONCE: input.invocationNonce,
PROVIDER_KEY_ID: input.trust.keyId,
PROVIDER_PUBLIC_KEY_FINGERPRINT: input.trust.publicKeyFingerprint,
CI_RUN_ID: input.expectedContext.run.id,
CI_RUN_ATTEMPT: String(input.expectedContext.run.attempt),
SOURCE_REVISION: input.expectedContext.source.revision,
SOURCE_SET_SHA256: input.expectedContext.source.sourceSetSha256,
CANDIDATE_ROOT: "/candidate",
CANDIDATE_LOCKFILE_PATH: "/candidate/pnpm-lock.yaml",
CANDIDATE_ARCHIVE_SHA256: input.expectedContext.candidate.archiveSha256,
CANDIDATE_BUNDLE_SHA256: input.expectedContext.candidate.bundleSha256,
CANDIDATE_DIST_SHA256: input.expectedContext.candidate.distSha256,
CANDIDATE_LOCKFILE_SHA256: input.expectedContext.candidate.lockfileSha256,
SECRET_SCAN_STATUS: input.expectedContext.secretScanAttestation.status,
SECRET_SCAN_LOCAL_EVIDENCE_ASSESSMENT_SHA256:
input.expectedContext.secretScanAttestation.localEvidenceAssessmentSha256,
SECRET_SCAN_SOURCE_SET_SHA256:
input.expectedContext.secretScanAttestation.sourceSetSha256,
SECRET_SCAN_POLICY_SHA256:
input.expectedContext.secretScanAttestation.policySha256,
SECRET_SCAN_SARIF_SHA256:
input.expectedContext.secretScanAttestation.sarifSha256,
SECRET_SCAN_INPUT_SHA256:
input.expectedContext.secretScanAttestation.scanInputSha256,
});
}
export type CaptureArchiveDependency = (
input: Readonly<{ archivePath: string; expectedSha256: string }>,
) => Promise<CapturedCandidateArchive>;
+42
View File
@@ -0,0 +1,42 @@
import { createPublicKey } from "node:crypto";
import path from "node:path";
import { readBoundedRegularFile } from "./ci-artifact-validator.ts";
import {
providerPublicKeyFingerprint,
type ProviderTrust,
} from "./provider-evidence.ts";
export async function readProviderTrust(
configuredRoot: string,
publicKeyPath: string | undefined,
keyId: string | undefined,
): Promise<ProviderTrust | null> {
if (!publicKeyPath || !keyId?.trim()) return null;
try {
const root = path.resolve(configuredRoot);
const absolute = path.resolve(root, publicKeyPath);
const relative = path.relative(root, absolute);
const outside =
relative === ".." ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative);
const bytes = await readBoundedRegularFile({
root: outside ? path.dirname(absolute) : root,
relativePath: outside
? path.basename(absolute)
: relative.replaceAll(path.sep, "/"),
maxBytes: 1_048_576,
});
const publicKey = createPublicKey(
new TextDecoder("utf-8", { fatal: true }).decode(bytes),
);
return Object.freeze({
keyId,
publicKey,
publicKeyFingerprint: providerPublicKeyFingerprint(publicKey),
});
} catch {
return null;
}
}
+59
View File
@@ -0,0 +1,59 @@
import {
validateProviderEvidence,
type ExpectedPromotionContext,
type ProviderTrust,
} from "./provider-evidence.ts";
import {
verifyReleaseCandidate,
type ReleaseCandidateManifest,
} from "./release-candidate.ts";
export async function validateProviderUpload(input: Readonly<{
kind: "vulnerability" | "provenance";
verifiedManifest: ReleaseCandidateManifest;
archiveSha256: string;
candidateRoot: string;
capturedReport: Buffer;
expectedContext: ExpectedPromotionContext;
trust: ProviderTrust;
nowEpochMs?: () => number;
}>): Promise<unknown> {
if (
input.expectedContext.candidate.archiveSha256 !== input.archiveSha256 ||
input.expectedContext.candidate.bundleSha256 !== input.verifiedManifest.bundleSha256 ||
input.expectedContext.candidate.distSha256 !== input.verifiedManifest.distSha256 ||
input.expectedContext.candidate.lockfileSha256 !== input.verifiedManifest.lockfileSha256
) {
throw new Error("provider supervisor expected candidate context mismatch");
}
const verifiedCandidate = await verifyReleaseCandidate(
input.verifiedManifest,
input.candidateRoot,
);
if (verifiedCandidate.failures.length > 0) {
throw new Error(
`provider input candidate root changed: ${verifiedCandidate.failures.join("; ")}`,
);
}
let report: unknown;
try {
report = JSON.parse(
new TextDecoder("utf-8", { fatal: true }).decode(input.capturedReport),
) as unknown;
} catch {
throw new TypeError("provider output is not canonical UTF-8 JSON");
}
const evaluated = validateProviderEvidence({
kind: input.kind,
value: report,
expected: input.expectedContext,
trust: input.trust,
nowEpochMs: input.nowEpochMs,
});
if (evaluated.status !== "PASS" || !evaluated.evidence) {
throw new Error(
`provider evidence context validation failed: ${evaluated.failures.join("; ")}`,
);
}
return evaluated.evidence;
}
+138
View File
@@ -0,0 +1,138 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
export type RealtimeBoundaryRuleId =
| "NATIVE_REALTIME_API_OUTSIDE_ADAPTER"
| "PRESENTATION_INTERVAL_OWNER"
| "UNSELECTED_REALTIME_RUNTIME_COMPOSED";
export type RealtimeBoundaryViolation = Readonly<{
ruleId: RealtimeBoundaryRuleId;
file: string;
line: number;
}>;
const SOURCE_EXTENSION = /\.(?:[cm]?ts|tsx)$/u;
const OWNED_NATIVE_ROOTS = [
"src/adapters/realtime/",
"src/adapters/web-push/",
] as const;
const REALTIME_ADAPTER_IMPORT =
/(?:from\s*|import\s*\()\s*["'][^"']*\/adapters\/(?:realtime|web-push)(?:\/[^"']*)?["']/gu;
const NATIVE_REALTIME_PATTERNS = [
/\bnew\s+(?:WebSocket|EventSource|Notification)\s*\(/gu,
/\bNotification\s*\.\s*requestPermission\s*\(/gu,
/\.\s*showNotification\s*\(/gu,
/\.\s*pushManager\s*\.\s*(?:subscribe|getSubscription)\s*\(/gu,
/\bReflect\s*\.\s*get\s*\([^,]+,\s*["'](?:WebSocket|EventSource|Notification|pushManager)["']/gu,
] as const;
const PRESENTATION_INTERVAL = /\bsetInterval\s*\(/gu;
export async function scanRealtimeBoundaries(
sourceRoot: string,
): Promise<readonly RealtimeBoundaryViolation[]> {
const absoluteRoot = path.resolve(sourceRoot);
const files = await collectSourceFiles(absoluteRoot);
const violations: RealtimeBoundaryViolation[] = [];
for (const file of files) {
const source = await readFile(file, "utf8");
const logicalFile = logicalSourcePath(absoluteRoot, file);
inspectFile(source, logicalFile, violations);
}
return Object.freeze(
violations
.sort(
(left, right) =>
left.file.localeCompare(right.file) ||
left.line - right.line ||
left.ruleId.localeCompare(right.ruleId),
)
.map((violation) => Object.freeze(violation)),
);
}
function inspectFile(
source: string,
logicalFile: string,
violations: RealtimeBoundaryViolation[],
): void {
const nativeOwned = OWNED_NATIVE_ROOTS.some((root) =>
logicalFile.startsWith(root),
);
const presentationOwned =
logicalFile.startsWith("src/presentation/") ||
/^src\/features\/[^/]+\/presentation\//u.test(logicalFile);
const compositionBoundary =
logicalFile.startsWith("src/bootstrap/") ||
/^src\/features\/installed-feature-/u.test(logicalFile);
const report = (
ruleId: RealtimeBoundaryRuleId,
index: number,
): void => {
violations.push({
ruleId,
file: logicalFile,
line: lineAt(source, index),
});
};
if (!nativeOwned) {
for (const pattern of NATIVE_REALTIME_PATTERNS) {
for (const match of source.matchAll(pattern)) {
report(
"NATIVE_REALTIME_API_OUTSIDE_ADAPTER",
match.index,
);
}
}
}
if (presentationOwned) {
for (const match of source.matchAll(PRESENTATION_INTERVAL)) {
report("PRESENTATION_INTERVAL_OWNER", match.index);
}
}
if (compositionBoundary) {
for (const match of source.matchAll(REALTIME_ADAPTER_IMPORT)) {
report("UNSELECTED_REALTIME_RUNTIME_COMPOSED", match.index);
}
}
}
async function collectSourceFiles(
directory: string,
): Promise<readonly string[]> {
const output: string[] = [];
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const resolved = path.join(directory, entry.name);
if (
entry.isDirectory() &&
!["node_modules", "dist", "artifacts", ".tmp"].includes(
entry.name,
)
) {
output.push(...(await collectSourceFiles(resolved)));
} else if (entry.isFile() && SOURCE_EXTENSION.test(entry.name)) {
output.push(resolved);
}
}
return output;
}
function logicalSourcePath(root: string, file: string): string {
const workspaceRelative = path
.relative(process.cwd(), file)
.split(path.sep)
.join("/");
if (root === path.resolve("src")) return workspaceRelative;
return path.relative(root, file).split(path.sep).join("/");
}
function lineAt(source: string, index: number): number {
let line = 1;
for (let offset = 0; offset < index; offset += 1) {
if (source.charCodeAt(offset) === 10) line += 1;
}
return line;
}
+336
View File
@@ -0,0 +1,336 @@
import { createHash } from "node:crypto";
export const COMPATIBILITY_IMPACTS = Object.freeze([
"none",
"additive",
"behavior-change",
"breaking",
] as const);
type CompatibilityImpact = (typeof COMPATIBILITY_IMPACTS)[number];
type RegistryRecord = Record<string, unknown> & {
registryId?: unknown;
contract?: unknown;
rows?: unknown;
};
type RegistryChange = {
changeId: string;
registryId: string;
rowName: string;
field: string;
kind: string;
impact: CompatibilityImpact;
before?: unknown;
after?: unknown;
};
type RegistryDiff = Readonly<{
impact: CompatibilityImpact;
changes: readonly RegistryChange[];
}>;
const impactRank = new Map(
COMPATIBILITY_IMPACTS.map((impact, index) => [impact, index]),
);
export function canonicalizeRegistryValue(value: unknown): unknown {
if (Array.isArray(value)) {
const projected: unknown[] = value.map(canonicalizeRegistryValue);
return projected.every(
(item) =>
item === null ||
["string", "number", "boolean"].includes(typeof item),
)
? projected.sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
)
: projected;
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, canonicalizeRegistryValue(item)]),
);
}
return value;
}
export function canonicalRegistryJson(value: unknown): string {
return JSON.stringify(canonicalizeRegistryValue(value)) ?? "undefined";
}
export function registrySnapshotDigest(snapshot: unknown): string {
return createHash("sha256")
.update(canonicalRegistryJson(snapshot))
.digest("hex");
}
function strongestImpact(
current: CompatibilityImpact,
candidate: CompatibilityImpact,
): CompatibilityImpact {
return (impactRank.get(candidate) ?? 0) > (impactRank.get(current) ?? 0)
? candidate
: current;
}
function valueType(value: unknown): string {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
function changeId(
registryId: string,
rowName: string,
field: string,
kind: string,
): string {
return `${registryId}:${rowName}:${field}:${kind}`;
}
/**
* Calculates a semantic diff. Object key and primitive-array ordering is
* canonicalized before comparison and therefore cannot create a false change.
*/
export function diffRegistrySnapshots(
before: Readonly<Record<string, unknown>>,
after: Readonly<Record<string, unknown>>,
): RegistryDiff {
const changes: RegistryChange[] = [];
let impact: CompatibilityImpact = "none";
const beforeRegistryRows = (before.registries ?? []) as RegistryRecord[];
const beforeRegistries = new Map<string, RegistryRecord>(
beforeRegistryRows.map((registry) => [
String(registry.registryId),
registry,
]),
);
const afterRegistryRows = (after.registries ?? []) as RegistryRecord[];
const afterRegistries = new Map<string, RegistryRecord>(
afterRegistryRows.map((registry) => [
String(registry.registryId),
registry,
]),
);
const registryIds = new Set([
...beforeRegistries.keys(),
...afterRegistries.keys(),
]);
for (const registryId of [...registryIds].sort()) {
const previous = beforeRegistries.get(registryId);
const current = afterRegistries.get(registryId);
if (!previous || !current) {
const changeImpact = previous ? "breaking" : "additive";
impact = strongestImpact(impact, changeImpact);
changes.push({
changeId: changeId(registryId, "*", "*", previous ? "removed" : "added"),
registryId,
rowName: "*",
field: "*",
kind: previous ? "registry-removed" : "registry-added",
impact: changeImpact,
});
continue;
}
const previousContract = (previous.contract ?? {}) as Record<string, unknown>;
const currentContract = (current.contract ?? {}) as Record<string, unknown>;
const contractFields = new Set([
...Object.keys(previousContract),
...Object.keys(currentContract),
]);
for (const field of [...contractFields].sort()) {
const beforeHas = Object.hasOwn(previousContract, field);
const afterHas = Object.hasOwn(currentContract, field);
const beforeValue = previousContract[field];
const afterValue = currentContract[field];
if (
beforeHas &&
afterHas &&
canonicalRegistryJson(beforeValue) ===
canonicalRegistryJson(afterValue)
) {
continue;
}
const kind = !beforeHas
? "contract-field-added"
: !afterHas
? "contract-field-removed"
: "contract-field-changed";
impact = strongestImpact(impact, "breaking");
changes.push({
changeId: changeId(registryId, "$contract", field, kind),
registryId,
rowName: "$contract",
field,
kind,
impact: "breaking",
before: canonicalizeRegistryValue(beforeValue),
after: canonicalizeRegistryValue(afterValue),
});
}
const breakingFields = new Set(
(currentContract.breakingFields ?? []) as string[],
);
const beforeRows = (previous.rows ?? {}) as Record<
string,
Record<string, unknown>
>;
const afterRows = (current.rows ?? {}) as Record<
string,
Record<string, unknown>
>;
const rowNames = new Set([
...Object.keys(beforeRows),
...Object.keys(afterRows),
]);
for (const rowName of [...rowNames].sort()) {
const beforeRow = beforeRows[rowName];
const afterRow = afterRows[rowName];
if (!beforeRow || !afterRow) {
const changeImpact = beforeRow ? "breaking" : "additive";
impact = strongestImpact(impact, changeImpact);
changes.push({
changeId: changeId(
registryId,
rowName,
"*",
beforeRow ? "removed" : "added",
),
registryId,
rowName,
field: "*",
kind: beforeRow ? "row-removed" : "row-added",
impact: changeImpact,
});
continue;
}
const fields = new Set([
...Object.keys(beforeRow),
...Object.keys(afterRow),
]);
for (const field of [...fields].sort()) {
const beforeHas = Object.hasOwn(beforeRow, field);
const afterHas = Object.hasOwn(afterRow, field);
const beforeValue = beforeRow[field];
const afterValue = afterRow[field];
if (
beforeHas &&
afterHas &&
canonicalRegistryJson(beforeValue) ===
canonicalRegistryJson(afterValue)
) {
continue;
}
let kind: string;
let changeImpact: CompatibilityImpact;
if (!beforeHas) {
kind = "field-added";
changeImpact = "additive";
} else if (!afterHas) {
kind = "field-removed";
changeImpact = "breaking";
} else if (valueType(beforeValue) !== valueType(afterValue)) {
kind = "field-type-changed";
changeImpact = "breaking";
} else if (
Array.isArray(beforeValue) &&
Array.isArray(afterValue) &&
beforeValue.some(
(item) =>
!afterValue.some(
(candidate) =>
canonicalRegistryJson(candidate) ===
canonicalRegistryJson(item),
),
)
) {
kind = "allowed-value-removed";
changeImpact = "breaking";
} else {
kind = "field-changed";
changeImpact = breakingFields.has(field)
? "breaking"
: "behavior-change";
}
impact = strongestImpact(impact, changeImpact);
changes.push({
changeId: changeId(registryId, rowName, field, kind),
registryId,
rowName,
field,
kind,
impact: changeImpact,
before: canonicalizeRegistryValue(beforeValue),
after: canonicalizeRegistryValue(afterValue),
});
}
}
}
return Object.freeze({
impact,
changes: Object.freeze(changes),
});
}
export function verifyRegistryBaselineApproval(
snapshot: Readonly<Record<string, unknown>>,
approval: Readonly<Record<string, unknown>>,
) {
const actualDigest = registrySnapshotDigest(snapshot);
const approvedDigest = approval.snapshotDigest;
return Object.freeze({
passed:
approval.schemaVersion === 1 &&
typeof approval.owner === "string" &&
approval.owner.length > 0 &&
typeof approval.approvedAt === "string" &&
approvedDigest === actualDigest,
actualDigest,
approvedDigest:
typeof approvedDigest === "string" ? approvedDigest : "missing",
});
}
export function validateBreakingEvidence(
diff: RegistryDiff,
evidenceFile: Readonly<Record<string, unknown>>,
) {
const entries = (evidenceFile.changes ?? []) as Array<Record<string, unknown>>;
const evidence = new Map<string, Record<string, unknown>>(
entries.map((entry) => [String(entry.changeId), entry]),
);
const failures: string[] = [];
for (const change of diff.changes.filter(
(entry) => entry.impact === "breaking",
)) {
const entry = evidence.get(change.changeId);
if (!entry) {
failures.push(`breaking change missing evidence: ${change.changeId}`);
continue;
}
for (const field of [
"versionBump",
"migration",
"compatibilityWindow",
"rollback",
"owner",
]) {
const value = entry[field];
if (typeof value !== "string" || value.trim().length === 0) {
failures.push(
`breaking change ${change.changeId} missing non-empty ${field}`,
);
}
}
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
});
}
+261
View File
@@ -0,0 +1,261 @@
import { createHash } from "node:crypto";
import { lstat, readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { gzipSync } from "node:zlib";
import { z } from "zod";
import { supplyChainDigest } from "./supply-chain.ts";
const sha256 = z.string().regex(/^[a-f0-9]{64}$/u);
const candidateFileSchema = z
.object({
path: z.string().min(1),
bytes: z.int().nonnegative(),
sha256,
})
.strict();
export const releaseCandidateManifestSchema = z
.object({
schemaVersion: z.literal(1),
distSha256: sha256,
lockfileSha256: sha256,
bundleSha256: sha256,
files: z.array(candidateFileSchema).min(1),
})
.strict();
export type ReleaseCandidateManifest = z.infer<
typeof releaseCandidateManifestSchema
>;
export const RELEASE_CANDIDATE_MANIFEST_PATH =
"artifacts/release/release-candidate.json";
export const LOCAL_EVIDENCE_ASSESSMENT_PATH =
"artifacts/security/local-evidence-assessment.json";
export const LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS = Object.freeze([
"scripts/contracts/release-artifacts.ts",
"scripts/create-release-candidate.ts",
"scripts/generate-supply-chain.ts",
"scripts/lib/build-manifest-outputs.ts",
"scripts/lib/json-schema.ts",
"scripts/lib/local-policy-evidence.ts",
"scripts/lib/local-release-evidence.ts",
"scripts/lib/release-candidate.ts",
"scripts/lib/release-input-evidence.ts",
"scripts/lib/release-runtime-coherence.ts",
"scripts/lib/repository-file-inventory.ts",
"scripts/lib/secret-scan-evaluator.ts",
"scripts/lib/secret-scan-policy.ts",
"scripts/lib/secret-scan.ts",
"scripts/lib/supply-chain.ts",
"scripts/lib/validated-json-artifact.ts",
"src/contracts/release-artifacts.ts",
"src/features/installed-contract-contributions.ts",
"src/features/installed-feature-contracts.ts",
] as const);
export const LOCAL_EVIDENCE_POLICY_INPUT_PATHS = Object.freeze([
"config/security/dependency-baseline.approval.json",
"config/security/dependency-baseline.json",
"config/security/dependency-change-evidence.json",
"config/security/dependency-policy.json",
"config/security/secret-scan-policy.json",
"config/security/vulnerability-exceptions.json",
"config/security/vulnerability-policy.json",
"schemas/artifacts/build-manifest.schema.json",
"schemas/artifacts/dependency-inventory.schema.json",
"schemas/artifacts/supply-chain-verification.schema.json",
...LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
] as const);
export const RELEASE_CANDIDATE_EVIDENCE_PATHS = Object.freeze([
"pnpm-lock.yaml",
"artifacts/performance/bundle.json",
"artifacts/quality/vite-module-inventory.json",
"artifacts/release/build-manifest.json",
"artifacts/release/checksums.txt",
"artifacts/release/dependency-inventory.json",
"artifacts/release/provenance.json",
"artifacts/release/verification.json",
"artifacts/release/sbom.cdx.json",
"artifacts/security/dependency-diff.json",
"artifacts/security/license-report.json",
LOCAL_EVIDENCE_ASSESSMENT_PATH,
"artifacts/security/scan.sarif",
"artifacts/security/supply-chain-coherence.json",
"artifacts/security/supply-chain-verification.json",
"artifacts/security/vulnerability-report.json",
...LOCAL_EVIDENCE_POLICY_INPUT_PATHS,
]);
export type DistOutput = Readonly<{
path: string;
bytes: number;
gzipBytes: number;
sha256: string;
}>;
export async function collectDistOutputs(
repositoryRoot = process.cwd(),
): Promise<DistOutput[]> {
const distRoot = path.resolve(repositoryRoot, "dist");
const files = await regularFilesWithin(distRoot);
if (files.length === 0) {
throw new Error("dist is missing or empty; run the production build first");
}
return Promise.all(
files.map(async (absolutePath) => {
const content = await readFile(absolutePath);
return Object.freeze({
path: path
.relative(repositoryRoot, absolutePath)
.replaceAll(path.sep, "/"),
bytes: content.byteLength,
gzipBytes: gzipSync(content).byteLength,
sha256: createHash("sha256").update(content).digest("hex"),
});
}),
);
}
export function distSha256(outputs: readonly DistOutput[]): string {
return supplyChainDigest(
outputs.map(({ path: outputPath, bytes, sha256 }) => ({
path: outputPath,
bytes,
sha256,
})),
);
}
export async function createReleaseCandidateManifest(
repositoryRoot = process.cwd(),
): Promise<ReleaseCandidateManifest> {
const outputs = await collectDistOutputs(repositoryRoot);
const evidence = await Promise.all(
RELEASE_CANDIDATE_EVIDENCE_PATHS.map((file) =>
digestRequiredFile(repositoryRoot, file),
),
);
const files = [
...outputs.map(({ path: outputPath, bytes, sha256 }) => ({
path: outputPath,
bytes,
sha256,
})),
...evidence,
].sort((left, right) => asciiCompare(left.path, right.path));
const dependencyInventory = JSON.parse(
await readFile(
path.resolve(repositoryRoot, "artifacts/release/dependency-inventory.json"),
"utf8",
),
) as { lockfileSha256?: unknown };
const rawLockfileSha256 = evidence.find(
(file) => file.path === "pnpm-lock.yaml",
)?.sha256;
if (
typeof rawLockfileSha256 !== "string" ||
dependencyInventory.lockfileSha256 !== rawLockfileSha256
) {
throw new Error(
"raw pnpm-lock digest mismatch with dependency inventory",
);
}
return releaseCandidateManifestSchema.parse({
schemaVersion: 1,
distSha256: distSha256(outputs),
lockfileSha256: rawLockfileSha256,
bundleSha256: supplyChainDigest(files),
files,
});
}
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
export async function verifyReleaseCandidate(
value: unknown,
repositoryRoot = process.cwd(),
): Promise<Readonly<{
manifest: ReleaseCandidateManifest | null;
currentDistSha256: string | null;
failures: readonly string[];
}>> {
const parsed = releaseCandidateManifestSchema.safeParse(value);
if (!parsed.success) {
return Object.freeze({
manifest: null,
currentDistSha256: null,
failures: Object.freeze(["release candidate manifest schema mismatch"]),
});
}
const failures: string[] = [];
let actual: ReleaseCandidateManifest | null = null;
try {
actual = await createReleaseCandidateManifest(repositoryRoot);
} catch (error) {
failures.push(
`release candidate inputs unreadable: ${error instanceof Error ? error.message : String(error)}`,
);
}
if (actual) {
if (parsed.data.distSha256 !== actual.distSha256) {
failures.push("release candidate dist digest mismatch");
}
if (parsed.data.lockfileSha256 !== actual.lockfileSha256) {
failures.push("release candidate lockfile digest mismatch");
}
if (parsed.data.bundleSha256 !== actual.bundleSha256) {
failures.push("release candidate bundle digest mismatch");
}
if (JSON.stringify(parsed.data.files) !== JSON.stringify(actual.files)) {
failures.push("release candidate file set or file digest mismatch");
}
}
return Object.freeze({
manifest: parsed.data,
currentDistSha256: actual?.distSha256 ?? null,
failures: Object.freeze(failures),
});
}
async function digestRequiredFile(repositoryRoot: string, file: string) {
const absolutePath = path.resolve(repositoryRoot, file);
const relative = path.relative(repositoryRoot, absolutePath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(`candidate path escapes repository root: ${file}`);
}
const metadata = await lstat(absolutePath);
if (!metadata.isFile()) {
throw new Error(`candidate input is not a regular file: ${file}`);
}
const content = await readFile(absolutePath);
return Object.freeze({
path: file,
bytes: content.byteLength,
sha256: createHash("sha256").update(content).digest("hex"),
});
}
async function regularFilesWithin(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries.sort((left, right) =>
asciiCompare(left.name, right.name),
)) {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) {
files.push(...(await regularFilesWithin(target)));
} else if (entry.isFile()) {
files.push(target);
} else {
throw new Error(`dist contains a non-regular entry: ${target}`);
}
}
return files;
}
+19
View File
@@ -0,0 +1,19 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { supplyChainDigest } from "./supply-chain.ts";
export async function digestReleaseInputFiles(
files: readonly string[],
readBytes: (file: string) => Promise<Buffer> = readFile,
): Promise<string> {
const rows = await Promise.all(
[...files].sort().map(async (file) => ({
path: file,
sha256: createHash("sha256")
.update(await readBytes(file))
.digest("hex"),
})),
);
return supplyChainDigest(rows);
}
+85
View File
@@ -0,0 +1,85 @@
import { isVersionCompatible } from "../../src/application/policies/compatibility.ts";
import { verifyContractSet } from "../../src/contracts/contract-set.ts";
import type { InstalledContractPackageIdentity } from "../../src/contracts/external-contract-runtime.ts";
import type { ReleaseArtifact } from "../../src/contracts/release-artifacts.ts";
import { compareReleaseToRuntime } from "../../src/contracts/release-tokens.ts";
export type ReleaseRuntimeCoherenceInput = Readonly<{
release: ReleaseArtifact;
runtime: Readonly<{
BUILD_ID: string;
CONFIG_SCHEMA_VERSION: string;
API_CONTRACT_VERSION?: string;
RELEASE_ID: string;
}>;
contractPackages: readonly InstalledContractPackageIdentity[];
}>;
export type ReleaseRuntimeCoherence = Readonly<{
compatible: boolean;
mismatches: readonly string[];
warnings: readonly string[];
}>;
/**
* Verifies the runtime identity using the release schema's own contract model.
* V1 retains the scalar compatibility policy. V2 has no scalar projection: its
* identity is the exact compiled package tuple set and canonical set digest.
*/
export async function verifyReleaseRuntimeCoherence(
input: ReleaseRuntimeCoherenceInput,
): Promise<ReleaseRuntimeCoherence> {
if (input.release.schemaVersion === 1) {
if (input.runtime.API_CONTRACT_VERSION === undefined) {
return compareWithoutContractScalar(input, ["apiContractVersion"]);
}
return compareReleaseToRuntime(input.release, {
...input.runtime,
API_CONTRACT_VERSION: input.runtime.API_CONTRACT_VERSION,
});
}
const comparison = compareWithoutContractScalar(input);
const contractSet = await verifyContractSet({
expected: input.contractPackages,
manifest: input.release.contractSet,
});
if (contractSet.ok) return comparison;
const mismatches = Object.freeze([
...comparison.mismatches,
contractSet.code,
]);
return Object.freeze({
compatible: false,
mismatches,
warnings: comparison.warnings,
});
}
function compareWithoutContractScalar(
input: Pick<ReleaseRuntimeCoherenceInput, "release" | "runtime">,
initialMismatches: readonly string[] = [],
): ReleaseRuntimeCoherence {
const mismatches = [...initialMismatches];
if (input.release.buildId !== input.runtime.BUILD_ID) {
mismatches.push("buildId");
}
if (
!isVersionCompatible(
input.release.configSchemaVersion,
input.runtime.CONFIG_SCHEMA_VERSION,
)
) {
mismatches.push("configSchemaVersion");
}
const warnings =
input.release.releaseId === input.runtime.RELEASE_ID
? []
: ["releaseId"];
return Object.freeze({
compatible: mismatches.length === 0,
mismatches: Object.freeze(mismatches),
warnings: Object.freeze(warnings),
});
}
+239
View File
@@ -0,0 +1,239 @@
import { spawnSync } from "node:child_process";
import {
cp,
mkdir,
readFile,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import {
loadCiGateContract,
parseCiGateContract,
} from "../contracts/ci-gates.ts";
import { generateCiWorkflow } from "../generate-ci-workflow.ts";
export const REMOVAL_FIXTURE_COPY_TARGETS = Object.freeze([
"src", "tests", "recipes", "scripts", "schemas", "config", "public",
".gitea", ".storybook", "index.html", "package.json", "tsconfig.base.json",
"tsconfig.json", "tsconfig.app.json", "tsconfig.node.json", "tsconfig.test.json",
"tsconfig.recipes.json", "tsconfig.web-worker.json", "tsconfig.service-worker.json",
"vite.service-worker.config.ts", "vite.config.ts", "vitest.config.ts",
"playwright.config.ts", "playwright.capabilities.config.ts", "playwright.dev.config.ts",
"playwright.storybook.config.ts", "playwright.visual.config.ts", "eslint.config.ts",
".dependency-cruiser.json", ".nvmrc",
] as const);
export function requireRemovalFixtureEnvironment(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required for removal verification`);
return value;
}
export async function prepareRemovalFixture(
root: string,
copyTargets: readonly string[] = REMOVAL_FIXTURE_COPY_TARGETS,
): Promise<void> {
await rm(root, { recursive: true, force: true });
await mkdir(root, { recursive: true });
for (const target of copyTargets) {
await cp(target, path.join(root, target), { recursive: true });
}
await symlink(path.resolve("node_modules"), path.join(root, "node_modules"), "dir");
}
export function runRemovalFixturePnpm(
root: string,
pnpmCli: string,
script: string,
extra: readonly string[] = [],
): boolean {
return spawnSync(process.execPath, [pnpmCli, script, ...extra], {
cwd: root,
stdio: "inherit",
env: { ...process.env, CI_CONTRACT_MODE: "removal-fixture" },
}).status === 0;
}
export async function filesBelow(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
return (await Promise.all(entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesBelow(target) : [target];
}))).flat();
}
function isWithin(target: string, root: string): boolean {
const relative = path.relative(root, target);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
export async function runtimeImportGraph(
root: string,
runtimeSourceRoots: readonly string[],
): Promise<Readonly<{ dependentTests: readonly string[]; importingFiles: readonly string[] }>> {
const files = (await filesBelow(root))
.filter((file) => /\.(?:[cm]?ts|tsx)$/u.test(file))
.map((file) => path.resolve(file));
if (files.length === 0) {
throw new Error("removal fixture scanned module universe is empty");
}
const sourceSet = new Set(files);
const runtimeRoots = runtimeSourceRoots.map((entry) => path.resolve(root, entry));
const imports = new Map<string, readonly string[]>();
for (const file of files) {
const source = await readFile(file, "utf8");
const specifiers = [...source.matchAll(/(?:from\s*|import\s*\(\s*|import\s*)["']([^"']+)["']/gu)]
.map((match) => match[1])
.filter((specifier): specifier is string => typeof specifier === "string" && specifier.startsWith("."));
imports.set(file, specifiers.map((specifier) => {
const base = path.resolve(path.dirname(file), specifier);
return [base, `${base}.ts`, `${base}.tsx`, `${base}.mts`, `${base}.cts`, path.join(base, "index.ts"), path.join(base, "index.tsx")]
.find((candidate) => sourceSet.has(candidate)) ?? base;
}));
}
const memo = new Map<string, boolean>();
const reachesRuntime = (file: string, visiting = new Set<string>()): boolean => {
if (runtimeRoots.some((runtimeRoot) => isWithin(file, runtimeRoot))) return true;
const known = memo.get(file);
if (known !== undefined) return known;
if (visiting.has(file)) return false;
visiting.add(file);
const reaches = (imports.get(file) ?? []).some((dependency) =>
runtimeRoots.some((runtimeRoot) => isWithin(dependency, runtimeRoot)) ||
(sourceSet.has(dependency) && reachesRuntime(dependency, visiting))
);
visiting.delete(file);
memo.set(file, reaches);
return reaches;
};
const testsRoot = path.resolve(root, "tests");
return Object.freeze({
dependentTests: Object.freeze(files.filter((file) => isWithin(file, testsRoot) && reachesRuntime(file))),
importingFiles: Object.freeze(files.filter((file) =>
!runtimeRoots.some((runtimeRoot) => isWithin(file, runtimeRoot)) &&
(imports.get(file) ?? []).some((dependency) =>
runtimeRoots.some((runtimeRoot) => isWithin(dependency, runtimeRoot))
)
)),
});
}
export async function removeRuntimeDependentTests(
root: string,
runtimeSourceRoots: readonly string[],
): Promise<number> {
const graph = await runtimeImportGraph(root, runtimeSourceRoots);
if (graph.dependentTests.length === 0) {
throw new Error("removal fixture: no runtime-dependent tests discovered");
}
await Promise.all(graph.dependentTests.map((file) => rm(file, { force: true })));
return graph.dependentTests.length;
}
export async function assertNoRuntimeImports(
root: string,
runtimeSourceRoots: readonly string[],
capability: string,
): Promise<void> {
const graph = await runtimeImportGraph(root, runtimeSourceRoots);
if (graph.importingFiles.length > 0) {
throw new Error(`Removed ${capability} runtime is still imported by: ${graph.importingFiles.map((file) => path.relative(root, file)).join(", ")}`);
}
}
export function pruneScriptOrchestration(
scripts: Record<string, string>,
orchestrationScript: string,
removedScripts: ReadonlySet<string>,
): void {
const command = scripts[orchestrationScript];
if (!command) return;
scripts[orchestrationScript] = command.split(" && ").filter((segment) =>
![...removedScripts].some((removed) =>
new RegExp(`(?:^|\\s)(?:corepack\\s+)?pnpm\\s+${removed.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}(?:\\s|$)`, "u").test(segment)
)
).join(" && ");
}
export async function regenerateRemovalFixtureWorkflow(root: string): Promise<void> {
const contract = await loadCiGateContract(root, { mode: "removal-fixture" });
await generateCiWorkflow({ root, contract, check: false });
}
export async function pruneRemovalFixtureCiContract(options: Readonly<{
root: string;
removedScripts: ReadonlySet<string>;
removedEvidencePathFragments: readonly string[];
}>): Promise<void> {
const packagePath = path.join(options.root, "package.json");
const gatesPath = path.join(options.root, "config/ci/gates.json");
const packageDocument = JSON.parse(await readFile(packagePath, "utf8")) as {
scripts: Record<string, string>;
};
for (const script of options.removedScripts) delete packageDocument.scripts[script];
packageDocument.scripts["check:ci-workflow"] =
"node scripts/generate-ci-workflow.ts --check --reduced-removal-fixture";
packageDocument.scripts["check:ci"] =
"corepack pnpm check:artifact-schemas && node scripts/check-ci-contract.ts --reduced-removal-fixture && corepack pnpm check:ci-workflow";
const contract = structuredClone(
parseCiGateContract(JSON.parse(await readFile(gatesPath, "utf8"))),
);
const removedCommandIds = new Set(
contract.commands
.filter(({ script }) => options.removedScripts.has(script))
.map(({ id }) => id),
);
const missing = [...options.removedScripts].filter(
(script) => !contract.commands.some((command) => command.script === script),
);
if (missing.length > 0) {
throw new Error(`removal fixture CI command set is incomplete: ${missing.join(", ")}`);
}
const removedArtifactIds = new Set(
contract.artifacts
.filter(({ path: artifactPath }) =>
options.removedEvidencePathFragments.some((fragment) => artifactPath.includes(fragment))
)
.map(({ id }) => id),
);
for (const fragment of options.removedEvidencePathFragments) {
if (!contract.artifacts.some(({ path: artifactPath }) => artifactPath.includes(fragment))) {
throw new Error(`removal fixture CI evidence is missing: ${fragment}`);
}
}
contract.commands = contract.commands.filter(({ id }) => !removedCommandIds.has(id));
contract.artifacts = contract.artifacts
.filter(({ id }) => !removedArtifactIds.has(id))
.map((artifact) => artifact.production === "command-generated"
? {
...artifact,
producerCommandIds: artifact.producerCommandIds.filter(
(commandId) => !removedCommandIds.has(commandId),
),
}
: artifact)
.filter((artifact) =>
artifact.production !== "command-generated" || artifact.producerCommandIds.length > 0
);
const retainedArtifactIds = new Set(contract.artifacts.map(({ id }) => id));
for (const gate of contract.gates) {
gate.commandIds = gate.commandIds.filter((commandId) => !removedCommandIds.has(commandId));
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter((artifactId) =>
retainedArtifactIds.has(artifactId)
);
}
const referencedSchemaIds = new Set(contract.artifacts.map(({ schemaId }) => schemaId));
contract.artifactSchemas = contract.artifactSchemas.filter(({ id }) =>
referencedSchemaIds.has(id)
);
const validated = parseCiGateContract(contract, { mode: "removal-fixture" });
await Promise.all([
writeFile(packagePath, `${JSON.stringify(packageDocument, null, 2)}\n`),
writeFile(gatesPath, `${JSON.stringify(validated, null, 2)}\n`),
]);
}
+327
View File
@@ -0,0 +1,327 @@
import { spawnSync } from "node:child_process";
import {
lstat,
open,
readdir,
realpath,
} from "node:fs/promises";
import type { Stats } from "node:fs";
import path from "node:path";
export type GitFileListResult = Readonly<{
error?: Error;
status: number | null;
signal: NodeJS.Signals | null;
stdout: Buffer;
stderr: Buffer;
}>;
export type RepositoryFileInventory = Readonly<{
trackedFiles: readonly string[];
generatedFiles: readonly string[];
files: readonly string[];
}>;
export type RepositoryFileInventoryPolicy = Readonly<{
trackedRoots: readonly string[];
generatedRoots: readonly string[];
optionalRoots: readonly string[];
}>;
type InventoryOptions = Readonly<{
repositoryRoot?: string;
trackedRoots: readonly string[];
generatedRoots?: readonly string[];
optionalRoots?: readonly string[];
runGit?: (repositoryRoot: string) => GitFileListResult;
lstatPath?: (target: string) => Promise<Stats>;
realpathPath?: (target: string) => Promise<string>;
assertReadable?: (target: string) => Promise<void>;
}>;
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function requiredStringArray(
value: unknown,
label: string,
options: Readonly<{ allowEmpty: boolean }>,
): readonly string[] {
if (
!Array.isArray(value) ||
(!options.allowEmpty && value.length === 0) ||
value.some((entry) => typeof entry !== "string" || entry.length === 0)
) {
throw new TypeError(`${label} must be an array of non-empty strings`);
}
if (new Set(value).size !== value.length) {
throw new TypeError(`${label} must not contain duplicate roots`);
}
return Object.freeze([...value] as string[]);
}
export function parseRepositoryFileInventoryPolicy(
value: unknown,
): RepositoryFileInventoryPolicy {
if (!isRecord(value)) {
throw new TypeError("repository inventory policy must be an object");
}
return Object.freeze({
trackedRoots: requiredStringArray(value.trackedRoots, "trackedRoots", {
allowEmpty: false,
}),
generatedRoots: requiredStringArray(
value.generatedRoots,
"generatedRoots",
{ allowEmpty: true },
),
optionalRoots:
value.optionalRoots === undefined
? Object.freeze([])
: requiredStringArray(value.optionalRoots, "optionalRoots", {
allowEmpty: true,
}),
});
}
function defaultGitFileList(repositoryRoot: string): GitFileListResult {
const result = spawnSync("git", ["ls-files", "-z"], {
cwd: repositoryRoot,
encoding: "buffer",
maxBuffer: 64 * 1024 * 1024,
});
return {
...(result.error ? { error: result.error } : {}),
status: result.status,
signal: result.signal,
stdout: result.stdout ?? Buffer.alloc(0),
stderr: result.stderr ?? Buffer.alloc(0),
};
}
async function defaultAssertReadable(target: string): Promise<void> {
const handle = await open(target, "r");
await handle.close();
}
export function normalizeRepositoryRelativePath(
value: string,
label = "repository path",
): string {
if (
value.length === 0 ||
path.posix.isAbsolute(value) ||
path.win32.isAbsolute(value) ||
value.includes("\\") ||
value.includes("\0") ||
value.endsWith("/")
) {
throw new TypeError(`${label} must be a repository-relative POSIX path`);
}
const normalized = path.posix.normalize(value);
if (
normalized === "." ||
normalized === ".." ||
normalized.startsWith("../") ||
normalized !== value
) {
throw new TypeError(`${label} must be a repository-relative POSIX path`);
}
return normalized;
}
function isWithinRoot(root: string, target: string): boolean {
const relative = path.relative(root, target);
return (
relative === "" ||
(relative !== ".." &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative))
);
}
function parseGitFileList(result: GitFileListResult): string[] {
if (
result.error ||
result.status !== 0 ||
result.signal !== null ||
result.stderr.byteLength > 0
) {
const detail = result.error?.message ?? result.stderr.toString("utf8").trim();
throw new Error(
`git ls-files failed${result.signal ? ` (${result.signal})` : ""}${detail ? `: ${detail}` : ""}`,
);
}
if (result.stdout.byteLength === 0) return [];
if (result.stdout.at(-1) !== 0) {
throw new TypeError("git ls-files returned output without a terminal NUL");
}
let decoded: string;
try {
decoded = utf8Decoder.decode(result.stdout);
} catch (error) {
throw new TypeError("git ls-files returned malformed UTF-8", {
cause: error,
});
}
const rows = decoded.slice(0, -1).split("\0");
if (rows.some((row) => row.length === 0)) {
throw new TypeError("git ls-files returned an empty NUL-delimited path");
}
const normalized = rows.map((row) =>
normalizeRepositoryRelativePath(row, "git ls-files path"),
);
if (new Set(normalized).size !== normalized.length) {
throw new TypeError("git ls-files returned a duplicate path");
}
return normalized;
}
function hasErrorCode(error: unknown, code: string): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === code
);
}
export async function buildRepositoryFileInventory(
options: InventoryOptions,
): Promise<RepositoryFileInventory> {
const repositoryRoot = path.resolve(options.repositoryRoot ?? process.cwd());
const lstatPath = options.lstatPath ?? lstat;
const realpathPath = options.realpathPath ?? realpath;
const assertReadable = options.assertReadable ?? defaultAssertReadable;
const trackedRoots = options.trackedRoots.map((root) =>
normalizeRepositoryRelativePath(root, "tracked root"),
);
const generatedRoots = (options.generatedRoots ?? []).map((root) =>
normalizeRepositoryRelativePath(root, "generated root"),
);
const optionalRoots = new Set(
(options.optionalRoots ?? []).map((root) =>
normalizeRepositoryRelativePath(root, "optional root"),
),
);
for (const root of optionalRoots) {
if (!generatedRoots.includes(root)) {
throw new TypeError(`optional root is not generated: ${root}`);
}
}
const resolvedRepositoryRoot = await realpathPath(repositoryRoot);
async function validateRegularFile(relativePath: string): Promise<void> {
const absolutePath = path.resolve(repositoryRoot, relativePath);
if (!isWithinRoot(repositoryRoot, absolutePath)) {
throw new TypeError(`repository inventory path escapes root: ${relativePath}`);
}
const metadata = await lstatPath(absolutePath);
if (!metadata.isFile() || metadata.isSymbolicLink()) {
throw new TypeError(`repository inventory path is not a regular file: ${relativePath}`);
}
const resolvedPath = await realpathPath(absolutePath);
if (!isWithinRoot(resolvedRepositoryRoot, resolvedPath)) {
throw new TypeError(`repository inventory symlink escapes root: ${relativePath}`);
}
try {
await assertReadable(absolutePath);
} catch (error) {
throw new Error(`repository inventory file is unreadable: ${relativePath}`, {
cause: error,
});
}
}
async function validateRoot(
relativeRoot: string,
optional: boolean,
): Promise<Stats | null> {
const absoluteRoot = path.resolve(repositoryRoot, relativeRoot);
try {
const metadata = await lstatPath(absoluteRoot);
const resolvedRoot = await realpathPath(absoluteRoot);
if (!isWithinRoot(resolvedRepositoryRoot, resolvedRoot)) {
throw new TypeError(`repository root escapes repository: ${relativeRoot}`);
}
if (metadata.isSymbolicLink() || (!metadata.isFile() && !metadata.isDirectory())) {
throw new TypeError(`repository root is not a regular file or directory: ${relativeRoot}`);
}
return metadata;
} catch (error) {
if (optional && hasErrorCode(error, "ENOENT")) return null;
throw new Error(`required repository root is unavailable: ${relativeRoot}`, {
cause: error,
});
}
}
for (const root of trackedRoots) {
await validateRoot(root, false);
}
const trackedFiles = parseGitFileList(
(options.runGit ?? defaultGitFileList)(repositoryRoot),
).sort();
for (const root of trackedRoots) {
if (!trackedFiles.some((file) => file === root || file.startsWith(`${root}/`))) {
throw new Error(`required tracked file inventory is empty: ${root}`);
}
}
for (const file of trackedFiles) {
await validateRegularFile(file);
}
const generatedFiles: string[] = [];
async function collectGenerated(relativeTarget: string): Promise<void> {
const absoluteTarget = path.resolve(repositoryRoot, relativeTarget);
const metadata = await lstatPath(absoluteTarget);
if (metadata.isSymbolicLink()) {
throw new TypeError(`generated inventory path is a symlink: ${relativeTarget}`);
}
if (metadata.isFile()) {
await validateRegularFile(relativeTarget);
generatedFiles.push(relativeTarget);
return;
}
if (!metadata.isDirectory()) {
throw new TypeError(`generated inventory path is not regular: ${relativeTarget}`);
}
const entries = await readdir(absoluteTarget, { withFileTypes: true });
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
await collectGenerated(
normalizeRepositoryRelativePath(
`${relativeTarget}/${entry.name}`,
"generated inventory path",
),
);
}
}
for (const root of generatedRoots) {
const metadata = await validateRoot(root, optionalRoots.has(root));
if (metadata) await collectGenerated(root);
}
if (new Set(generatedFiles).size !== generatedFiles.length) {
throw new TypeError("generated inventory contains duplicate paths");
}
const uniqueTracked = [...trackedFiles].sort();
const uniqueGenerated = [...generatedFiles].sort();
const trackedSet = new Set(uniqueTracked);
const collisions = uniqueGenerated.filter((file) => trackedSet.has(file));
if (collisions.length > 0) {
throw new TypeError(
`tracked and generated inventory paths collide: ${collisions.join(", ")}`,
);
}
return Object.freeze({
trackedFiles: Object.freeze(uniqueTracked),
generatedFiles: Object.freeze(uniqueGenerated),
files: Object.freeze([...uniqueTracked, ...uniqueGenerated].sort()),
});
}
+346
View File
@@ -0,0 +1,346 @@
import { randomUUID } from "node:crypto";
import { constants, type Stats } from "node:fs";
import {
lstat,
mkdir,
open,
realpath,
rename,
rm,
} from "node:fs/promises";
import path from "node:path";
import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts";
type WritableHandle = Readonly<{
writeFile(data: string): Promise<unknown>;
sync(): Promise<unknown>;
close(): Promise<unknown>;
}>;
type DirectoryHandle = Readonly<{
sync(): Promise<unknown>;
close(): Promise<unknown>;
}>;
export type RiskCoverageArtifactFileSystem = Readonly<{
openFile(target: string, flags: number, mode: number): Promise<WritableHandle>;
openDirectory(target: string): Promise<DirectoryHandle>;
rename(source: string, destination: string): Promise<unknown>;
rm(target: string, options: Readonly<{ force: true }>): Promise<unknown>;
}>;
type WriterDependencies = Readonly<{
createNonce?: () => string;
fileSystem?: RiskCoverageArtifactFileSystem;
}>;
type ReadableHandle = Readonly<{
stat(): Promise<Stats>;
readFile(encoding: "utf8"): Promise<string>;
close(): Promise<unknown>;
}>;
type InputDependencies = Readonly<{
lstatPath?: (target: string) => Promise<Stats>;
realpathPath?: (target: string) => Promise<string>;
openFile?: (target: string, flags: number) => Promise<ReadableHandle>;
}>;
const defaultFileSystem: RiskCoverageArtifactFileSystem = Object.freeze({
openFile: async (target, flags, mode) => {
const handle = await open(target, flags, mode);
return {
writeFile: async (data) => handle.writeFile(data, "utf8"),
sync: async () => handle.sync(),
close: async () => handle.close(),
};
},
openDirectory: async (target) => {
const handle = await open(target, constants.O_RDONLY);
return {
sync: async () => handle.sync(),
close: async () => handle.close(),
};
},
rename,
rm,
});
function hasErrorCode(error: unknown, code: string): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === code
);
}
function isWithin(root: string, target: string): boolean {
const relative = path.relative(root, target);
return (
relative === "" ||
(relative !== ".." &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative))
);
}
function hasStableIdentity(metadata: Stats): boolean {
return (
Number.isSafeInteger(metadata.dev) &&
Number.isSafeInteger(metadata.ino) &&
metadata.dev > 0 &&
metadata.ino > 0
);
}
function sameFileIdentity(before: Stats, after: Stats): boolean {
if (!hasStableIdentity(before) || !hasStableIdentity(after)) {
throw new TypeError("stable file identity unavailable");
}
return before.dev === after.dev && before.ino === after.ino;
}
async function rejectSymlinkAncestors(
repositoryRoot: string,
relativePath: string,
label: string,
lstatPath: (target: string) => Promise<Stats>,
): Promise<void> {
let current = repositoryRoot;
let currentRelative = "";
const directory = path.posix.dirname(relativePath);
if (directory === ".") return;
for (const segment of directory.split("/")) {
current = path.join(current, segment);
currentRelative = currentRelative ? `${currentRelative}/${segment}` : segment;
const metadata = await lstatPath(current);
if (metadata.isSymbolicLink()) {
throw new TypeError(`${label} ancestor is a symlink: ${currentRelative}`);
}
if (!metadata.isDirectory()) {
throw new TypeError(`${label} ancestor is not a directory: ${currentRelative}`);
}
}
}
function assertDirectory(metadata: Stats, relativePath: string): void {
if (metadata.isSymbolicLink()) {
throw new TypeError(`artifact ancestor is a symlink: ${relativePath}`);
}
if (!metadata.isDirectory()) {
throw new TypeError(`artifact ancestor is not a directory: ${relativePath}`);
}
}
export async function readRiskCoverageInput(input: Readonly<{
repositoryRoot: string;
relativePath: string;
label: string;
}>, dependencies: InputDependencies = {}): Promise<Readonly<{
relativePath: string;
absolutePath: string;
text: string;
}>> {
const lstatPath = dependencies.lstatPath ?? lstat;
const realpathPath = dependencies.realpathPath ?? realpath;
const openFile = dependencies.openFile ??
((target: string, flags: number) => open(target, flags));
const relativePath = normalizeRepositoryRelativePath(
input.relativePath,
`${input.label} path`,
);
const repositoryRoot = path.resolve(input.repositoryRoot);
const repositoryRealpath = await realpathPath(repositoryRoot);
const absolutePath = path.resolve(repositoryRoot, relativePath);
await rejectSymlinkAncestors(
repositoryRoot,
relativePath,
input.label,
lstatPath,
);
const metadata = await lstatPath(absolutePath);
if (metadata.isSymbolicLink()) {
throw new TypeError(`${input.label} path is a symlink: ${relativePath}`);
}
if (!metadata.isFile()) {
throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`);
}
const resolvedPath = await realpathPath(absolutePath);
if (!isWithin(repositoryRealpath, resolvedPath)) {
throw new TypeError(`${input.label} path is outside repository: ${relativePath}`);
}
const handle = await openFile(
absolutePath,
constants.O_RDONLY | constants.O_NOFOLLOW,
);
try {
const openedMetadata = await handle.stat();
if (!openedMetadata.isFile()) {
throw new TypeError(`${input.label} path is not a regular file: ${relativePath}`);
}
if (!sameFileIdentity(metadata, openedMetadata)) {
throw new TypeError(`${input.label} path changed during validation: ${relativePath}`);
}
const text = await handle.readFile("utf8");
return Object.freeze({ relativePath, absolutePath, text });
} finally {
await handle.close();
}
}
export async function resolveRiskCoverageArtifactPath(input: Readonly<{
repositoryRoot: string;
relativePath: string;
inputPaths: readonly string[];
}>): Promise<string> {
const relativePath = normalizeRepositoryRelativePath(
input.relativePath,
"artifact path",
);
if (!relativePath.startsWith("artifacts/quality/")) {
throw new TypeError("artifact path must be below artifacts/quality");
}
const normalizedInputs = input.inputPaths.map((inputPath) =>
normalizeRepositoryRelativePath(inputPath, "input path"),
);
if (normalizedInputs.includes(relativePath)) {
throw new TypeError(`artifact path must not overwrite an input: ${relativePath}`);
}
const repositoryRoot = path.resolve(input.repositoryRoot);
const repositoryRealpath = await realpath(repositoryRoot);
const relativeDirectory = path.posix.dirname(relativePath);
let currentDirectory = repositoryRoot;
let currentRelative = "";
for (const segment of relativeDirectory.split("/")) {
currentDirectory = path.join(currentDirectory, segment);
currentRelative = currentRelative ? `${currentRelative}/${segment}` : segment;
let metadata: Stats;
try {
metadata = await lstat(currentDirectory);
} catch (error) {
if (!hasErrorCode(error, "ENOENT")) throw error;
try {
await mkdir(currentDirectory);
} catch (mkdirError) {
if (!hasErrorCode(mkdirError, "EEXIST")) throw mkdirError;
}
metadata = await lstat(currentDirectory);
}
assertDirectory(metadata, currentRelative);
const resolvedDirectory = await realpath(currentDirectory);
if (!isWithin(repositoryRealpath, resolvedDirectory)) {
throw new TypeError(`artifact ancestor is outside repository: ${currentRelative}`);
}
}
const absolutePath = path.resolve(repositoryRoot, relativePath);
let destinationMetadata: Stats | undefined;
try {
destinationMetadata = await lstat(absolutePath);
if (destinationMetadata.isSymbolicLink()) {
throw new TypeError(`artifact path is a symlink: ${relativePath}`);
}
if (!destinationMetadata.isFile()) {
throw new TypeError(`artifact path is not a regular file: ${relativePath}`);
}
} catch (error) {
if (!hasErrorCode(error, "ENOENT")) throw error;
}
if (destinationMetadata) {
if (!hasStableIdentity(destinationMetadata)) {
throw new TypeError("artifact stable file identity unavailable");
}
const destinationRealpath = await realpath(absolutePath);
for (const inputPath of normalizedInputs) {
const inputAbsolutePath = path.resolve(repositoryRoot, inputPath);
const inputRealpath = await realpath(inputAbsolutePath);
const inputMetadata = await lstat(inputAbsolutePath);
if (!hasStableIdentity(inputMetadata)) {
throw new TypeError(`input stable file identity unavailable: ${inputPath}`);
}
if (
inputRealpath === destinationRealpath ||
(inputMetadata.dev === destinationMetadata.dev &&
inputMetadata.ino === destinationMetadata.ino)
) {
throw new TypeError(`artifact path is the same file as an input: ${inputPath}`);
}
}
}
return absolutePath;
}
export async function writeRiskCoverageArtifactAtomic(
input: Readonly<{
repositoryRoot: string;
relativePath: string;
inputPaths: readonly string[];
value: unknown;
}>,
dependencies: WriterDependencies = {},
): Promise<void> {
const serialized = JSON.stringify(input.value, null, 2);
if (serialized === undefined) {
throw new TypeError("risk coverage artifact is not JSON serializable");
}
const destination = await resolveRiskCoverageArtifactPath(input);
const temporaryPath = path.join(
path.dirname(destination),
`.${path.basename(destination)}.${(dependencies.createNonce ?? randomUUID)()}.tmp`,
);
const fileSystem = dependencies.fileSystem ?? defaultFileSystem;
let ownsTemporaryFile = false;
try {
const handle = await fileSystem.openFile(
temporaryPath,
constants.O_WRONLY |
constants.O_CREAT |
constants.O_EXCL |
constants.O_NOFOLLOW,
0o600,
);
ownsTemporaryFile = true;
let primaryFailure: unknown;
try {
await handle.writeFile(`${serialized}\n`);
await handle.sync();
} catch (error) {
primaryFailure = error;
}
try {
await handle.close();
} catch (error) {
primaryFailure ??= error;
}
if (primaryFailure !== undefined) throw primaryFailure;
await fileSystem.rename(temporaryPath, destination);
ownsTemporaryFile = false;
const directoryHandle = await fileSystem.openDirectory(path.dirname(destination));
try {
try {
await directoryHandle.sync();
} catch (error) {
// Windows and some filesystems do not support fsync on directory handles.
if (!hasErrorCode(error, "EINVAL") && !hasErrorCode(error, "ENOTSUP")) {
throw error;
}
}
} finally {
await directoryHandle.close();
}
} catch (error) {
if (ownsTemporaryFile) {
try {
await fileSystem.rm(temporaryPath, { force: true });
} catch {
// Preserve the publication failure and clean only our nonce-owned path.
}
}
throw error;
}
}
+901
View File
@@ -0,0 +1,901 @@
import { constants, type Dirent, type Stats } from "node:fs";
import {
lstat,
open,
readdir,
realpath,
} from "node:fs/promises";
import path from "node:path";
import babelParser from "@babel/eslint-parser";
import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts";
const coverageMetrics = [
"lines",
"statements",
"functions",
"branches",
] as const;
const teamIdPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
const maximumWaiverDurationMs = 90 * 24 * 60 * 60 * 1_000;
export const REQUIRED_HIGH_RISK_PATHS = Object.freeze([
"src/adapters/http/http-execution-v3.ts",
"src/adapters/http/request-builder.ts",
"src/adapters/http/bounded-body-reader.ts",
"src/adapters/http/bounded-json.ts",
"src/bootstrap/read-bounded-boot-json.ts",
"src/adapters/service-worker/service-worker-lifecycle.ts",
"src/adapters/query-cache/server-state-scope-runtime.ts",
"src/bootstrap/load-release-manifest.ts",
] as const);
type CoverageMetric = (typeof coverageMetrics)[number];
type Thresholds = Readonly<Record<CoverageMetric, number>>;
type CoverageCounter = Readonly<{
total: number;
covered: number;
skipped: number;
pct: number;
}>;
type CoverageMetrics = Readonly<Record<CoverageMetric, CoverageCounter>>;
export type RiskCoveragePolicy = Readonly<{
schemaVersion: 2;
repositoryBaseline: number;
generatedPaths: readonly string[];
summary: Thresholds;
criticalModules: readonly Readonly<{
path: string;
owner: string;
minimum: Thresholds;
}>[];
highRiskPaths: readonly string[];
waivers: readonly Readonly<{
path: string;
owner: string;
reason: string;
expiresAt: string;
}>[];
}>;
export type ProductionModuleInventory = Readonly<{
files: readonly string[];
preExclusionTotal: number;
generatedExclusions: readonly string[];
counterBearingModules: readonly string[];
counterlessModules: readonly string[];
}>;
export type RiskCoverageResult = Readonly<{
status: "PASS" | "FAIL";
selectedTotal: number;
repositoryTotal: number;
counterBearingTotal: number;
instrumentedCounterBearingTotal: number;
counterlessTotal: number;
counterlessModules: readonly string[];
preExclusionTotal: number;
generatedExclusionCount: number;
generatedExclusions: readonly string[];
ownershipScope: "ALL_POLICY_HIGH_RISK";
ownedHighRiskPaths: readonly string[];
waivedHighRiskPaths: readonly string[];
uncoveredModules: readonly string[];
results: readonly Readonly<{
scope: string;
metric: CoverageMetric;
threshold: number;
received: number;
passed: boolean;
}>[];
failures: readonly string[];
}>;
type ReadableFileHandle = Readonly<{
stat(): Promise<Stats>;
readFile(encoding: "utf8"): Promise<string>;
close(): Promise<unknown>;
}>;
type InventoryOptions = Readonly<{
repositoryRoot?: string;
generatedPaths?: readonly string[];
readDirectory?: (target: string) => Promise<Dirent[]>;
lstatPath?: (target: string) => Promise<Stats>;
realpathPath?: (target: string) => Promise<string>;
openFile?: (target: string, flags: number) => Promise<ReadableFileHandle>;
}>;
class FileIdentityChangedError extends Error {}
class StableFileIdentityUnavailableError extends Error {}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function assertExactKeys(
value: Record<string, unknown>,
allowed: readonly string[],
label: string,
): void {
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
if (unknown.length > 0) {
throw new TypeError(`${label} has unknown fields: ${unknown.sort().join(", ")}`);
}
}
function exactSourcePath(value: unknown, label: string): string {
if (
typeof value !== "string" ||
["*", "?", "[", "]", "{", "}"].some((character) => value.includes(character))
) {
throw new TypeError(`${label} must be an exact repository-relative POSIX path`);
}
const normalized = normalizeRepositoryRelativePath(value, label);
if (!normalized.startsWith("src/") || !/\.tsx?$/u.test(normalized)) {
throw new TypeError(`${label} must identify a TypeScript module below src`);
}
return normalized;
}
function uniquePaths(
value: unknown,
label: string,
options: Readonly<{ allowEmpty: boolean }> = { allowEmpty: true },
): readonly string[] {
if (!Array.isArray(value) || (!options.allowEmpty && value.length === 0)) {
throw new TypeError(
`${label} must be an array${options.allowEmpty ? "" : " with at least one path"}`,
);
}
const paths = value.map((entry) => exactSourcePath(entry, `${label} entry`));
if (new Set(paths).size !== paths.length) {
throw new TypeError(`${label} contains a duplicate path`);
}
return Object.freeze(paths);
}
function teamId(value: unknown, label: string): string {
if (typeof value !== "string" || !teamIdPattern.test(value)) {
throw new TypeError(`${label} must be a canonical team id`);
}
return value;
}
function thresholds(value: unknown, label: string): Thresholds {
if (!isRecord(value)) throw new TypeError(`${label} must be an object`);
assertExactKeys(value, coverageMetrics, label);
if (coverageMetrics.some((metric) => !(metric in value))) {
throw new TypeError(`${label} must define all coverage metrics`);
}
const parsed = {} as Record<CoverageMetric, number>;
for (const metric of coverageMetrics) {
const threshold = value[metric];
if (
typeof threshold !== "number" ||
!Number.isFinite(threshold) ||
threshold <= 0 ||
threshold > 100
) {
throw new TypeError(
`${label}.${metric} minimum must be a finite number greater than 0 and at most 100`,
);
}
parsed[metric] = threshold;
}
return Object.freeze(parsed);
}
function waiverReason(value: unknown, label: string): string {
if (typeof value !== "string" || value.length < 12 || value.length > 240) {
throw new TypeError(`${label} must contain 12 to 240 characters`);
}
if (value !== value.trim()) {
throw new TypeError(`${label} must not contain surrounding whitespace`);
}
if (
[...value].some((character) => {
const codePoint = character.codePointAt(0) ?? 0;
return codePoint <= 31 || codePoint === 127;
})
) {
throw new TypeError(`${label} must not contain control characters`);
}
return value;
}
export function parseRiskCoveragePolicy(
value: unknown,
options: Readonly<{ now?: number }> = {},
): RiskCoveragePolicy {
if (!isRecord(value)) throw new TypeError("risk coverage policy must be an object");
assertExactKeys(
value,
[
"schemaVersion",
"repositoryBaseline",
"generatedPaths",
"summary",
"criticalModules",
"highRiskPaths",
"waivers",
],
"risk coverage policy",
);
if (value.schemaVersion !== 2) {
throw new TypeError("risk coverage policy schemaVersion must be 2");
}
if (
typeof value.repositoryBaseline !== "number" ||
!Number.isSafeInteger(value.repositoryBaseline) ||
value.repositoryBaseline <= 0
) {
throw new TypeError("repositoryBaseline must be a positive safe integer");
}
const generatedPaths = uniquePaths(value.generatedPaths, "generatedPaths");
const highRiskPaths = uniquePaths(value.highRiskPaths, "highRiskPaths", {
allowEmpty: false,
});
if (!Array.isArray(value.criticalModules) || value.criticalModules.length === 0) {
throw new TypeError("criticalModules must be a non-empty array");
}
const criticalModules = value.criticalModules.map((candidate, index) => {
if (!isRecord(candidate)) {
throw new TypeError(`criticalModules[${index}] must be an object`);
}
assertExactKeys(candidate, ["path", "owner", "minimum"], `criticalModules[${index}]`);
return Object.freeze({
path: exactSourcePath(candidate.path, `criticalModules[${index}].path`),
owner: teamId(candidate.owner, `criticalModules[${index}].owner`),
minimum: thresholds(candidate.minimum, `criticalModules[${index}].minimum`),
});
});
if (new Set(criticalModules.map((entry) => entry.path)).size !== criticalModules.length) {
throw new TypeError("criticalModules contains a duplicate path");
}
if (!Array.isArray(value.waivers)) throw new TypeError("waivers must be an array");
const currentTime = options.now ?? Date.now();
if (!Number.isFinite(currentTime)) throw new TypeError("policy time must be finite");
const waivers = value.waivers.map((candidate, index) => {
if (!isRecord(candidate)) throw new TypeError(`waivers[${index}] must be an object`);
assertExactKeys(candidate, ["path", "owner", "reason", "expiresAt"], `waivers[${index}]`);
const waiverPath = exactSourcePath(candidate.path, `waivers[${index}].path`);
const expiresAt = candidate.expiresAt;
if (typeof expiresAt !== "string") {
throw new TypeError(`waivers[${index}].expiresAt must be a canonical UTC ISO timestamp`);
}
const expiry = Date.parse(expiresAt);
if (!Number.isFinite(expiry) || new Date(expiry).toISOString() !== expiresAt) {
throw new TypeError(`waivers[${index}].expiresAt must be a canonical UTC ISO timestamp`);
}
if (expiry <= currentTime) throw new TypeError(`waivers[${index}] is expired`);
if (expiry - currentTime > maximumWaiverDurationMs) {
throw new TypeError(`waivers[${index}] expiry must be within 90 days`);
}
if (!highRiskPaths.includes(waiverPath)) {
throw new TypeError(`waivers[${index}] is stale because ${waiverPath} is not high-risk`);
}
return Object.freeze({
path: waiverPath,
owner: teamId(candidate.owner, `waivers[${index}].owner`),
reason: waiverReason(candidate.reason, `waivers[${index}].reason`),
expiresAt,
});
});
if (new Set(waivers.map((entry) => entry.path)).size !== waivers.length) {
throw new TypeError("waivers contains a duplicate path");
}
const criticalPaths = new Set(criticalModules.map((entry) => entry.path));
const waiverPaths = new Set(waivers.map((entry) => entry.path));
for (const highRiskPath of highRiskPaths) {
if (criticalPaths.has(highRiskPath) && waiverPaths.has(highRiskPath)) {
throw new TypeError(
`high-risk module cannot have both a critical owner and waiver: ${highRiskPath}`,
);
}
if (!criticalPaths.has(highRiskPath) && !waiverPaths.has(highRiskPath)) {
throw new TypeError(`high-risk module has no owner or waiver: ${highRiskPath}`);
}
}
return Object.freeze({
schemaVersion: 2,
repositoryBaseline: value.repositoryBaseline,
generatedPaths,
summary: thresholds(value.summary, "summary"),
criticalModules: Object.freeze(criticalModules),
highRiskPaths,
waivers: Object.freeze(waivers),
});
}
export function parseRepositoryRiskCoveragePolicy(
value: unknown,
options: Readonly<{ now?: number }> = {},
): RiskCoveragePolicy {
const policy = parseRiskCoveragePolicy(value, options);
for (const requiredPath of REQUIRED_HIGH_RISK_PATHS) {
if (!policy.highRiskPaths.includes(requiredPath)) {
throw new TypeError(`required high-risk path is missing: ${requiredPath}`);
}
if (policy.generatedPaths.includes(requiredPath)) {
throw new TypeError(
`required high-risk path cannot be generated-excluded: ${requiredPath}`,
);
}
}
return policy;
}
function isWithin(root: string, target: string): boolean {
const relative = path.relative(root, target);
return (
relative === "" ||
(relative !== ".." &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative))
);
}
function hasStableIdentity(metadata: Stats): boolean {
return (
Number.isSafeInteger(metadata.dev) &&
Number.isSafeInteger(metadata.ino) &&
metadata.dev > 0 &&
metadata.ino > 0
);
}
function sameFileIdentity(before: Stats, after: Stats): boolean {
if (!hasStableIdentity(before) || !hasStableIdentity(after)) {
throw new StableFileIdentityUnavailableError(
"stable file identity unavailable",
);
}
return before.dev === after.dev && before.ino === after.ino;
}
export function isProductionModulePath(relativePath: string): boolean {
return (
/\.tsx?$/u.test(relativePath) &&
!/\.d\.ts$/u.test(relativePath) &&
!/\.stories\.tsx?$/u.test(relativePath)
);
}
type TypeScriptAstNode = Readonly<{
type?: unknown;
body?: unknown;
declaration?: unknown;
declare?: unknown;
const?: unknown;
importKind?: unknown;
}>;
function statementIsCoverageCounterBearing(value: unknown): boolean {
if (!isRecord(value) || typeof value.type !== "string") {
throw new TypeError("TypeScript parser returned an invalid statement");
}
const statement = value as TypeScriptAstNode;
if (
[
"EmptyStatement",
"TSDeclareFunction",
"TSInterfaceDeclaration",
"TSNamespaceExportDeclaration",
"TSTypeAliasDeclaration",
].includes(statement.type as string)
) {
return false;
}
if (statement.type === "ImportDeclaration") {
return false;
}
if (
statement.type === "ExportAllDeclaration" ||
statement.type === "TSExportAssignment"
) {
return statement.type === "TSExportAssignment";
}
if (
statement.type === "ExportNamedDeclaration" ||
statement.type === "ExportDefaultDeclaration"
) {
return statement.declaration !== null && statement.declaration !== undefined
? statementIsCoverageCounterBearing(statement.declaration)
: false;
}
if (
statement.type === "FunctionDeclaration" ||
statement.type === "VariableDeclaration" ||
statement.type === "ClassDeclaration" ||
statement.type === "TSModuleDeclaration"
) {
return statement.declare !== true &&
(statement.type !== "FunctionDeclaration" || statement.body !== null);
}
if (statement.type === "TSEnumDeclaration") {
return statement.declare !== true && statement.const !== true;
}
if (statement.type === "TSImportEqualsDeclaration") {
return statement.importKind !== "type" && statement.declare !== true;
}
return true;
}
export function hasCoverageCounterBearingStatements(
source: string,
relativePath: string,
): boolean {
let parsed: unknown;
try {
parsed = babelParser.parse(source, {
sourceType: "module",
requireConfigFile: false,
filePath: relativePath,
babelOptions: {
parserOpts: {
plugins: [
"typescript",
...(relativePath.endsWith(".tsx") ? ["jsx"] : []),
],
},
},
});
} catch (error) {
throw new TypeError(`production module has invalid TypeScript syntax: ${relativePath}`, {
cause: error,
});
}
if (!isRecord(parsed) || !Array.isArray(parsed.body)) {
throw new TypeError("TypeScript parser returned an invalid program");
}
return parsed.body.some(statementIsCoverageCounterBearing);
}
export async function buildProductionModuleInventory(
options: InventoryOptions = {},
): Promise<ProductionModuleInventory> {
const repositoryRoot = path.resolve(options.repositoryRoot ?? process.cwd());
const sourceRoot = path.join(repositoryRoot, "src");
const readDirectory = options.readDirectory ??
((target: string) => readdir(target, { withFileTypes: true }));
const lstatPath = options.lstatPath ?? lstat;
const realpathPath = options.realpathPath ?? realpath;
const openFile = options.openFile ??
((target: string, flags: number) => open(target, flags));
const generatedPaths = uniquePaths(options.generatedPaths ?? [], "generatedPaths");
const generated = new Set(generatedPaths);
const repositoryRealpath = await realpathPath(repositoryRoot);
const sourceMetadata = await lstatPath(sourceRoot);
if (!sourceMetadata.isDirectory() || sourceMetadata.isSymbolicLink()) {
throw new TypeError("production source root is not a regular directory: src");
}
const sourceRealpath = await realpathPath(sourceRoot);
if (!isWithin(repositoryRealpath, sourceRealpath)) {
throw new TypeError("production source root is outside repository");
}
const allModules: string[] = [];
const counterBearingModules: string[] = [];
const counterlessModules: string[] = [];
async function visit(relativeDirectory: string): Promise<void> {
const absoluteDirectory = path.join(repositoryRoot, relativeDirectory);
const entries = await readDirectory(absoluteDirectory);
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
const relativeTarget = normalizeRepositoryRelativePath(
`${relativeDirectory}/${entry.name}`,
"production inventory path",
);
const absoluteTarget = path.join(repositoryRoot, relativeTarget);
const metadata = await lstatPath(absoluteTarget);
if (metadata.isSymbolicLink() || entry.isSymbolicLink()) {
throw new TypeError(`production inventory path is a symlink: ${relativeTarget}`);
}
if (metadata.isDirectory()) {
await visit(relativeTarget);
continue;
}
if (!isProductionModulePath(relativeTarget)) continue;
if (!metadata.isFile()) {
throw new TypeError(`production inventory path is not a regular file: ${relativeTarget}`);
}
const resolvedTarget = await realpathPath(absoluteTarget);
if (!isWithin(repositoryRealpath, resolvedTarget)) {
throw new TypeError(`production inventory path is outside repository: ${relativeTarget}`);
}
let handle: ReadableFileHandle | undefined;
let source: string;
try {
handle = await openFile(
absoluteTarget,
constants.O_RDONLY | constants.O_NOFOLLOW,
);
const openedMetadata = await handle.stat();
if (!openedMetadata.isFile()) {
throw new TypeError("opened target is not a regular file");
}
if (!sameFileIdentity(metadata, openedMetadata)) {
throw new FileIdentityChangedError("opened file identity changed");
}
source = await handle.readFile("utf8");
} catch (error) {
if (error instanceof StableFileIdentityUnavailableError) {
throw new Error(
`production inventory stable file identity unavailable: ${relativeTarget}`,
{ cause: error },
);
}
if (error instanceof FileIdentityChangedError) {
throw new Error(
`production inventory file changed during validation: ${relativeTarget}`,
{ cause: error },
);
}
throw new Error(`production inventory file is unreadable: ${relativeTarget}`, {
cause: error,
});
} finally {
await handle?.close();
}
allModules.push(relativeTarget);
if (hasCoverageCounterBearingStatements(source, relativeTarget)) {
counterBearingModules.push(relativeTarget);
} else {
counterlessModules.push(relativeTarget);
}
}
}
await visit("src");
for (const generatedPath of generatedPaths) {
if (!allModules.includes(generatedPath)) {
throw new TypeError(`generated path is stale or not a production module: ${generatedPath}`);
}
}
const inventory = allModules.filter((file) => !generated.has(file)).sort();
if (inventory.length === 0) throw new Error("production module inventory is empty");
if (new Set(inventory).size !== inventory.length) {
throw new TypeError("production module inventory contains a duplicate path");
}
return Object.freeze({
files: Object.freeze(inventory),
preExclusionTotal: allModules.length,
generatedExclusions: Object.freeze([...generatedPaths].sort()),
counterBearingModules: Object.freeze(
counterBearingModules.filter((file) => !generated.has(file)).sort(),
),
counterlessModules: Object.freeze(
counterlessModules.filter((file) => !generated.has(file)).sort(),
),
});
}
export function normalizeCoverageProducerPath(input: Readonly<{
repositoryRoot: string;
rawPath: string;
platform?: "posix" | "win32";
}>): string {
const platform = input.platform ?? (process.platform === "win32" ? "win32" : "posix");
const pathApi = platform === "win32" ? path.win32 : path.posix;
const { rawPath } = input;
if (rawPath.includes("\0")) throw new TypeError("coverage path contains NUL");
if (platform === "posix" && rawPath.includes("\\")) {
throw new TypeError("coverage path must use POSIX separators");
}
if (platform === "win32" && rawPath.includes("\\") && !pathApi.isAbsolute(rawPath)) {
throw new TypeError("relative coverage path must use POSIX separators");
}
if (pathApi.isAbsolute(rawPath)) {
const root = pathApi.resolve(input.repositoryRoot);
const relative = pathApi.relative(root, pathApi.resolve(rawPath));
if (
!relative ||
relative === ".." ||
relative.startsWith(`..${pathApi.sep}`) ||
pathApi.isAbsolute(relative)
) {
throw new TypeError(`coverage path is outside repository: ${rawPath}`);
}
return normalizeRepositoryRelativePath(
relative.split(pathApi.sep).join("/"),
"coverage path",
);
}
return normalizeRepositoryRelativePath(rawPath, "coverage path");
}
function expectedPct(total: number, covered: number): number {
return total === 0 ? 100 : Math.floor((covered / total) * 10_000) / 100;
}
function parseCoverageCounter(value: unknown, label: string): CoverageCounter {
if (!isRecord(value)) throw new TypeError(`${label} must be an object`);
const fields = ["total", "covered", "skipped", "pct"] as const;
assertExactKeys(value, fields, label);
if (fields.some((field) => !(field in value))) {
throw new TypeError(`${label} must define total, covered, skipped, and pct`);
}
for (const count of ["total", "covered", "skipped"] as const) {
if (
typeof value[count] !== "number" ||
!Number.isSafeInteger(value[count]) ||
value[count] < 0
) {
throw new TypeError(`${label}.${count} must be a nonnegative safe integer`);
}
}
const total = value.total as number;
const covered = value.covered as number;
const skipped = value.skipped as number;
if (covered + skipped > total) {
throw new TypeError(`${label} covered plus skipped must not exceed total`);
}
const pct = value.pct;
const calculatedPct = expectedPct(total, covered);
if (typeof pct !== "number" || !Number.isFinite(pct) || pct !== calculatedPct) {
throw new TypeError(`${label}.pct must equal ${calculatedPct}`);
}
return Object.freeze({ total, covered, skipped, pct });
}
function parseCoverageMetrics(value: unknown, label: string): CoverageMetrics {
if (!isRecord(value)) throw new TypeError(`${label} must be an object`);
const unknownMetrics = Object.keys(value).filter(
(metric) => metric !== "branchesTrue" && !coverageMetrics.includes(metric as CoverageMetric),
);
if (unknownMetrics.length > 0) {
throw new TypeError(
`${label} has unknown coverage metric keys: ${unknownMetrics.sort().join(", ")}`,
);
}
if (value.branchesTrue !== undefined) {
parseCoverageCounter(value.branchesTrue, `${label}.branchesTrue`);
}
const parsed = {} as Record<CoverageMetric, CoverageCounter>;
for (const metric of coverageMetrics) {
parsed[metric] = parseCoverageCounter(value[metric], `${label}.${metric}`);
}
return Object.freeze(parsed);
}
function aggregateCoverage(selected: readonly CoverageMetrics[]): CoverageMetrics {
const aggregate = {} as Record<CoverageMetric, CoverageCounter>;
for (const metric of coverageMetrics) {
let total = 0;
let covered = 0;
let skipped = 0;
for (const metrics of selected) {
total += metrics[metric].total;
covered += metrics[metric].covered;
skipped += metrics[metric].skipped;
if (![total, covered, skipped].every(Number.isSafeInteger)) {
throw new TypeError(`recomputed coverage ${metric} count exceeds safe integer range`);
}
}
aggregate[metric] = Object.freeze({
total,
covered,
skipped,
pct: expectedPct(total, covered),
});
}
return Object.freeze(aggregate);
}
function assertMatchingTotal(
producer: CoverageMetrics,
recomputed: CoverageMetrics,
): void {
for (const metric of coverageMetrics) {
const actual = producer[metric];
const expected = recomputed[metric];
if (
actual.total !== expected.total ||
actual.covered !== expected.covered ||
actual.skipped !== expected.skipped ||
actual.pct !== expected.pct
) {
throw new TypeError(
`coverage total.${metric} does not match recomputed inventory total`,
);
}
}
}
export function evaluateRiskCoverage(input: Readonly<{
repositoryRoot?: string;
inventory: ProductionModuleInventory;
policy: RiskCoveragePolicy;
summary: unknown;
}>): RiskCoverageResult {
const repositoryRoot = path.resolve(input.repositoryRoot ?? process.cwd());
const inventory = input.inventory.files.map((file) => exactSourcePath(file, "inventory path"));
if (inventory.length === 0) throw new TypeError("production module inventory is empty");
if (new Set(inventory).size !== inventory.length) {
throw new TypeError("production module inventory contains a duplicate path");
}
const counterBearingModules = input.inventory.counterBearingModules.map((file) =>
exactSourcePath(file, "counter-bearing inventory path"),
);
const counterlessModules = input.inventory.counterlessModules.map((file) =>
exactSourcePath(file, "counterless inventory path"),
);
const counterBearingSet = new Set(counterBearingModules);
const counterlessSet = new Set(counterlessModules);
const partition = [...counterBearingModules, ...counterlessModules].sort();
if (
counterBearingSet.size !== counterBearingModules.length ||
counterlessSet.size !== counterlessModules.length ||
counterBearingModules.some((file) => counterlessSet.has(file)) ||
partition.join("\n") !== [...inventory].sort().join("\n")
) {
throw new TypeError("production inventory counter-bearing provenance is inconsistent");
}
const generatedExclusions = input.inventory.generatedExclusions.map((file) =>
exactSourcePath(file, "generated exclusion"),
);
if (
new Set(generatedExclusions).size !== generatedExclusions.length ||
input.inventory.preExclusionTotal !== inventory.length + generatedExclusions.length ||
!Number.isSafeInteger(input.inventory.preExclusionTotal)
) {
throw new TypeError("production inventory provenance is inconsistent");
}
const policyGenerated = [...input.policy.generatedPaths].sort();
if (generatedExclusions.join("\n") !== policyGenerated.join("\n")) {
throw new TypeError("production inventory generated exclusions do not match policy");
}
if (!isRecord(input.summary) || !("total" in input.summary)) {
throw new TypeError("coverage summary must contain total metrics");
}
const totalMetrics = parseCoverageMetrics(input.summary.total, "coverage total");
const selected = new Map<string, CoverageMetrics>();
for (const [rawPath, rawMetrics] of Object.entries(input.summary)) {
if (rawPath === "total") continue;
const normalized = normalizeCoverageProducerPath({ repositoryRoot, rawPath });
if (selected.has(normalized)) throw new TypeError(`duplicate coverage path: ${normalized}`);
selected.set(normalized, parseCoverageMetrics(rawMetrics, `coverage ${normalized}`));
}
const inventorySet = new Set(inventory);
const generatedSet = new Set(generatedExclusions);
for (const selectedPath of selected.keys()) {
if (!inventorySet.has(selectedPath) && !generatedSet.has(selectedPath)) {
throw new TypeError(`unexpected coverage path outside inventory: ${selectedPath}`);
}
}
const inventoryMetrics = inventory.flatMap((file) => {
const metrics = selected.get(file);
return metrics ? [metrics] : [];
});
const zeroCoverageModules = inventory.filter((file) => {
const metrics = selected.get(file);
return (
metrics !== undefined &&
coverageMetrics.every((metric) => metrics[metric].total === 0)
);
});
const zeroCoverageSet = new Set(zeroCoverageModules);
const zeroCounterBearingModules = zeroCoverageModules.filter((file) =>
counterBearingSet.has(file),
);
const counterlessWithCounters = counterlessModules.filter((file) => {
const metrics = selected.get(file);
return metrics !== undefined && !zeroCoverageSet.has(file);
});
const instrumentedCounterBearingTotal = counterBearingModules.filter((file) => {
const metrics = selected.get(file);
return metrics !== undefined && !zeroCoverageSet.has(file);
}).length;
assertMatchingTotal(totalMetrics, aggregateCoverage([...selected.values()]));
const recomputedInventoryMetrics = aggregateCoverage(inventoryMetrics);
const failures: string[] = [];
const results: Array<{
scope: string;
metric: CoverageMetric;
threshold: number;
received: number;
passed: boolean;
}> = [];
function evaluate(scope: string, actual: CoverageMetrics, minimum: Thresholds): void {
for (const metric of coverageMetrics) {
const threshold = minimum[metric];
const received = actual[metric].pct;
const hasCoverageTotal = actual[metric].total > 0;
const passed = hasCoverageTotal && received >= threshold;
results.push({ scope, metric, threshold, received, passed });
if (!hasCoverageTotal) {
failures.push(`${scope}.${metric} coverage total must be greater than 0`);
} else if (!passed) {
failures.push(`${scope}.${metric} expected >= ${threshold}, received ${received}`);
}
}
}
evaluate("total", recomputedInventoryMetrics, input.policy.summary);
if (inventory.length < input.policy.repositoryBaseline) {
failures.push(
`repository module baseline expected >= ${input.policy.repositoryBaseline}, received ${inventory.length}`,
);
}
const uncoveredModules = inventory
.filter(
(file) =>
!selected.has(file) ||
(counterBearingSet.has(file) && zeroCoverageSet.has(file)),
)
.sort();
failures.push(
...inventory
.filter((file) => !selected.has(file))
.map((file) => `production module missing from coverage: ${file}`),
...zeroCounterBearingModules.map(
(file) => `counter-bearing module has zero coverage totals: ${file}`,
),
...counterlessWithCounters.map(
(file) => `counterless module has coverage counters: ${file}`,
),
);
for (const modulePolicy of input.policy.criticalModules) {
if (!inventorySet.has(modulePolicy.path)) {
failures.push(`critical module is outside production inventory: ${modulePolicy.path}`);
continue;
}
if (counterlessSet.has(modulePolicy.path)) {
failures.push(
`critical policy-sensitive module cannot be counterless: ${modulePolicy.path}`,
);
}
const actual = selected.get(modulePolicy.path);
if (!actual) {
failures.push(`critical module missing from coverage: ${modulePolicy.path}`);
continue;
}
evaluate(modulePolicy.path, actual, modulePolicy.minimum);
}
const criticalPaths = new Set(input.policy.criticalModules.map((entry) => entry.path));
const waiverPaths = new Set(input.policy.waivers.map((entry) => entry.path));
for (const highRiskPath of input.policy.highRiskPaths) {
if (!inventorySet.has(highRiskPath)) {
failures.push(`high-risk module is outside production inventory: ${highRiskPath}`);
}
if (counterlessSet.has(highRiskPath)) {
failures.push(
`high-risk policy-sensitive module cannot be counterless: ${highRiskPath}`,
);
}
}
for (const waiver of input.policy.waivers) {
if (!inventorySet.has(waiver.path)) failures.push(`coverage waiver is stale: ${waiver.path}`);
}
const ownedHighRiskPaths = input.policy.highRiskPaths
.filter((modulePath) => criticalPaths.has(modulePath))
.sort();
const waivedHighRiskPaths = input.policy.highRiskPaths
.filter((modulePath) => waiverPaths.has(modulePath))
.sort();
return Object.freeze({
status: failures.length === 0 ? "PASS" : "FAIL",
selectedTotal: inventory.length - uncoveredModules.length,
repositoryTotal: inventory.length,
counterBearingTotal: counterBearingModules.length,
instrumentedCounterBearingTotal,
counterlessTotal: counterlessModules.length,
counterlessModules: Object.freeze([...counterlessModules].sort()),
preExclusionTotal: input.inventory.preExclusionTotal,
generatedExclusionCount: generatedExclusions.length,
generatedExclusions: Object.freeze([...generatedExclusions].sort()),
ownershipScope: "ALL_POLICY_HIGH_RISK",
ownedHighRiskPaths: Object.freeze(ownedHighRiskPaths),
waivedHighRiskPaths: Object.freeze(waivedHighRiskPaths),
uncoveredModules: Object.freeze(uncoveredModules),
results: Object.freeze(results),
failures: Object.freeze(failures),
});
}
+283
View File
@@ -0,0 +1,283 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
import {
buildRepositoryFileInventory,
parseRepositoryFileInventoryPolicy,
} from "./repository-file-inventory.ts";
import {
findSecretMatches,
secretScanRules,
type SecretAllowlistEntry,
type SecretFinding,
} from "./secret-scan.ts";
import {
parseSecretScanIncludedPaths,
selectIncludedInventoryFiles,
} from "./secret-scan-policy.ts";
import { supplyChainDigest } from "./supply-chain.ts";
const nonEmptyString = z.string().min(1);
const sarifRuleSchema = z
.object({
id: nonEmptyString,
shortDescription: z.object({ text: nonEmptyString }).strict(),
})
.strict();
const sarifResultSchema = z
.object({
ruleId: nonEmptyString,
message: z.object({ text: nonEmptyString }).strict(),
partialFingerprints: z
.object({ primaryLocationLineHash: nonEmptyString })
.strict()
.optional(),
locations: z
.array(
z
.object({
physicalLocation: z
.object({
artifactLocation: z.object({ uri: nonEmptyString }).strict(),
region: z.object({ startLine: z.int().positive() }).strict(),
})
.strict(),
})
.strict(),
)
.optional(),
})
.strict();
export const secretScanSarifSchema = z
.object({
version: z.literal("2.1.0"),
$schema: z.literal("https://json.schemastore.org/sarif-2.1.0.json"),
runs: z
.array(
z
.object({
tool: z
.object({
driver: z
.object({
name: z.literal("ca-frontend-secret-scan"),
rules: z.array(sarifRuleSchema),
})
.strict(),
})
.strict(),
results: z.array(sarifResultSchema),
})
.strict(),
)
.length(1),
})
.strict();
type AllowlistEntry = SecretAllowlistEntry &
Readonly<{ owner: string; reason: string }>;
export type SecretScanPolicy = Readonly<{
excludedPaths: readonly string[];
trackedRoots: readonly string[];
generatedRoots: readonly string[];
optionalRoots: readonly string[];
includedPaths: readonly string[] | null;
allowlist: readonly AllowlistEntry[];
}>;
export function parseSecretScanPolicy(value: unknown): SecretScanPolicy {
const document = isRecord(value) ? value : {};
const inventoryPolicy = parseRepositoryFileInventoryPolicy(value);
const allowlist = Array.isArray(document.allowlist)
? document.allowlist.map((rawEntry) => {
const entry = isRecord(rawEntry) ? rawEntry : {};
return Object.freeze({
path: typeof entry.path === "string" ? entry.path : "",
ruleId: typeof entry.ruleId === "string" ? entry.ruleId : "",
owner: typeof entry.owner === "string" ? entry.owner : "",
reason: typeof entry.reason === "string" ? entry.reason : "",
expiresAt:
typeof entry.expiresAt === "string" ? entry.expiresAt : "",
});
})
: [];
return Object.freeze({
excludedPaths: Object.freeze(strings(document.excludedPaths)),
trackedRoots: inventoryPolicy.trackedRoots,
generatedRoots: inventoryPolicy.generatedRoots,
optionalRoots: inventoryPolicy.optionalRoots,
includedPaths: parseSecretScanIncludedPaths(document.includedPaths),
allowlist: Object.freeze(allowlist),
});
}
export async function evaluateRepositorySecretScan(input: Readonly<{
repositoryRoot?: string;
policyPath?: string;
now?: number;
}>) {
const repositoryRoot = path.resolve(input.repositoryRoot ?? process.cwd());
const policy = parseSecretScanPolicy(
JSON.parse(
await readFile(
path.resolve(
repositoryRoot,
input.policyPath ?? "config/security/secret-scan-policy.json",
),
"utf8",
),
) as unknown,
);
const inventory = await buildRepositoryFileInventory({
repositoryRoot,
trackedRoots: policy.trackedRoots,
generatedRoots: policy.generatedRoots,
optionalRoots: policy.optionalRoots,
});
return evaluateSecretScan({
policy,
inventoryFiles: inventory.files,
readText: (file) => readFile(path.join(repositoryRoot, file), "utf8"),
now: input.now,
});
}
export async function evaluateSecretScan(input: Readonly<{
policy: SecretScanPolicy;
inventoryFiles: readonly string[];
readText: (file: string) => Promise<string>;
now?: number;
}>) {
const now = input.now ?? Date.now();
const findings: SecretFinding[] = [];
const policyFailures: string[] = [];
for (const entry of input.policy.allowlist) {
const expiry = Date.parse(entry.expiresAt);
if (
!entry.path.startsWith("tests/") ||
!entry.owner.trim() ||
!entry.reason.trim() ||
!Number.isFinite(expiry) ||
expiry <= now
) {
policyFailures.push(
`invalid or expired secret allowlist entry: ${entry.path}:${entry.ruleId}`,
);
}
}
const scanFiles = selectIncludedInventoryFiles(
input.inventoryFiles,
input.policy.includedPaths,
);
const excluded = new Set(
input.policy.excludedPaths.map((entry) => entry.replaceAll("\\", "/")),
);
const scanInputs: Readonly<{ path: string; bytes: number; sha256: string }>[] = [];
for (const scanFile of [...new Set(scanFiles)].sort()) {
const normalized = scanFile.replaceAll("\\", "/");
if (
[...excluded].some(
(entry) =>
normalized === entry || normalized.startsWith(`${entry}/`),
) ||
/\.(?:png|jpe?g|gif|webp|woff2?|zip|gz|sarif)$/iu.test(normalized)
) {
continue;
}
const content = await input.readText(scanFile);
const bytes = Buffer.from(content, "utf8");
scanInputs.push(Object.freeze({
path: normalized,
bytes: bytes.byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
}));
findings.push(
...findSecretMatches(normalized, content, {
allowlist: input.policy.allowlist,
now,
}),
);
}
const sarif = secretScanSarifSchema.parse({
version: "2.1.0",
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
runs: [
{
tool: {
driver: {
name: "ca-frontend-secret-scan",
rules: secretScanRules().map((pattern) => ({
id: pattern.id,
shortDescription: { text: "Potential credential material" },
})),
},
},
results: [
...findings.map((finding) => ({
ruleId: finding.ruleId,
message: { text: "Potential secret material must be removed." },
partialFingerprints: {
primaryLocationLineHash: finding.fingerprint,
},
locations: [
{
physicalLocation: {
artifactLocation: { uri: finding.file },
region: { startLine: finding.line },
},
},
],
})),
...policyFailures.map((failure) => ({
ruleId: "invalid-allowlist",
message: { text: failure },
})),
],
},
],
});
return Object.freeze({
findings: Object.freeze(findings),
policyFailures: Object.freeze(policyFailures),
scanFiles: Object.freeze([...scanFiles]),
scanInputs: Object.freeze(scanInputs),
scanInputSha256: supplyChainDigest(scanInputs),
sarif,
});
}
export function verifyStoredSecretScan(
evaluation: Awaited<ReturnType<typeof evaluateSecretScan>>,
stored: unknown,
): string[] {
const failures: string[] = [];
const blockingCount =
evaluation.findings.length + evaluation.policyFailures.length;
if (blockingCount > 0) {
failures.push(
`recomputed secret scan contains ${blockingCount} blocking result(s)`,
);
}
const parsed = secretScanSarifSchema.safeParse(stored);
if (
!parsed.success ||
supplyChainDigest(parsed.data) !== supplyChainDigest(evaluation.sarif)
) {
failures.push("stored secret scan SARIF does not match recomputed results");
}
return failures;
}
function strings(value: unknown): string[] {
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === "string")
: [];
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
+59
View File
@@ -0,0 +1,59 @@
import { normalizeRepositoryRelativePath } from "./repository-file-inventory.ts";
export function parseSecretScanIncludedPaths(
value: unknown,
): readonly string[] | null {
if (value === undefined) return null;
if (
!Array.isArray(value) ||
value.length === 0 ||
value.some(
(entry) =>
typeof entry !== "string" ||
entry.length === 0 ||
entry.trim() !== entry,
)
) {
throw new TypeError(
"includedPaths must be a non-empty array of repository-relative POSIX paths",
);
}
const normalized = value.map((entry) =>
normalizeRepositoryRelativePath(entry as string, "included path"),
);
if (new Set(normalized).size !== normalized.length) {
throw new TypeError("includedPaths must not contain duplicate paths");
}
return Object.freeze(normalized);
}
export function selectIncludedInventoryFiles(
inventoryFiles: readonly string[],
includedPaths: readonly string[] | null,
): readonly string[] {
if (includedPaths === null) return Object.freeze([...inventoryFiles]);
const validatedIncludedPaths = parseSecretScanIncludedPaths(includedPaths);
if (validatedIncludedPaths === null) {
throw new TypeError("includedPaths unexpectedly omitted");
}
for (const includedPath of validatedIncludedPaths) {
if (
!inventoryFiles.some(
(file) =>
file === includedPath || file.startsWith(`${includedPath}/`),
)
) {
throw new Error(
`secret scan included path matches no inventory file: ${includedPath}`,
);
}
}
return Object.freeze(
inventoryFiles.filter((file) =>
validatedIncludedPaths.some(
(includedPath) =>
file === includedPath || file.startsWith(`${includedPath}/`),
),
),
);
}
+73
View File
@@ -0,0 +1,73 @@
import { createHash } from "node:crypto";
export type SecretFinding = Readonly<{
ruleId: string;
file: string;
line: number;
fingerprint: string;
}>;
export type SecretAllowlistEntry = Readonly<{
path: string;
ruleId: string;
expiresAt: string;
}>;
const secretPatterns: readonly Readonly<{
id: string;
expression: RegExp;
}>[] = [
{
id: "private-key",
expression: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g,
},
{ id: "aws-access-key", expression: /\bAKIA[0-9A-Z]{16}\b/g },
{ id: "github-token", expression: /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g },
{
id: "assigned-secret",
expression:
/(?<![\w])(["']?)(?:client_secret|password|private_key)\1(?![\w])\s*[:=]\s*["'][^"'${}]{12,}["']/gi,
},
];
export function secretScanRules(): readonly Readonly<{
id: string;
expression: RegExp;
}>[] {
return secretPatterns;
}
export function findSecretMatches(
file: string,
content: string,
options: Readonly<{
allowlist?: readonly SecretAllowlistEntry[];
now?: number;
}> = {},
): SecretFinding[] {
const allowlist = options.allowlist ?? [];
const now = options.now ?? Date.now();
const findings: SecretFinding[] = [];
for (const pattern of secretPatterns) {
pattern.expression.lastIndex = 0;
for (const match of content.matchAll(pattern.expression)) {
const isAllowed = allowlist.some(
(entry) =>
entry.path === file &&
entry.ruleId === pattern.id &&
Date.parse(entry.expiresAt) > now,
);
if (isAllowed) continue;
const matchIndex = match.index ?? 0;
findings.push({
ruleId: pattern.id,
file,
line: content.slice(0, matchIndex).split(/\r?\n/u).length,
fingerprint: createHash("sha256")
.update(`${pattern.id}:${file}:${String(matchIndex)}`)
.digest("hex"),
});
}
}
return findings;
}
+170
View File
@@ -0,0 +1,170 @@
import { spawnSync } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
type ScanResult = Readonly<{
error?: Error;
status: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
}>;
type SecurityFixtureCheckDependencies = Readonly<{
createTempDirectory?: () => Promise<string>;
runScan?: (artifactPath: string, policyPath: string) => ScanResult;
readArtifact?: (artifactPath: string) => Promise<string>;
cleanup?: (directory: string) => Promise<void>;
}>;
type Document = Record<string, unknown>;
function record(value: unknown, label: string): Document {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(`${label} must be an object`);
}
return value as Document;
}
function defaultScan(artifactPath: string, policyPath: string): ScanResult {
const scan = spawnSync(
"node",
[
"scripts/security-scan.ts",
"--policy",
policyPath,
"--artifact",
artifactPath,
],
{ encoding: "utf8" },
);
return {
...(scan.error ? { error: scan.error } : {}),
status: scan.status,
signal: scan.signal,
stdout: scan.stdout ?? "",
stderr: scan.stderr ?? "",
};
}
function assertExactFindings(rawArtifact: string): void {
const sarif = record(JSON.parse(rawArtifact), "security fixture SARIF");
const runs = Array.isArray(sarif.runs) ? sarif.runs : [];
const run = record(runs[0], "security fixture SARIF run");
const results = Array.isArray(run.results) ? run.results : [];
const actual = results
.map((rawResult) => {
const result = record(rawResult, "security fixture result");
const locations = Array.isArray(result.locations) ? result.locations : [];
const location = record(locations[0], "security fixture location");
const physical = record(
location.physicalLocation,
"security fixture physical location",
);
const artifactLocation = record(
physical.artifactLocation,
"security fixture artifact location",
);
return `${String(artifactLocation.uri)}:${String(result.ruleId)}`;
})
.sort();
const root = "tests/fixtures/security/secret-detection/forbidden";
const expected = [
`${root}/config.json:assigned-secret`,
`${root}/dist.ts:assigned-secret`,
`${root}/source.ts:aws-access-key`,
].sort();
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(
`security fixture findings mismatch: expected ${expected.join(", ")}; received ${actual.join(", ")}`,
);
}
}
export async function checkSecurityFixtures(
dependencies: SecurityFixtureCheckDependencies = {},
): Promise<void> {
const createTempDirectory =
dependencies.createTempDirectory ??
(() => mkdtemp(path.join(tmpdir(), "ca-security-fixture-")));
const runScan = dependencies.runScan ?? defaultScan;
const readArtifact = dependencies.readArtifact ?? ((target) => readFile(target, "utf8"));
const cleanup =
dependencies.cleanup ??
((directory) => rm(directory, { recursive: true, force: true }));
const directory = await createTempDirectory();
const artifactPath = path.join(directory, "scan-fixture.sarif");
try {
const scan = runScan(
artifactPath,
"tests/fixtures/security/secret-detection/forbidden-policy.json",
);
const expectedDiagnostic = "Security scan found 3 blocking result(s).";
if (
scan.error ||
scan.status !== 1 ||
scan.signal !== null ||
scan.stderr !== `${expectedDiagnostic}\n`
) {
throw new Error(
`forbidden security fixture did not fail exactly: ${scan.error?.message ?? scan.stderr}`,
);
}
assertExactFindings(await readArtifact(artifactPath));
} finally {
await cleanup(directory);
}
}
export async function checkNonmatchingSecurityIncludeFixture(
dependencies: SecurityFixtureCheckDependencies = {},
): Promise<void> {
const createTempDirectory =
dependencies.createTempDirectory ??
(() => mkdtemp(path.join(tmpdir(), "ca-security-include-fixture-")));
const runScan = dependencies.runScan ?? defaultScan;
const readArtifact =
dependencies.readArtifact ?? ((target) => readFile(target, "utf8"));
const cleanup =
dependencies.cleanup ??
((directory) => rm(directory, { recursive: true, force: true }));
const directory = await createTempDirectory();
const artifactPath = path.join(directory, "scan-fixture.sarif");
try {
const includedPath =
"tests/fixtures/security/secret-detection/misspelled";
const scan = runScan(
artifactPath,
"tests/fixtures/security/secret-detection/nonmatching-policy.json",
);
if (
scan.error ||
scan.status !== 1 ||
scan.signal !== null ||
!scan.stderr.includes(
`secret scan included path matches no inventory file: ${includedPath}`,
)
) {
throw new Error(
`nonmatching security include fixture did not fail closed: ${scan.error?.message ?? scan.stderr}`,
);
}
try {
await readArtifact(artifactPath);
} catch (error) {
if (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === "ENOENT"
) {
return;
}
throw error;
}
throw new Error("nonmatching security include fixture wrote an artifact");
} finally {
await cleanup(directory);
}
}
+101
View File
@@ -0,0 +1,101 @@
import type {
InstalledServiceWorkerSelection,
ServiceWorkerHandlerId,
StaticAssetManifestV1,
} from "../../src/contracts/service-worker.ts";
const DIGEST = /^sha256:[0-9a-f]{64}$/u;
export type ServiceWorkerBuildInput = Readonly<{
assets: StaticAssetManifestV1;
handlers: readonly ServiceWorkerHandlerId[];
contractSetDigest: string;
releaseManifestUrl: string;
}>;
/**
* ACTIVE worker compilation is a release-integrity boundary. Missing generated
* modules, stale identities and placeholder digests are fatal build defects;
* they must never be converted into a worker that merely degrades at runtime.
*/
export function resolveServiceWorkerBuildInput(input: Readonly<{
selection: InstalledServiceWorkerSelection | null;
assets: unknown;
contractSet: unknown;
runtimeConfig: unknown;
buildId: string;
releaseId: string;
}>): ServiceWorkerBuildInput {
if (input.selection?.mode !== "ACTIVE") {
throw new TypeError(
"Service Worker build requires an ACTIVE static selection.",
);
}
if (!Array.isArray(input.selection.handlers)) {
throw new TypeError("Service Worker handlers must be an array.");
}
const handlers = new Set<ServiceWorkerHandlerId>();
for (const handler of input.selection.handlers) {
if (
handler !== "PWA_STATIC_ASSETS" &&
handler !== "OFFLINE_SYNC_WAKEUP" &&
handler !== "WEB_PUSH"
) {
throw new TypeError(`Unknown Service Worker handler: ${String(handler)}.`);
}
if (handlers.has(handler)) {
throw new TypeError(`Duplicate Service Worker handler: ${handler}.`);
}
handlers.add(handler);
}
if (handlers.has("WEB_PUSH")) {
throw new TypeError(
"WEB_PUSH requires an installed product-owned worker contribution.",
);
}
const assets = parseAssets(input.assets);
if (assets.buildId !== input.buildId || assets.releaseId !== input.releaseId) {
throw new TypeError("Generated Service Worker asset identity is stale.");
}
const contractSet = record(input.contractSet);
const contractSetDigest = contractSet?.setDigest;
if (typeof contractSetDigest !== "string" || !DIGEST.test(contractSetDigest)) {
throw new TypeError("Generated contract set digest is invalid.");
}
const runtimeConfig = record(input.runtimeConfig);
const releaseManifestUrl = runtimeConfig?.RELEASE_MANIFEST_URL;
if (
typeof releaseManifestUrl !== "string" ||
releaseManifestUrl.length === 0 ||
releaseManifestUrl.length > 2_048
) {
throw new TypeError("Runtime release manifest URL is invalid.");
}
return Object.freeze({
assets,
handlers: Object.freeze([...handlers]),
contractSetDigest,
releaseManifestUrl,
});
}
function parseAssets(value: unknown): StaticAssetManifestV1 {
const candidate = record(value);
if (
candidate?.schemaVersion !== 1 ||
typeof candidate.buildId !== "string" ||
typeof candidate.releaseId !== "string" ||
typeof candidate.setDigest !== "string" ||
!DIGEST.test(candidate.setDigest) ||
!Array.isArray(candidate.assets)
) {
throw new TypeError("Generated Service Worker asset manifest is invalid.");
}
return candidate as unknown as StaticAssetManifestV1;
}
function record(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
@@ -0,0 +1,94 @@
import { appendFile } from "node:fs/promises";
import {
cleanupFinalizedPromotion,
finalizeVerifiedPromotion,
} from "./promotion-stager.ts";
export async function runStageVerifiedPromotionCli(
environment: NodeJS.ProcessEnv,
dependencies: Readonly<{
cwd?: () => string;
finalize?: typeof finalizeVerifiedPromotion;
cleanup?: typeof cleanupFinalizedPromotion;
appendOutput?: (path: string, content: string) => Promise<void>;
writeStdout?: (content: string) => void;
}> = {},
): Promise<void> {
const required = (name: string): string => {
const value = environment[name];
if (!value) throw new TypeError(`promotion staging environment is missing ${name}`);
return value;
};
const attempt = Number(
environment.GITEA_RUN_ATTEMPT ??
environment.GITHUB_RUN_ATTEMPT ??
required("CI_RUN_ATTEMPT"),
);
if (!Number.isInteger(attempt) || attempt < 1 || attempt > 1_000) {
throw new TypeError("promotion staging run attempt is invalid");
}
const runnerTempRoot = required("RUNNER_TEMP");
const staged = await (dependencies.finalize ?? finalizeVerifiedPromotion)({
repositoryRoot: (dependencies.cwd ?? process.cwd)(),
archivePath: required("CANDIDATE_ARCHIVE_PATH"),
expectedArchiveSha256: required("CANDIDATE_ARCHIVE_SHA256"),
vulnerabilityReportPath: required("VULNERABILITY_REPORT_PATH"),
provenanceAttestationPath: required("PROVENANCE_ATTESTATION_PATH"),
vulnerabilityPublicKeyPath: required("VULNERABILITY_PUBLIC_KEY_PATH"),
vulnerabilityKeyId: required("VULNERABILITY_KEY_ID"),
provenancePublicKeyPath: required("PROVENANCE_PUBLIC_KEY_PATH"),
provenanceKeyId: required("PROVENANCE_KEY_ID"),
expectedRun: {
id:
environment.GITEA_RUN_ID ??
environment.GITHUB_RUN_ID ??
required("CI_RUN_ID"),
attempt,
sourceRevision:
environment.EXPECTED_SOURCE_REVISION ?? required("VITE_COMMIT_SHA"),
},
vulnerabilityInvocationNonce: required("VULNERABILITY_INVOCATION_NONCE"),
provenanceInvocationNonce: required("PROVENANCE_INVOCATION_NONCE"),
runnerTempRoot,
});
try {
const output = required("GITHUB_OUTPUT");
const content = [
`staging_root=${staged.stagingRoot}`,
`cleanup_token=${staged.cleanupToken}`,
`runner_temp_dev=${staged.runnerTempIdentity.dev}`,
`runner_temp_ino=${staged.runnerTempIdentity.ino}`,
`staging_dev=${staged.stagingIdentity.dev}`,
`staging_ino=${staged.stagingIdentity.ino}`,
"",
].join("\n");
await (dependencies.appendOutput ?? defaultAppendOutput)(output, content);
} catch (error) {
try {
await (dependencies.cleanup ?? cleanupFinalizedPromotion)({
runnerTempRoot,
stagingRoot: staged.stagingRoot,
cleanupToken: staged.cleanupToken,
runnerTempIdentity: staged.runnerTempIdentity,
stagingIdentity: staged.stagingIdentity,
});
} catch (cleanupError) {
throw new AggregateError(
[error, cleanupError],
"promotion output publication and direct staging cleanup both failed",
{ cause: cleanupError },
);
}
throw error;
}
(dependencies.writeStdout ?? process.stdout.write.bind(process.stdout))(
`Promotion staging: ${staged.files
.map(({ name, sha256 }) => `${name}=${sha256}`)
.join(", ")} PASS\n`,
);
}
async function defaultAppendOutput(path: string, content: string): Promise<void> {
await appendFile(path, content, { encoding: "utf8" });
}
+24
View File
@@ -0,0 +1,24 @@
export function deterministicSupplyChainGeneratedAt(input: Readonly<{
generatedAt: string;
sourceDateEpoch: string | null;
}>): string {
const generatedAtMs = Date.parse(input.generatedAt);
if (!Number.isFinite(generatedAtMs)) {
throw new TypeError("build manifest generatedAt must be an ISO timestamp");
}
if (input.sourceDateEpoch !== null) {
if (!/^(?:0|[1-9]\d*)$/u.test(input.sourceDateEpoch)) {
throw new TypeError("SOURCE_DATE_EPOCH must be whole seconds");
}
const epoch = Number(input.sourceDateEpoch);
if (
!Number.isSafeInteger(epoch) ||
new Date(epoch * 1000).toISOString() !== input.generatedAt
) {
throw new TypeError(
"build manifest generatedAt must match SOURCE_DATE_EPOCH",
);
}
}
return new Date(generatedAtMs).toISOString();
}
+514
View File
@@ -0,0 +1,514 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
export type DependencyScope = "production" | "development";
export type LockfilePackage = Readonly<{
name: string;
version: string;
integrity: string;
}>;
export type DependencyInventoryRow = Readonly<{
name: string;
version: string;
direct: boolean;
scope: DependencyScope;
optional: boolean;
license: string;
dependencies: readonly string[];
}>;
export type DependencyUpgrade = Readonly<{
name: string;
from: string;
to: string;
}>;
export type DependencyInventoryDiff = Readonly<{
added: readonly string[];
removed: readonly string[];
changed: readonly string[];
upgrades: readonly DependencyUpgrade[];
}>;
type MutableDependencyRecord = {
name: string;
version: string;
direct: boolean;
scope: DependencyScope;
optional: boolean;
packagePath: string;
dependencies: Set<string>;
};
type Document = Readonly<Record<string, unknown>>;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function recordValue(value: unknown): Record<string, unknown> {
return isRecord(value) ? value : {};
}
function recordRows(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter(isRecord) : [];
}
function stringRows(value: unknown): string[] {
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === "string")
: [];
}
function dependencyIdentity(row: Readonly<Record<string, unknown>>): string {
return `${String(row.name ?? "")}@${String(row.version ?? "")}`;
}
export function canonicalizeSupplyChainValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value
.map(canonicalizeSupplyChainValue)
.sort((left, right) =>
String(JSON.stringify(left)).localeCompare(String(JSON.stringify(right))),
);
}
if (isRecord(value)) {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, canonicalizeSupplyChainValue(item)]),
);
}
return value;
}
export function supplyChainDigest(value: unknown): string {
return createHash("sha256")
.update(JSON.stringify(canonicalizeSupplyChainValue(value)))
.digest("hex");
}
export function parsePnpmLockfilePackages(
lockfile: string,
): LockfilePackage[] {
const entries: LockfilePackage[] = [];
let inPackages = false;
let current: { name: string; version: string; integrity: string } | null = null;
for (const line of lockfile.split(/\r?\n/)) {
if (line === "packages:") {
inPackages = true;
continue;
}
if (line === "snapshots:") {
if (current) entries.push(current);
break;
}
if (!inPackages) continue;
const packageMatch = line.match(/^ {2}(\S.*):$/);
if (packageMatch?.[1]) {
if (current) entries.push(current);
const key = packageMatch[1].replace(/^['"]|['"]$/g, "");
const separator = key.lastIndexOf("@");
current = {
name: key.slice(0, separator),
version: key.slice(separator + 1),
integrity: "",
};
continue;
}
const integrityMatch = line.match(/\bintegrity:\s*([^,}\s]+)/);
if (current && integrityMatch?.[1]) {
current.integrity = integrityMatch[1];
}
}
return entries.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
export function isValidSha512Integrity(integrity: string): boolean {
if (!integrity.startsWith("sha512-")) return false;
try {
return Buffer.from(integrity.slice("sha512-".length), "base64").length === 64;
} catch {
return false;
}
}
export function normalizeLicense(raw: unknown): string {
if (typeof raw === "string" && raw.trim()) return raw.trim();
if (isRecord(raw) && typeof raw.type === "string") return raw.type;
if (Array.isArray(raw)) {
const licenses = raw
.map(normalizeLicense)
.filter((license) => license !== "NOASSERTION");
return licenses.length > 0 ? licenses.join(" OR ") : "NOASSERTION";
}
return "NOASSERTION";
}
export async function flattenPnpmDependencyTree(
root: Record<string, unknown>,
directProduction: Readonly<Record<string, string>>,
directDevelopment: Readonly<Record<string, string>>,
): Promise<DependencyInventoryRow[]> {
const records = new Map<string, MutableDependencyRecord>();
const directIds = new Set<string>();
for (const [name, rawDependency] of Object.entries(
recordValue(root.dependencies),
)) {
if (Object.hasOwn(directProduction, name) && isRecord(rawDependency)) {
directIds.add(`${name}@${String(rawDependency.version ?? "")}`);
}
}
for (const [name, rawDependency] of Object.entries(
recordValue(root.devDependencies),
)) {
if (Object.hasOwn(directDevelopment, name) && isRecord(rawDependency)) {
directIds.add(`${name}@${String(rawDependency.version ?? "")}`);
}
}
function visit(
node: Record<string, unknown>,
scope: DependencyScope,
optionalPath: boolean,
): void {
const groups = {
dependencies: node.dependencies,
devDependencies: node.devDependencies,
optionalDependencies: node.optionalDependencies,
};
for (const [groupName, group] of Object.entries(groups)) {
for (const [name, rawDependency] of Object.entries(recordValue(group))) {
if (!isRecord(rawDependency)) continue;
const version = String(rawDependency.version ?? "");
const packagePath = String(rawDependency.path ?? "");
const identity = `${name}@${version}`;
const childScope: DependencyScope =
scope === "production" && groupName !== "devDependencies"
? "production"
: "development";
const childOptional =
optionalPath || groupName === "optionalDependencies";
const previous = records.get(identity);
const dependencies = previous?.dependencies ?? new Set<string>();
for (const childGroup of [
rawDependency.dependencies,
rawDependency.optionalDependencies,
]) {
for (const [childName, rawChild] of Object.entries(
recordValue(childGroup),
)) {
if (isRecord(rawChild)) {
dependencies.add(
`${childName}@${String(rawChild.version ?? "")}`,
);
}
}
}
records.set(identity, {
name,
version,
direct: directIds.has(identity),
scope:
previous?.scope === "production" || childScope === "production"
? "production"
: "development",
optional: previous
? previous.optional && childOptional
: childOptional,
packagePath: previous?.packagePath || packagePath,
dependencies,
});
visit(rawDependency, childScope, childOptional);
}
}
}
const productionRoot: Record<string, unknown> = {
dependencies: Object.fromEntries(
Object.entries(recordValue(root.dependencies)).filter(([name]) =>
Object.hasOwn(directProduction, name),
),
),
};
const developmentRoot: Record<string, unknown> = {
devDependencies: Object.fromEntries(
Object.entries(recordValue(root.devDependencies)).filter(([name]) =>
Object.hasOwn(directDevelopment, name),
),
),
};
visit(productionRoot, "production", false);
visit(developmentRoot, "development", false);
const result: DependencyInventoryRow[] = [];
for (const record of records.values()) {
let license = "NOASSERTION";
let optional = record.optional;
if (record.packagePath) {
try {
const parsed: unknown = JSON.parse(
await readFile(`${record.packagePath}/package.json`, "utf8"),
);
const manifest = recordValue(parsed);
license = normalizeLicense(manifest.license ?? manifest.licenses);
} catch {
// Platform-specific optional packages may not be materialized locally.
optional = true;
}
}
result.push({
name: record.name,
version: record.version,
direct: record.direct,
scope: record.scope,
optional,
license,
dependencies: [...record.dependencies].sort(),
});
}
return result.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
export function diffDependencyInventories(
before: Document,
after: Document,
): DependencyInventoryDiff {
const beforeRows = recordRows(before.dependencies);
const afterRows = recordRows(after.dependencies);
const beforeMap = new Map(
beforeRows.map((row) => [dependencyIdentity(row), row] as const),
);
const afterMap = new Map(
afterRows.map((row) => [dependencyIdentity(row), row] as const),
);
const added = [...afterMap.keys()].filter((key) => !beforeMap.has(key));
const removed = [...beforeMap.keys()].filter((key) => !afterMap.has(key));
const changed: string[] = [];
for (const key of [...beforeMap.keys()].filter((item) => afterMap.has(item))) {
if (supplyChainDigest(beforeMap.get(key)) !== supplyChainDigest(afterMap.get(key))) {
changed.push(key);
}
}
const upgrades: DependencyUpgrade[] = [];
for (const removedKey of removed) {
const previous = beforeMap.get(removedKey);
if (!previous) continue;
const replacement = added.find(
(addedKey) => afterMap.get(addedKey)?.name === previous.name,
);
const next = replacement ? afterMap.get(replacement) : undefined;
if (next) {
upgrades.push({
name: String(previous.name ?? ""),
from: String(previous.version ?? ""),
to: String(next.version ?? ""),
});
}
}
return Object.freeze({
added: Object.freeze(added.sort()),
removed: Object.freeze(removed.sort()),
changed: Object.freeze(changed.sort()),
upgrades: Object.freeze(
upgrades.sort((left, right) => left.name.localeCompare(right.name)),
),
});
}
export function validateLicensePolicy(
inventory: Document,
policy: Document,
) {
const allowed = new Set(stringRows(policy.allowedLicenses));
const denied = stringRows(policy.deniedLicensePatterns);
const failures: string[] = [];
const results: Array<Readonly<{
package: string;
license: string;
passed: boolean;
reason: string | null;
}>> = [];
for (const dependency of recordRows(inventory.dependencies)) {
const license = String(dependency.license ?? "NOASSERTION");
const explicitlyDenied = denied.some((pattern) =>
new RegExp(pattern, "i").test(license),
);
const unknownAccepted =
license === "NOASSERTION" && dependency.optional === true;
const passed =
!explicitlyDenied && (allowed.has(license) || unknownAccepted);
results.push({
package: dependencyIdentity(dependency),
license,
passed,
reason: unknownAccepted ? "platform-optional-not-materialized" : null,
});
if (!passed) {
failures.push(
`${dependencyIdentity(dependency)} has disallowed license ${license}`,
);
}
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
results: Object.freeze(results),
});
}
export function validateDependencyReview(
diff: DependencyInventoryDiff,
inventory: Document,
evidenceFile: Document,
) {
const byIdentity = new Map(
recordRows(inventory.dependencies).map(
(row) => [dependencyIdentity(row), row] as const,
),
);
const evidence = new Map<string, Record<string, unknown>>();
for (const entry of recordRows(evidenceFile.changes)) {
if (typeof entry.changeId === "string") evidence.set(entry.changeId, entry);
}
const highRisk = diff.added.filter((identity) => {
const row = byIdentity.get(identity);
return row?.direct === true && row.scope === "production";
});
const failures: string[] = [];
for (const identity of highRisk) {
const changeId = `add:${identity}`;
const entry = evidence.get(changeId);
if (!entry) {
failures.push(`high-risk dependency missing review: ${changeId}`);
continue;
}
for (const field of ["owner", "reviewer", "reason", "rollback"] as const) {
const value = entry[field];
if (typeof value !== "string" || !value.trim()) {
failures.push(`${changeId} missing ${field}`);
}
}
if (entry.owner === entry.reviewer) {
failures.push(`${changeId} may not be self-approved`);
}
}
return Object.freeze({
passed: failures.length === 0,
highRisk: Object.freeze(highRisk),
failures: Object.freeze(failures),
});
}
const severityRank: ReadonlyMap<string, number> = new Map([
["unknown", 0],
["low", 1],
["moderate", 2],
["high", 3],
["critical", 4],
]);
export function validateVulnerabilityReport(
report: Document,
policy: Document,
exceptionFile: Document,
lockfileSha256: string,
now: Date = new Date(),
) {
const failures: string[] = [];
if (report.scannedLockfileSha256 !== lockfileSha256) {
failures.push("vulnerability report lockfile digest mismatch");
}
if (typeof report.provider !== "string" || !report.provider.trim()) {
failures.push("vulnerability report provider missing");
}
const threshold = severityRank.get(String(policy.blockAtSeverity)) ?? 3;
const exceptions = recordRows(exceptionFile.exceptions);
const blocking: string[] = [];
for (const finding of recordRows(report.findings)) {
const severity = String(finding.severity ?? "unknown").toLowerCase();
if ((severityRank.get(severity) ?? 0) < threshold) continue;
const exception = exceptions.find(
(entry) =>
entry.vulnerabilityId === finding.id &&
entry.packageName === finding.packageName,
);
const expiry =
typeof exception?.expiresAt === "string"
? Date.parse(exception.expiresAt)
: Number.NaN;
const validException = Boolean(
exception &&
typeof exception.owner === "string" &&
exception.owner.trim() &&
typeof exception.reviewer === "string" &&
exception.reviewer.trim() &&
exception.owner !== exception.reviewer &&
typeof exception.reason === "string" &&
exception.reason.trim() &&
Number.isFinite(expiry) &&
expiry > now.getTime(),
);
if (!validException) {
blocking.push(
`${String(finding.id)}:${String(finding.packageName)}@${String(finding.version)}:${severity}`,
);
}
}
return Object.freeze({
passed: failures.length === 0 && blocking.length === 0,
failures: Object.freeze(failures),
blocking: Object.freeze(blocking),
});
}
export function verifySupplyChainCoherence(
sbom: Document,
inventory: Document,
provenance: Document,
distDigest: string,
) {
const failures: string[] = [];
const componentCount = Array.isArray(sbom.components)
? sbom.components.length
: -1;
const dependencyCount = Array.isArray(inventory.dependencies)
? inventory.dependencies.length
: -2;
if (componentCount !== dependencyCount) {
failures.push("SBOM component count does not match inventory");
}
const metadata = recordValue(sbom.metadata);
const properties = recordRows(metadata.properties);
if (
properties.find(
(property) =>
property.name === "ca:lockfileSha256" &&
property.value === inventory.lockfileSha256,
) === undefined
) {
failures.push("SBOM lockfile digest does not match inventory");
}
const subject = recordRows(provenance.subject)[0];
const subjectDigest = recordValue(subject?.digest);
if (subjectDigest.sha256 !== distDigest) {
failures.push("provenance subject does not match built dist digest");
}
const predicate = recordValue(provenance.predicate);
const materials = recordValue(predicate.materials);
if (materials.lockfileSha256 !== inventory.lockfileSha256) {
failures.push("provenance lockfile material does not match inventory");
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
});
}
+36
View File
@@ -0,0 +1,36 @@
import { z } from "zod";
export const testEvidenceReportSchema = z
.object({
schemaVersion: z.literal(2),
sourceRoot: z.string().min(1),
status: z.enum(["PASS", "FAIL"]),
facts: z
.object({
scannedFiles: z.number().int().nonnegative(),
visualBaselines: z.number().int().nonnegative(),
sharedScenarios: z.number().int().nonnegative(),
declaredScenarioExecutions: z.number().int().nonnegative(),
executedScenarioExecutions: z.number().int().nonnegative(),
})
.strict(),
failures: z.array(z.string()),
})
.strict()
.superRefine((report, context) => {
const passed = report.status === "PASS";
if (passed !== (report.failures.length === 0)) {
context.addIssue({ code: "custom", path: ["status"], message: "status must agree with failures" });
}
if (
passed &&
report.facts.declaredScenarioExecutions !==
report.facts.executedScenarioExecutions
) {
context.addIssue({
code: "custom",
path: ["facts", "executedScenarioExecutions"],
message: "PASS requires exact declared/executed scenario agreement",
});
}
});
@@ -0,0 +1,294 @@
import { execFile } from "node:child_process";
import {
cp,
mkdtemp,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const coverageMetrics = [
"lines",
"statements",
"functions",
"branches",
] as const;
const runtimeModule = "src/runtime.ts";
const counterlessModules = Object.freeze([
"src/import-empty.ts",
"src/import-side-effect.ts",
"src/import-type-empty.ts",
"src/import-value.ts",
"src/reexport-named.ts",
"src/reexport-star.ts",
"src/type-only.ts",
] as const);
const expectedModules = Object.freeze(
[runtimeModule, ...counterlessModules].sort(),
);
const childOutputLimit = 1_800;
type ChildOutput = Readonly<{ stdout: string; stderr: string }>;
type ChildInput = Readonly<{
repositoryRoot: string;
ownedRoot: string;
configPath: string;
}>;
type CheckerOptions = Readonly<{
repositoryRoot?: string;
createOwnedRoot?: () => Promise<string>;
runVitest?: (input: ChildInput) => Promise<ChildOutput>;
}>;
export type V8CoverageCounterSemantics = Readonly<{
counterBearingModules: readonly string[];
counterlessModules: readonly string[];
}>;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function hasErrorCode(error: unknown, code: string): boolean {
return isRecord(error) && error.code === code;
}
function normalizeSummaryPath(value: string, fixtureRoot: string): string {
const relative = path.isAbsolute(value) ? path.relative(fixtureRoot, value) : value;
const normalized = relative.split(path.sep).join("/");
if (
normalized.length === 0 ||
normalized === ".." ||
normalized.startsWith("../") ||
path.posix.isAbsolute(normalized)
) {
throw new TypeError(`V8 coverage row is outside the owned fixture: ${value}`);
}
return normalized;
}
function coverageCounter(
value: unknown,
label: string,
): Readonly<{ total: number; covered: number; skipped: number; pct: number }> {
if (!isRecord(value)) throw new TypeError(`${label} must be an object`);
const { total, covered, skipped, pct } = value;
if (
typeof total !== "number" ||
!Number.isSafeInteger(total) ||
typeof covered !== "number" ||
!Number.isSafeInteger(covered) ||
typeof skipped !== "number" ||
!Number.isSafeInteger(skipped) ||
typeof pct !== "number" ||
!Number.isFinite(pct) ||
total < 0 ||
covered < 0 ||
skipped < 0 ||
covered + skipped > total
) {
throw new TypeError(`${label} contains invalid coverage counters`);
}
const expectedPercentage =
total === 0 ? 100 : Math.floor((covered / total) * 10_000) / 100;
if (pct !== expectedPercentage) {
throw new TypeError(`${label} contains invalid coverage counters`);
}
return { total, covered, skipped, pct };
}
function coverageRow(
value: unknown,
label: string,
): Readonly<Record<(typeof coverageMetrics)[number], ReturnType<typeof coverageCounter>>> {
if (!isRecord(value)) throw new TypeError(`${label} must be an object`);
return Object.fromEntries(
coverageMetrics.map((metric) => [
metric,
coverageCounter(value[metric], `${label}.${metric}`),
]),
) as Readonly<
Record<(typeof coverageMetrics)[number], ReturnType<typeof coverageCounter>>
>;
}
export function assertV8CoverageCounterSemantics(
value: unknown,
fixtureRoot: string,
): V8CoverageCounterSemantics {
if (!isRecord(value)) throw new TypeError("V8 coverage summary must be an object");
coverageRow(value.total, "V8 coverage total");
const rows = new Map<string, ReturnType<typeof coverageRow>>();
for (const [producerPath, producerRow] of Object.entries(value)) {
if (producerPath === "total") continue;
const normalizedPath = normalizeSummaryPath(producerPath, fixtureRoot);
if (rows.has(normalizedPath)) {
throw new TypeError(`V8 coverage row is duplicated: ${normalizedPath}`);
}
rows.set(
normalizedPath,
coverageRow(producerRow, `V8 coverage row ${normalizedPath}`),
);
}
const actualModules = [...rows.keys()].sort();
const missing = expectedModules.filter((modulePath) => !rows.has(modulePath));
const additional = actualModules.filter(
(modulePath) => !expectedModules.includes(modulePath),
);
if (missing.length > 0 || additional.length > 0) {
throw new Error(
`V8 coverage row set mismatch; missing: ${missing.join(", ") || "none"}; additional: ${additional.join(", ") || "none"}`,
);
}
const runtimeRow = rows.get(runtimeModule)!;
if (!coverageMetrics.some((metric) => runtimeRow[metric].total > 0)) {
throw new Error(`V8 runtime module is not counter-bearing: ${runtimeModule}`);
}
for (const modulePath of counterlessModules) {
const row = rows.get(modulePath)!;
const exactAllZero = coverageMetrics.every((metric) => {
const counter = row[metric];
return (
counter.total === 0 &&
counter.covered === 0 &&
counter.skipped === 0 &&
counter.pct === 100
);
});
if (!exactAllZero) {
throw new Error(
`V8 counterless module must have exact all-zero counters: ${modulePath}`,
);
}
}
return Object.freeze({
counterBearingModules: Object.freeze([runtimeModule]),
counterlessModules: Object.freeze([...counterlessModules]),
});
}
function boundedChildOutput(value: unknown): string {
const output =
typeof value === "string"
? value
: value instanceof Uint8Array
? new TextDecoder().decode(value)
: "";
if (output.length <= childOutputLimit) return output;
return `${output.slice(0, childOutputLimit)}\n[truncated ${output.length - childOutputLimit} characters]`;
}
async function defaultRunVitest(input: ChildInput): Promise<ChildOutput> {
const result = await execFileAsync(
process.execPath,
[
path.join(input.repositoryRoot, "node_modules/vitest/vitest.mjs"),
"run",
"--config",
input.configPath,
"--coverage",
"--reporter=dot",
"--no-color",
],
{
cwd: input.ownedRoot,
encoding: "utf8",
timeout: 30_000,
maxBuffer: 256 * 1024,
},
);
return { stdout: result.stdout, stderr: result.stderr };
}
function assertOwnedTemporaryRoot(value: string): string {
const temporaryRoot = path.resolve(tmpdir());
const ownedRoot = path.resolve(value);
const relative = path.relative(temporaryRoot, ownedRoot);
if (
relative.length === 0 ||
relative === ".." ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
throw new TypeError(`V8 coverage fixture root is not an owned temp path: ${ownedRoot}`);
}
return ownedRoot;
}
export async function checkV8CoverageCounterSemantics(
options: CheckerOptions = {},
): Promise<V8CoverageCounterSemantics> {
const repositoryRoot = path.resolve(options.repositoryRoot ?? process.cwd());
const createOwnedRoot =
options.createOwnedRoot ??
(() => mkdtemp(path.join(tmpdir(), "v8-counter-semantics-")));
const runVitest = options.runVitest ?? defaultRunVitest;
const ownedRoot = assertOwnedTemporaryRoot(await createOwnedRoot());
const fixtureSource = path.join(
repositoryRoot,
"tests/fixtures/v8-coverage-counter-semantics",
);
const configPath = path.join(ownedRoot, "vitest.config.mjs");
const reportsDirectory = path.join(ownedRoot, "coverage");
try {
await cp(fixtureSource, ownedRoot, { recursive: true });
await writeFile(
configPath,
`export default ${JSON.stringify(
{
root: ownedRoot,
test: {
globals: true,
include: ["counter-semantics.fixture.ts"],
setupFiles: [],
coverage: {
provider: "v8",
reportsDirectory,
reporter: ["json-summary"],
include: ["src/**/*.ts"],
},
},
},
null,
2,
)};\n`,
"utf8",
);
try {
await runVitest({ repositoryRoot, ownedRoot, configPath });
} catch (error) {
const record = isRecord(error) ? error : {};
throw new Error(
`V8 coverage counter semantics child Vitest failed\nstdout:\n${boundedChildOutput(record.stdout)}\nstderr:\n${boundedChildOutput(record.stderr)}`,
{ cause: error },
);
}
const summaryPath = path.join(reportsDirectory, "coverage-summary.json");
let summaryText: string;
try {
summaryText = await readFile(summaryPath, "utf8");
} catch (error) {
if (hasErrorCode(error, "ENOENT")) {
throw new Error("V8 coverage counter semantics coverage summary is missing", {
cause: error,
});
}
throw error;
}
let summary: unknown;
try {
summary = JSON.parse(summaryText) as unknown;
} catch (error) {
throw new TypeError("V8 coverage counter semantics summary is invalid JSON", {
cause: error,
});
}
return assertV8CoverageCounterSemantics(summary, ownedRoot);
} finally {
await rm(ownedRoot, { recursive: true, force: true });
}
}
+144
View File
@@ -0,0 +1,144 @@
import { randomUUID } from "node:crypto";
import { constants } from "node:fs";
import {
open as openFile,
rename as renameFile,
rm as removeFile,
} from "node:fs/promises";
import path from "node:path";
import type { z } from "zod";
export type ValidatedJsonArtifactInput = Readonly<{
path: string;
schema: z.ZodType;
value: unknown;
}>;
export function serializeValidatedJsonArtifact(
input: ValidatedJsonArtifactInput,
): Buffer {
const parsed = input.schema.parse(input.value);
const serialized = JSON.stringify(parsed, null, 2);
if (serialized === undefined) {
throw new TypeError("Validated JSON artifact is not serializable");
}
return Buffer.from(`${serialized}\n`, "utf8");
}
export type ValidatedJsonArtifactFileSystem = Readonly<{
open: (path: string, flags: number, mode: number) => Promise<{
writeFile(data: string, encoding: "utf8"): Promise<unknown>;
sync(): Promise<unknown>;
close(): Promise<unknown>;
}>;
openDirectory: (path: string) => Promise<{
sync(): Promise<unknown>;
close(): Promise<unknown>;
}>;
rename: (source: string, destination: string) => Promise<unknown>;
rm: (path: string, options: Readonly<{ force: true }>) => Promise<unknown>;
}>;
type ValidatedJsonArtifactWriterDependencies = Readonly<{
createNonce?: () => string;
fileSystem?: ValidatedJsonArtifactFileSystem;
}>;
const defaultFileSystem: ValidatedJsonArtifactFileSystem = Object.freeze({
open: async (target, flags, mode) => openFile(target, flags, mode),
openDirectory: async (target) => openFile(target, constants.O_RDONLY),
rename: async (source, destination) => renameFile(source, destination),
rm: async (target, options) => removeFile(target, options),
});
function hasErrorCode(error: unknown, code: string): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === code
);
}
/**
* Builds a writer whose only publish operation is an atomic sibling rename.
* Dependency injection is limited to the file-system boundary so failure
* ownership can be verified without exposing a caller-selected cleanup path.
*/
export function createValidatedJsonArtifactWriter(
dependencies: ValidatedJsonArtifactWriterDependencies = {},
) {
const createNonce = dependencies.createNonce ?? randomUUID;
const fileSystem = dependencies.fileSystem ?? defaultFileSystem;
return async function writeArtifact(
input: ValidatedJsonArtifactInput,
): Promise<void> {
const serialized = serializeValidatedJsonArtifact(input);
const temporaryPath = path.join(
path.dirname(input.path),
`.${path.basename(input.path)}.${createNonce()}.tmp`,
);
let ownsTemporaryFile = false;
try {
const handle = await fileSystem.open(
temporaryPath,
constants.O_WRONLY |
constants.O_CREAT |
constants.O_EXCL |
constants.O_NOFOLLOW,
0o600,
);
ownsTemporaryFile = true;
let writeFailed = false;
let writeFailure: unknown;
try {
await handle.writeFile(serialized.toString("utf8"), "utf8");
await handle.sync();
} catch (error) {
writeFailed = true;
writeFailure = error;
}
let closeFailed = false;
let closeFailure: unknown;
try {
await handle.close();
} catch (error) {
closeFailed = true;
closeFailure = error;
}
if (writeFailed) throw writeFailure;
if (closeFailed) throw closeFailure;
await fileSystem.rename(temporaryPath, input.path);
ownsTemporaryFile = false;
const directoryHandle = await fileSystem.openDirectory(
path.dirname(input.path),
);
try {
try {
await directoryHandle.sync();
} catch (error) {
if (!hasErrorCode(error, "EINVAL") && !hasErrorCode(error, "ENOTSUP")) {
throw error;
}
}
} finally {
await directoryHandle.close();
}
} catch (error) {
if (ownsTemporaryFile) {
try {
await fileSystem.rm(temporaryPath, { force: true });
} catch {
// Preserve the publishing failure; cleanup is confined to our nonce.
}
}
throw error;
}
};
}
export const writeValidatedJsonArtifact =
createValidatedJsonArtifactWriter();
+63
View File
@@ -0,0 +1,63 @@
import path from "node:path";
import type { Plugin } from "vite";
type ModuleInventoryChunk = Readonly<{
fileName: string;
modules: readonly string[];
}>;
/**
* Rollup knows the exact source-module set for every emitted chunk. Persisting
* that graph makes optional-runtime exclusion verifiable without relying on
* minified names, error strings, or source maps.
*/
export function viteModuleInventoryPlugin(
repositoryRoot = process.cwd(),
): Plugin {
return {
name: "frontend-module-inventory",
generateBundle(_options, bundle) {
const chunks: ModuleInventoryChunk[] = Object.values(bundle)
.filter((output) => output.type === "chunk")
.map((chunk) => ({
fileName: chunk.fileName,
modules: Object.freeze(
[...new Set(
Object.keys(chunk.modules).map((moduleId) =>
normalizeModuleId(moduleId, repositoryRoot),
),
)].sort(),
),
}))
.sort((left, right) => left.fileName.localeCompare(right.fileName));
this.emitFile({
type: "asset",
fileName: ".vite/module-inventory.json",
source: `${JSON.stringify(
{
schemaVersion: 1,
chunks,
},
null,
2,
)}\n`,
});
},
};
}
function normalizeModuleId(
moduleId: string,
repositoryRoot: string,
): string {
const withoutQuery = moduleId.replace(/^\0/u, "").split("?", 1)[0] ?? "";
if (!path.isAbsolute(withoutQuery)) {
return withoutQuery.replaceAll("\\", "/");
}
const relative = path.relative(repositoryRoot, withoutQuery);
return relative.startsWith("..")
? `external:${path.basename(withoutQuery)}`
: relative.replaceAll("\\", "/");
}