Files
Portfolio/scripts/build-contract.mjs

204 lines
8.8 KiB
JavaScript

const allowedManifestFields = new Set([
'file', 'name', 'src', 'isEntry', 'isDynamicEntry', 'imports', 'dynamicImports', 'css', 'assets',
]);
const safeOutputPath = /^assets\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/u;
const safeBuildId = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
const sha256Pattern = /^[a-f0-9]{64}$/u;
const sorted = (values) => [...new Set(values)].sort();
const assertStringArray = (value, label) => {
if (value === undefined) return [];
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
throw new Error(`${label} must be an array of strings.`);
}
return value;
};
const validateRecord = (key, record) => {
if (typeof record !== 'object' || record === null || Array.isArray(record)) {
throw new Error(`Vite manifest record must be an object: ${key}`);
}
for (const field of Object.keys(record)) {
if (!allowedManifestFields.has(field)) throw new Error(`Unknown Vite manifest field: ${field}`);
}
if (typeof record.file !== 'string' || !safeOutputPath.test(record.file)) {
throw new Error(`Vite manifest contains an unsafe emitted path: ${String(record.file)}`);
}
for (const field of ['css', 'assets']) {
for (const path of assertStringArray(record[field], `Vite manifest ${field}`)) {
if (!safeOutputPath.test(path)) throw new Error(`Vite manifest contains an unsafe emitted path: ${path}`);
}
}
for (const field of ['imports', 'dynamicImports']) {
assertStringArray(record[field], `Vite manifest ${field}`);
}
for (const field of ['src', 'name']) {
if (record[field] !== undefined && typeof record[field] !== 'string') {
throw new Error(`Vite manifest ${field} must be a string.`);
}
}
for (const field of ['isEntry', 'isDynamicEntry']) {
if (record[field] !== undefined && typeof record[field] !== 'boolean') {
throw new Error(`Vite manifest ${field} must be boolean.`);
}
}
};
export const resolveViteAssetClosure = (manifest) => {
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) {
throw new Error('Vite manifest must be an object.');
}
const entry = manifest['index.html'];
if (entry === undefined || entry.isEntry !== true || entry.src !== 'index.html') {
throw new Error('Vite manifest must contain the exact index.html entry.');
}
for (const [key, record] of Object.entries(manifest)) {
validateRecord(key, record);
if (key !== 'index.html' && record.isEntry === true) {
throw new Error(`Unexpected additional Vite entry: ${key}`);
}
}
const allReachable = new Set();
const staticReachable = new Set();
const visit = (key, staticImport) => {
const record = manifest[key];
if (record === undefined) throw new Error(`Missing manifest import: ${key}`);
if (staticImport) staticReachable.add(key);
if (allReachable.has(key)) return;
allReachable.add(key);
for (const importedKey of record.imports ?? []) visit(importedKey, staticImport);
for (const importedKey of record.dynamicImports ?? []) visit(importedKey, false);
};
visit('index.html', true);
const unreachable = Object.keys(manifest).filter((key) => !allReachable.has(key));
if (unreachable.length > 0) throw new Error(`Unreachable manifest record: ${unreachable.join(', ')}`);
const executableModules = [];
const modulePreloads = [];
const styles = [];
const viteAssets = [];
for (const key of allReachable) {
const record = manifest[key];
executableModules.push(record.file);
if (staticReachable.has(key)) modulePreloads.push(record.file);
styles.push(...(record.css ?? []));
viteAssets.push(...(record.assets ?? []));
}
return {
entryModule: entry.file,
executableModules: sorted(executableModules),
modulePreloads: sorted(modulePreloads),
styles: sorted(styles),
viteAssets: sorted(viteAssets),
};
};
const assertSortedUniquePaths = (value, label) => {
if (!Array.isArray(value) || value.some((path) => typeof path !== 'string' || !safeOutputPath.test(path))) {
throw new Error(`${label} must contain only safe asset paths.`);
}
if (JSON.stringify(value) !== JSON.stringify(sorted(value))) {
throw new Error(`${label} must be sorted and unique.`);
}
return value;
};
export const parseArtifactContract = (input) => {
const fields = [
'schema', 'buildId', 'entryModule', 'executableModules', 'modulePreloads', 'styles', 'viteAssets', 'publicAssets', 'files',
];
if (typeof input !== 'object' || input === null || Array.isArray(input) ||
JSON.stringify(Object.keys(input).sort()) !== JSON.stringify([...fields].sort())) {
throw new Error('Artifact contract has unknown or missing fields.');
}
if (input.schema !== 'portfolio-artifact-contract/v1' ||
typeof input.buildId !== 'string' || !safeBuildId.test(input.buildId) ||
typeof input.entryModule !== 'string' || !safeOutputPath.test(input.entryModule)) {
throw new Error('Artifact contract identity is invalid.');
}
const executableModules = assertSortedUniquePaths(input.executableModules, 'Executable modules');
const modulePreloads = assertSortedUniquePaths(input.modulePreloads, 'Module preloads');
const styles = assertSortedUniquePaths(input.styles, 'Styles');
const viteAssets = assertSortedUniquePaths(input.viteAssets, 'Vite assets');
const publicAssets = assertSortedUniquePaths(input.publicAssets, 'Public assets');
if (!executableModules.includes(input.entryModule) || !modulePreloads.includes(input.entryModule) ||
modulePreloads.some((path) => !executableModules.includes(path))) {
throw new Error('Artifact contract module closure is inconsistent.');
}
if (!Array.isArray(input.files)) throw new Error('Artifact contract files must be an array.');
const files = input.files.map((record) => {
if (typeof record !== 'object' || record === null || Array.isArray(record) ||
JSON.stringify(Object.keys(record).sort()) !== JSON.stringify(['path', 'sha256']) ||
typeof record.path !== 'string' || !safeOutputPath.test(record.path) ||
typeof record.sha256 !== 'string' || !sha256Pattern.test(record.sha256)) {
throw new Error('Artifact contract file record is invalid.');
}
return record;
});
if (JSON.stringify(files.map((record) => record.path)) !== JSON.stringify(sorted(files.map((record) => record.path)))) {
throw new Error('Artifact contract files must be sorted and unique.');
}
const expectedFiles = sorted([...executableModules, ...styles, ...viteAssets, ...publicAssets]);
if (JSON.stringify(files.map((record) => record.path)) !== JSON.stringify(expectedFiles)) {
throw new Error('Artifact contract file inventory does not equal its trusted closure.');
}
return input;
};
export const createArtifactContract = (closure, publicManifest, digestByPath, buildId) => {
const publicAssets = sorted(publicManifest.assets.map((asset) => `assets/${asset.fileName}`));
const expectedPaths = sorted([
...closure.executableModules,
...closure.styles,
...closure.viteAssets,
...publicAssets,
]);
const digestPaths = Object.keys(digestByPath).sort();
if (JSON.stringify(digestPaths) !== JSON.stringify(expectedPaths)) {
throw new Error('Asset digests must exactly cover the trusted artifact closure.');
}
return parseArtifactContract({
schema: 'portfolio-artifact-contract/v1',
buildId,
entryModule: closure.entryModule,
executableModules: sorted(closure.executableModules),
modulePreloads: sorted(closure.modulePreloads),
styles: sorted(closure.styles),
viteAssets: sorted(closure.viteAssets),
publicAssets,
files: expectedPaths.map((path) => ({ path, sha256: digestByPath[path] })),
});
};
export const serializeArtifactContract = (contract) => `${JSON.stringify(parseArtifactContract(contract), null, 2)}\n`;
export const createBuildAuthority = (viteManifest, artifactContract) => ({
schema: 'portfolio-build-authority/v1',
viteManifest,
artifactContract: parseArtifactContract(artifactContract),
});
export const parseBuildAuthority = (input) => {
if (typeof input !== 'object' || input === null || Array.isArray(input) ||
JSON.stringify(Object.keys(input).sort()) !==
JSON.stringify(['artifactContract', 'schema', 'viteManifest'])) {
throw new Error('Build authority has unknown or missing fields.');
}
if (input.schema !== 'portfolio-build-authority/v1') throw new Error('Build authority schema is invalid.');
const closure = resolveViteAssetClosure(input.viteManifest);
const contract = parseArtifactContract(input.artifactContract);
for (const field of ['entryModule', 'executableModules', 'modulePreloads', 'styles', 'viteAssets']) {
if (JSON.stringify(contract[field]) !== JSON.stringify(closure[field])) {
throw new Error(`Build authority manifest disagrees with artifact contract: ${field}`);
}
}
return input;
};
export const serializeBuildAuthority = (authority) =>
`${JSON.stringify(parseBuildAuthority(authority), null, 2)}\n`;