feat: 기능 추가 과정중

This commit is contained in:
donghyeon-ka
2026-07-30 15:58:20 +09:00
parent d3ef801fe6
commit 6c52cdb916
648 changed files with 126325 additions and 6680 deletions
+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);
}
@@ -1,27 +1,27 @@
/**
* @typedef {{
* file: string,
* isEntry?: boolean,
* imports?: string[]
* }} ViteManifestEntry
*/
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.
*
* @param {Record<string, ViteManifestEntry>} manifest
*/
export function classifyViteJavascript(manifest) {
const initialFiles = new Set();
const visitedKeys = new Set();
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 = [];
const missingImports: string[] = [];
while (pendingKeys.length > 0) {
const key = /** @type {string} */ (pendingKeys.pop());
const key = pendingKeys.pop();
if (key === undefined) break;
if (visitedKeys.has(key)) continue;
visitedKeys.add(key);
const entry = manifest[key];
@@ -68,15 +68,10 @@ const fieldEvidenceInputSchema = z
}
});
/**
* @param {unknown} input
* @param {string | undefined} configuredMinimum
* @param {Date} [now]
*/
export function validateFieldEvidenceInput(
input,
configuredMinimum,
now = new Date(),
input: unknown,
configuredMinimum: string | undefined,
now: Date = new Date(),
) {
const parsed = fieldEvidenceInputSchema.safeParse(input);
const failures = parsed.success
@@ -4,15 +4,21 @@ const LOOPBACK_IPV4 = /^127(?:\.\d{1,3}){3}$/;
* A release gate must not promote a local preview server as live hosting
* evidence.
*
* @param {string} value
* @returns {
* | { passed: true; reason: null; url: URL; observedOrigin: string }
* | { passed: false; reason: string; url: URL | null; observedOrigin: string | null }
* }
*/
export function classifyLiveHostingBaseUrl(value) {
/** @type {URL} */
let url;
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 {
@@ -1,4 +1,4 @@
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
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),
@@ -15,19 +15,18 @@ const REVIEW_FIELDS = Object.freeze([
"Screen reader",
]);
/** @param {string} content */
export function validateManualA11yEvidence(content) {
export function validateManualA11yEvidence(content: string) {
const fields = Object.fromEntries(
content
.split(/\r?\n/)
.map((line) => /^([^:]+):\s*(.*)$/.exec(line))
.filter(Boolean)
.filter((match): match is RegExpExecArray => match !== null)
.map((match) => [
/** @type {RegExpExecArray} */ (match)[1].trim(),
/** @type {RegExpExecArray} */ (match)[2].trim(),
match[1].trim(),
match[2].trim(),
]),
);
const failures = [];
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");
+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);
}
-265
View File
@@ -1,265 +0,0 @@
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",
]);
const lifecycleRecipes = new Set([
"analytics-error-sink",
"browser-permission",
"client-workflow",
"file-transfer",
"generated-api",
"multi-tab",
"offline-indexeddb",
"realtime",
"service-worker-pwa",
"web-worker",
]);
/** @param {unknown} value */
function nonEmptyStrings(value) {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every((entry) => typeof entry === "string" && entry.trim().length > 0)
);
}
/**
* @param {unknown} input
* @param {Readonly<Record<string, unknown>>} packageDocument
* @returns {string[]}
*/
export function validateRecipeCatalog(input, packageDocument) {
const document =
/** @type {Record<string, any>} */ (
input && typeof input === "object" ? input : {}
);
/** @type {string[]} */
const violations = [];
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 actualIds = document.recipes
.map(/** @param {Record<string, unknown>} recipe */ (recipe) => 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 document.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",
]) {
if (typeof recipe[field] !== "string" || recipe[field].trim().length === 0) {
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
}
}
for (const field of [
"forbiddenWhen",
"failureKinds",
"securityPrivacy",
"removal",
]) {
if (!nonEmptyStrings(recipe[field])) {
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
}
}
if (
!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 = {
.../** @type {Record<string, string>} */ (packageDocument.dependencies ?? {}),
.../** @type {Record<string, string>} */ (
packageDocument.devDependencies ?? {}
),
};
for (const pattern of document.vendorPackagePatterns ?? []) {
const wildcard = String(pattern).endsWith("*");
const prefix = String(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;
}
/** @param {string} directory @returns {Promise<string[]>} */
export async function sourceFiles(directory) {
let entries;
try {
entries = await readdir(directory, { withFileTypes: true });
} catch (error) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "ENOENT"
) {
return [];
}
throw error;
}
const groups = await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory()
? sourceFiles(target)
: /\.(?:js|jsx|mjs|ts|tsx|mts)$/.test(entry.name)
? [target]
: [];
}),
);
return groups.flat();
}
/**
* @param {string} root
* @param {{scanProductionBoundary?: boolean}} [options]
*/
export async function scanOptionalRecipeSources(
root,
{ scanProductionBoundary = true } = {},
) {
/** @type {Array<{ruleId: string; path: string}>} */
const violations = [];
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]);
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 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;
}
/** @param {string} distRoot */
export async function scanProductionBundle(distRoot) {
/** @type {string[]} */
const violations = [];
for (const file of await sourceFiles(distRoot)) {
const content = await readFile(file, "utf8");
if (content.includes("frontend-optional-recipe-must-not-reach-production")) {
violations.push(path.relative(process.cwd(), file));
}
}
return violations;
}
+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)];
}
+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;
}
@@ -5,17 +5,36 @@ export const COMPATIBILITY_IMPACTS = Object.freeze([
"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]),
);
/** @param {unknown} value @returns {unknown} */
export function canonicalizeRegistryValue(value) {
export function canonicalizeRegistryValue(value: unknown): unknown {
if (Array.isArray(value)) {
const projected =
/** @type {unknown[]} */ (value.map(canonicalizeRegistryValue));
const projected: unknown[] = value.map(canonicalizeRegistryValue);
return projected.every(
(item) =>
item === null ||
@@ -36,64 +55,64 @@ export function canonicalizeRegistryValue(value) {
return value;
}
/** @param {unknown} value @returns {string} */
export function canonicalRegistryJson(value) {
export function canonicalRegistryJson(value: unknown): string {
return JSON.stringify(canonicalizeRegistryValue(value)) ?? "undefined";
}
/** @param {unknown} snapshot */
export function registrySnapshotDigest(snapshot) {
export function registrySnapshotDigest(snapshot: unknown): string {
return createHash("sha256")
.update(canonicalRegistryJson(snapshot))
.digest("hex");
}
/** @param {string} current @param {string} candidate */
function strongestImpact(current, candidate) {
function strongestImpact(
current: CompatibilityImpact,
candidate: CompatibilityImpact,
): CompatibilityImpact {
return (impactRank.get(candidate) ?? 0) > (impactRank.get(current) ?? 0)
? candidate
: current;
}
/** @param {unknown} value */
function valueType(value) {
function valueType(value: unknown): string {
if (value === null) return "null";
if (Array.isArray(value)) return "array";
return typeof value;
}
/**
* @param {string} registryId
* @param {string} rowName
* @param {string} field
* @param {string} kind
*/
function changeId(registryId, rowName, field, kind) {
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.
*
* @param {Readonly<Record<string, unknown>>} before
* @param {Readonly<Record<string, unknown>>} after
*/
export function diffRegistrySnapshots(before, after) {
const changes = /** @type {Array<Record<string, unknown>>} */ ([]);
let impact = "none";
const beforeRegistries =
/** @type {Map<string, Record<string, unknown>>} */ (new Map(
/** @type {Array<Record<string, unknown>>} */ (before.registries ?? []).map(
(registry) => [String(registry.registryId), registry],
),
));
const afterRegistries =
/** @type {Map<string, Record<string, unknown>>} */ (new Map(
/** @type {Array<Record<string, unknown>>} */ (after.registries ?? []).map(
(registry) => [String(registry.registryId), registry],
),
));
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(),
@@ -116,10 +135,8 @@ export function diffRegistrySnapshots(before, after) {
continue;
}
const previousContract =
/** @type {Record<string, unknown>} */ (previous.contract ?? {});
const currentContract =
/** @type {Record<string, unknown>} */ (current.contract ?? {});
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),
@@ -156,16 +173,16 @@ export function diffRegistrySnapshots(before, after) {
}
const breakingFields = new Set(
/** @type {string[]} */ (
currentContract.breakingFields ?? []
),
(currentContract.breakingFields ?? []) as string[],
);
const beforeRows =
/** @type {Record<string, Record<string, unknown>>} */ (
previous.rows ?? {}
);
const afterRows =
/** @type {Record<string, Record<string, unknown>>} */ (current.rows ?? {});
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),
@@ -209,8 +226,8 @@ export function diffRegistrySnapshots(before, after) {
) {
continue;
}
let kind;
let changeImpact;
let kind: string;
let changeImpact: CompatibilityImpact;
if (!beforeHas) {
kind = "field-added";
changeImpact = "additive";
@@ -261,11 +278,10 @@ export function diffRegistrySnapshots(before, after) {
});
}
/**
* @param {Readonly<Record<string, unknown>>} snapshot
* @param {Readonly<Record<string, unknown>>} approval
*/
export function verifyRegistryBaselineApproval(snapshot, approval) {
export function verifyRegistryBaselineApproval(
snapshot: Readonly<Record<string, unknown>>,
approval: Readonly<Record<string, unknown>>,
) {
const actualDigest = registrySnapshotDigest(snapshot);
const approvedDigest = approval.snapshotDigest;
return Object.freeze({
@@ -281,17 +297,15 @@ export function verifyRegistryBaselineApproval(snapshot, approval) {
});
}
/**
* @param {ReturnType<typeof diffRegistrySnapshots>} diff
* @param {Readonly<Record<string, unknown>>} evidenceFile
*/
export function validateBreakingEvidence(diff, evidenceFile) {
const evidence = new Map(
/** @type {Array<Record<string, unknown>>} */ (
evidenceFile.changes ?? []
).map((entry) => [entry.changeId, entry]),
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 = [];
const failures: string[] = [];
for (const change of diff.changes.filter(
(entry) => entry.impact === "breaking",
)) {
@@ -307,7 +321,8 @@ export function validateBreakingEvidence(diff, evidenceFile) {
"rollback",
"owner",
]) {
if (typeof entry[field] !== "string" || entry[field].trim().length === 0) {
const value = entry[field];
if (typeof value !== "string" || value.trim().length === 0) {
failures.push(
`breaking change ${change.changeId} missing non-empty ${field}`,
);
-547
View File
@@ -1,547 +0,0 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
/** @param {unknown} value @returns {unknown} */
export function canonicalizeSupplyChainValue(value) {
if (Array.isArray(value)) {
return value
.map(canonicalizeSupplyChainValue)
.sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
);
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, canonicalizeSupplyChainValue(item)]),
);
}
return value;
}
/** @param {unknown} value */
export function supplyChainDigest(value) {
return createHash("sha256")
.update(JSON.stringify(canonicalizeSupplyChainValue(value)))
.digest("hex");
}
/** @param {string} lockfile */
export function parsePnpmLockfilePackages(lockfile) {
const entries =
/** @type {Array<{name: string, version: string, integrity: string}>} */ (
[]
);
let inPackages = false;
/** @type {{name: string, version: string, integrity: string} | null} */
let current = 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) {
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) {
current.integrity = integrityMatch[1];
}
}
return entries.sort((left, right) =>
`${left.name}@${left.version}`.localeCompare(
`${right.name}@${right.version}`,
),
);
}
/** @param {string} integrity */
export function isValidSha512Integrity(integrity) {
if (!integrity.startsWith("sha512-")) return false;
try {
return Buffer.from(integrity.slice("sha512-".length), "base64").length === 64;
} catch {
return false;
}
}
/**
* @param {unknown} raw
* @returns {string}
*/
export function normalizeLicense(raw) {
if (typeof raw === "string" && raw.trim()) return raw.trim();
if (
raw &&
typeof raw === "object" &&
"type" in 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";
}
/**
* @param {Record<string, unknown>} root
* @param {Readonly<Record<string, string>>} directProduction
* @param {Readonly<Record<string, string>>} directDevelopment
*/
export async function flattenPnpmDependencyTree(
root,
directProduction,
directDevelopment,
) {
const records =
/** @type {Map<string, {
* name: string,
* version: string,
* direct: boolean,
* scope: "production" | "development",
* optional: boolean,
* packagePath: string,
* dependencies: Set<string>
* }>} */ (new Map());
const directIds = new Set();
for (const [name, rawDependency] of Object.entries(
/** @type {Record<string, unknown>} */ (root.dependencies ?? {}),
)) {
if (
Object.hasOwn(directProduction, name) &&
rawDependency &&
typeof rawDependency === "object" &&
!Array.isArray(rawDependency)
) {
directIds.add(
`${name}@${String(
/** @type {Record<string, unknown>} */ (rawDependency).version ?? "",
)}`,
);
}
}
for (const [name, rawDependency] of Object.entries(
/** @type {Record<string, unknown>} */ (root.devDependencies ?? {}),
)) {
if (
Object.hasOwn(directDevelopment, name) &&
rawDependency &&
typeof rawDependency === "object" &&
!Array.isArray(rawDependency)
) {
directIds.add(
`${name}@${String(
/** @type {Record<string, unknown>} */ (rawDependency).version ?? "",
)}`,
);
}
}
/**
* @param {Record<string, unknown>} node
* @param {"production" | "development"} scope
* @param {boolean} optionalPath
*/
function visit(node, scope, optionalPath) {
for (const [groupName, group] of Object.entries({
dependencies: node.dependencies,
devDependencies: node.devDependencies,
optionalDependencies: node.optionalDependencies,
})) {
if (!group || typeof group !== "object" || Array.isArray(group)) continue;
for (const [name, rawDependency] of Object.entries(group)) {
if (
!rawDependency ||
typeof rawDependency !== "object" ||
Array.isArray(rawDependency)
) {
continue;
}
const dependency =
/** @type {Record<string, unknown>} */ (rawDependency);
const version = String(dependency.version ?? "");
const packagePath = String(dependency.path ?? "");
const identity = `${name}@${version}`;
const childScope =
scope === "production" && groupName !== "devDependencies"
? "production"
: "development";
const childOptional =
optionalPath || groupName === "optionalDependencies";
const previous = records.get(identity);
const dependencies = previous?.dependencies ?? new Set();
for (const childGroup of [
dependency.dependencies,
dependency.optionalDependencies,
]) {
if (
!childGroup ||
typeof childGroup !== "object" ||
Array.isArray(childGroup)
) {
continue;
}
for (const [childName, rawChild] of Object.entries(childGroup)) {
if (
rawChild &&
typeof rawChild === "object" &&
!Array.isArray(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(dependency, childScope, childOptional);
}
}
}
const productionRoot = {
dependencies: Object.fromEntries(
Object.entries(
/** @type {Record<string, unknown>} */ (root.dependencies ?? {}),
).filter(([name]) => Object.hasOwn(directProduction, name)),
),
};
const developmentRoot = {
devDependencies: Object.fromEntries(
Object.entries(
/** @type {Record<string, unknown>} */ (root.devDependencies ?? {}),
).filter(([name]) => Object.hasOwn(directDevelopment, name)),
),
};
visit(productionRoot, "production", false);
visit(developmentRoot, "development", false);
const result = [];
for (const record of records.values()) {
let license = "NOASSERTION";
let optional = record.optional;
if (record.packagePath) {
try {
const manifest = JSON.parse(
await readFile(`${record.packagePath}/package.json`, "utf8"),
);
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}`,
),
);
}
/**
* @param {Readonly<Record<string, unknown>>} before
* @param {Readonly<Record<string, unknown>>} after
*/
export function diffDependencyInventories(before, after) {
const beforeRows =
/** @type {Array<Record<string, unknown>>} */ (before.dependencies ?? []);
const afterRows =
/** @type {Array<Record<string, unknown>>} */ (after.dependencies ?? []);
const beforeMap = new Map(
beforeRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const afterMap = new Map(
afterRows.map((row) => [`${row.name}@${row.version}`, row]),
);
const added = [...afterMap.keys()].filter((key) => !beforeMap.has(key));
const removed = [...beforeMap.keys()].filter((key) => !afterMap.has(key));
const changed = [];
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 = [];
for (const removedKey of removed) {
const previous = beforeMap.get(removedKey);
const replacement = added.find(
(addedKey) => afterMap.get(addedKey)?.name === previous?.name,
);
if (replacement) {
upgrades.push({
name: previous?.name,
from: previous?.version,
to: afterMap.get(replacement)?.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) =>
String(left.name).localeCompare(String(right.name)),
),
),
});
}
/**
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} policy
*/
export function validateLicensePolicy(inventory, policy) {
const allowed = new Set(
/** @type {string[]} */ (policy.allowedLicenses ?? []),
);
const denied = /** @type {string[]} */ (policy.deniedLicensePatterns ?? []);
const failures = [];
const results = [];
for (const dependency of /** @type {Array<Record<string, unknown>>} */ (
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: `${dependency.name}@${dependency.version}`,
license,
passed,
reason: unknownAccepted ? "platform-optional-not-materialized" : null,
});
if (!passed) {
failures.push(
`${dependency.name}@${dependency.version} has disallowed license ${license}`,
);
}
}
return Object.freeze({
passed: failures.length === 0,
failures: Object.freeze(failures),
results: Object.freeze(results),
});
}
/**
* @param {ReturnType<typeof diffDependencyInventories>} diff
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} evidenceFile
*/
export function validateDependencyReview(diff, inventory, evidenceFile) {
const rows =
/** @type {Array<Record<string, unknown>>} */ (inventory.dependencies ?? []);
const byIdentity = new Map(
rows.map((row) => [`${row.name}@${row.version}`, row]),
);
const evidence = new Map(
/** @type {Array<Record<string, unknown>>} */ (
evidenceFile.changes ?? []
).map((entry) => [entry.changeId, entry]),
);
const highRisk = diff.added.filter((identity) => {
const row = byIdentity.get(identity);
return row?.direct === true && row.scope === "production";
});
const failures = [];
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"]) {
if (typeof entry[field] !== "string" || !entry[field].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 = new Map([
["unknown", 0],
["low", 1],
["moderate", 2],
["high", 3],
["critical", 4],
]);
/**
* @param {Readonly<Record<string, unknown>>} report
* @param {Readonly<Record<string, unknown>>} policy
* @param {Readonly<Record<string, unknown>>} exceptionFile
* @param {string} lockfileSha256
* @param {Date} [now]
*/
export function validateVulnerabilityReport(
report,
policy,
exceptionFile,
lockfileSha256,
now = new Date(),
) {
const failures = [];
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 =
/** @type {Array<Record<string, unknown>>} */ (
exceptionFile.exceptions ?? []
);
const blocking = [];
for (const finding of /** @type {Array<Record<string, unknown>>} */ (
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 =
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(
`${finding.id}:${finding.packageName}@${finding.version}:${severity}`,
);
}
}
return Object.freeze({
passed: failures.length === 0 && blocking.length === 0,
failures: Object.freeze(failures),
blocking: Object.freeze(blocking),
});
}
/**
* @param {Readonly<Record<string, unknown>>} sbom
* @param {Readonly<Record<string, unknown>>} inventory
* @param {Readonly<Record<string, unknown>>} provenance
* @param {string} distDigest
*/
export function verifySupplyChainCoherence(
sbom,
inventory,
provenance,
distDigest,
) {
const failures = [];
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 =
/** @type {Record<string, unknown>} */ (sbom.metadata ?? {});
const properties =
/** @type {Array<{name?: string, value?: string}>} */ (
metadata.properties ?? []
);
if (properties.find(
/** @param {{name?: string, value?: string}} property */
(property) =>
property.name === "ca:lockfileSha256" &&
property.value === inventory.lockfileSha256,
) === undefined) {
failures.push("SBOM lockfile digest does not match inventory");
}
const subject =
/** @type {Array<Record<string, unknown>>} */ (provenance.subject ?? [])[0];
const subjectDigest =
/** @type {Record<string, unknown>} */ (subject?.digest ?? {});
if (subjectDigest.sha256 !== distDigest) {
failures.push("provenance subject does not match built dist digest");
}
const predicate =
/** @type {Record<string, unknown>} */ (provenance.predicate ?? {});
const materials =
/** @type {Record<string, unknown>} */ (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),
});
}
+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),
});
}
+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("\\", "/");
}