fix: harden prerender publication boundary

This commit is contained in:
2026-09-02 02:44:23 +03:00
parent 3dd4a818d0
commit c16c4c57da
22 changed files with 1275 additions and 231 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
node_modules/
dist/
dist-client/
dist-ssr/
coverage/
playwright-report/

View File

@@ -11,7 +11,7 @@
"typecheck": "vue-tsc --noEmit",
"test:unit": "vitest run",
"test:browser": "playwright test",
"build:client": "vite build --outDir dist/client --manifest",
"build:client": "vite build --outDir dist-client --manifest",
"build:ssr": "vite build --ssr src/entry-server.ts --outDir dist-ssr",
"prerender": "node scripts/prerender.mjs",
"verify:artifacts": "node scripts/verify-artifacts.mjs",

View File

@@ -0,0 +1,40 @@
import type { ViteManifest } from './prerender.mjs';
export interface ViteAssetClosure {
entryModule: string;
executableModules: string[];
modulePreloads: string[];
styles: string[];
viteAssets: string[];
}
export interface ArtifactContractFile {
path: string;
sha256: string;
}
export interface ArtifactContract extends ViteAssetClosure {
schema: 'portfolio-artifact-contract/v1';
buildId: string;
publicAssets: string[];
files: ArtifactContractFile[];
}
export interface BuildAuthority {
schema: 'portfolio-build-authority/v1';
viteManifest: ViteManifest;
artifactContract: ArtifactContract;
}
export function resolveViteAssetClosure(manifest: ViteManifest): ViteAssetClosure;
export function parseArtifactContract(input: unknown): ArtifactContract;
export function createArtifactContract(
closure: ViteAssetClosure,
publicManifest: { assets: ReadonlyArray<{ fileName: string }> },
digestByPath: Readonly<Record<string, string>>,
buildId: string,
): ArtifactContract;
export function serializeArtifactContract(contract: ArtifactContract): string;
export function createBuildAuthority(viteManifest: ViteManifest, artifactContract: ArtifactContract): BuildAuthority;
export function parseBuildAuthority(input: unknown): BuildAuthority;
export function serializeBuildAuthority(authority: BuildAuthority): string;

203
scripts/build-contract.mjs Normal file
View File

@@ -0,0 +1,203 @@
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`;

View File

@@ -0,0 +1 @@
export function parseExactConfigArgument(arguments_: string[]): string;

View File

@@ -0,0 +1,16 @@
export const parseExactConfigArgument = (arguments_) => {
if (arguments_.length !== 2 || arguments_[0] !== '--config') {
throw new Error('Prerender accepts exactly --config <path>.');
}
const configPath = arguments_[1];
if (
configPath.length === 0 ||
configPath.startsWith('/') ||
configPath.includes('\\') ||
configPath.split('/').some((part) => part === '' || part === '.' || part === '..') ||
!/^config\/[a-z0-9][a-z0-9._-]*\.json$/u.test(configPath)
) {
throw new Error('Config must be a safe repository-relative JSON path inside config/.');
}
return configPath;
};

View File

@@ -6,12 +6,13 @@ export interface ViteManifestEntry {
isEntry?: boolean;
css?: string[];
imports?: string[];
dynamicImports?: string[];
assets?: string[];
}
export type ViteManifest = Record<string, ViteManifestEntry>;
export function parseExactConfigArgument(arguments_: string[]): string;
export function assertExpectedDistInputs(distRoot: string): Promise<void>;
export function assemblePrerenderedHtml(
template: string,
manifest: ViteManifest,

View File

@@ -1,9 +1,22 @@
import { cp, lstat, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { constants } from 'node:fs';
import { createHash, randomUUID } from 'node:crypto';
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
import { dirname, relative, resolve, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { loadProductionInputs } from '../src/content/load-production-inputs.ts';
import { createPageMetadata, createStaticHostArtifacts } from '../src/content/metadata.ts';
import { serializeJsonLd } from '../src/security/serialize-json-ld.ts';
import {
createArtifactContract,
createBuildAuthority,
resolveViteAssetClosure,
serializeBuildAuthority,
} from './build-contract.mjs';
import { parseExactConfigArgument } from './config-argument.mjs';
import { captureDirectoryIdentity, publishStagedDirectory } from './safe-publication.mjs';
import { verifyArtifactDirectory } from './verify-artifacts.mjs';
export { parseExactConfigArgument } from './config-argument.mjs';
const scriptPath = fileURLToPath(import.meta.url);
const repositoryRoot = resolve(dirname(scriptPath), '..');
@@ -19,33 +32,6 @@ const exactlyOne = (source, expression, label) => {
if (matches.length !== 1) throw new Error(`Expected exactly one ${label}.`);
};
export const parseExactConfigArgument = (arguments_) => {
if (arguments_.length !== 2 || arguments_[0] !== '--config') {
throw new Error('Prerender accepts exactly --config <path>.');
}
const configPath = arguments_[1];
if (
configPath.length === 0 ||
configPath.startsWith('/') ||
configPath.includes('\\') ||
configPath.split('/').some((part) => part === '' || part === '.' || part === '..') ||
!/^config\/[a-z0-9][a-z0-9._-]*\.json$/u.test(configPath)
) {
throw new Error('Config must be a safe repository-relative JSON path inside config/.');
}
return configPath;
};
const manifestEntry = (manifest) => {
const entries = Object.values(manifest).filter((entry) => entry.isEntry === true);
if (entries.length !== 1) throw new Error('Vite manifest must contain exactly one client entry.');
const entry = entries[0];
if (typeof entry.file !== 'string' || !/^assets\/[a-zA-Z0-9._-]+\.js$/u.test(entry.file)) {
throw new Error('Vite manifest client entry is unsafe.');
}
return entry;
};
const metadataMarkup = (metadata) => {
const image = metadata.openGraph.image === undefined
? ''
@@ -63,8 +49,8 @@ const metadataMarkup = (metadata) => {
};
export const assemblePrerenderedHtml = (template, manifest, markup, inputs) => {
const entry = manifestEntry(manifest);
const entryUrl = `/${entry.file}`;
const closure = resolveViteAssetClosure(manifest);
const entryUrl = `/${closure.entryModule}`;
exactlyOne(template, /<title>[^<]*<\/title>/gu, 'title');
exactlyOne(template, /<div id="app"><\/div>/gu, 'empty app root');
exactlyOne(template, /<script type="module"[^>]*><\/script>/gu, 'client module script');
@@ -73,8 +59,7 @@ export const assemblePrerenderedHtml = (template, manifest, markup, inputs) => {
const metadata = createPageMetadata(inputs.content, inputs.manifest, inputs.site);
const snapshot = serializeJsonLd(inputs);
const preloads = [entry.file, ...(entry.imports ?? []).map((key) => manifest[key]?.file)]
.filter((file, index, files) => typeof file === 'string' && files.indexOf(file) === index)
const preloads = closure.modulePreloads
.map((file) => ` <link rel="modulepreload" href="/${file}" />`)
.join('\n');
const withoutModule = template.replace(/\s*<script type="module"[^>]*><\/script>/u, '');
@@ -89,62 +74,6 @@ export const assemblePrerenderedHtml = (template, manifest, markup, inputs) => {
return withHead.replace('<!--portfolio-body-->', clientBootstrap);
};
const assertDirectoryMembers = async (root, expected, label) => {
const entries = await readdir(root, { withFileTypes: true });
for (const entry of entries) {
if (!expected.has(entry.name)) throw new Error(`Unexpected build output in ${label}: ${entry.name}`);
if (entry.isSymbolicLink()) throw new Error(`Unexpected linked build output in ${label}: ${entry.name}`);
}
for (const name of expected) {
if (!entries.some((entry) => entry.name === name)) throw new Error(`Missing build output in ${label}: ${name}`);
}
return entries;
};
const manifestAssetFiles = (manifest) => {
const files = new Set();
for (const entry of Object.values(manifest)) {
for (const file of [entry.file, ...(entry.css ?? []), ...(entry.assets ?? [])]) {
if (typeof file !== 'string' || !/^assets\/[a-zA-Z0-9._-]+$/u.test(file)) {
throw new Error('Vite manifest contains an unsafe output path.');
}
files.add(file.slice('assets/'.length));
}
}
return files;
};
export const assertExpectedDistInputs = async (distRoot) => {
const allowed = new Set([
'client', 'assets', 'index.html', 'robots.txt', 'sitemap.xml', '_headers.example', '_redirects.example',
]);
const rootEntries = await readdir(distRoot, { withFileTypes: true });
for (const entry of rootEntries) {
if (!allowed.has(entry.name)) throw new Error(`Unexpected build output: ${entry.name}`);
if (entry.isSymbolicLink()) throw new Error(`Unexpected linked build output: ${entry.name}`);
}
const clientStats = await lstat(resolve(distRoot, 'client'));
if (clientStats.isSymbolicLink() || !clientStats.isDirectory()) {
throw new Error('Client output must be a non-symlink directory.');
}
await assertDirectoryMembers(resolve(distRoot, 'client'), new Set(['.vite', 'assets', 'index.html']), 'client');
await assertDirectoryMembers(resolve(distRoot, 'client/.vite'), new Set(['manifest.json']), 'client/.vite');
const manifest = JSON.parse(await readFile(resolve(distRoot, 'client/.vite/manifest.json'), 'utf8'));
await assertDirectoryMembers(resolve(distRoot, 'client/assets'), manifestAssetFiles(manifest), 'client/assets');
if (rootEntries.some((entry) => entry.name === 'assets')) {
if (!rootEntries.some((entry) => entry.name === 'index.html')) {
throw new Error('Existing root assets require their generated index.html inventory.');
}
const existingHtml = await readFile(resolve(distRoot, 'index.html'), 'utf8');
const referencedAssets = new Set(
[...existingHtml.matchAll(/(?:src|href)="\/assets\/([a-zA-Z0-9._-]+)"/gu)].map((match) => match[1]),
);
await assertDirectoryMembers(resolve(distRoot, 'assets'), referencedAssets, 'assets');
}
};
const assertInsideRepository = (path) => {
const pathRelativeToRoot = relative(repositoryRoot, path);
if (pathRelativeToRoot === '..' || pathRelativeToRoot.startsWith(`..${sep}`)) {
@@ -152,12 +81,152 @@ const assertInsideRepository = (path) => {
}
};
const isMissing = (error) =>
typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
const fileIdentity = (stats) => ({
dev: stats.dev,
ino: stats.ino,
size: stats.size,
mtimeMs: stats.mtimeMs,
});
const sameFileIdentity = (left, right) =>
left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs;
const assertRegularFile = async (path, label) => {
const stats = await lstat(path);
if (stats.isSymbolicLink() || !stats.isFile() || stats.nlink !== 1) {
throw new Error(`${label} must be one regular, non-linked file.`);
}
return fileIdentity(stats);
};
const readSafeRegularFile = async (path, label) => {
const initialIdentity = await assertRegularFile(path, label);
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
try {
const openedStats = await handle.stat();
if (!openedStats.isFile() || openedStats.nlink !== 1 ||
!sameFileIdentity(initialIdentity, fileIdentity(openedStats))) {
throw new Error(`${label} identity changed while opening.`);
}
const bytes = await handle.readFile();
if (!sameFileIdentity(initialIdentity, await assertRegularFile(path, label))) {
throw new Error(`${label} identity changed while reading.`);
}
return bytes;
} finally {
await handle.close();
}
};
const assertExactDirectoryMembers = async (root, expectedNames, label) => {
await captureDirectoryIdentity(root);
const entries = await readdir(root, { withFileTypes: true });
const actualNames = entries.map((entry) => entry.name).sort();
const expected = [...expectedNames].sort();
if (JSON.stringify(actualNames) !== JSON.stringify(expected)) {
throw new Error(`${label} inventory is not exact.`);
}
if (entries.some((entry) => entry.isSymbolicLink())) throw new Error(`${label} contains a symlink.`);
return entries;
};
const loadTrustedClientBuild = async (clientRoot) => {
await assertExactDirectoryMembers(clientRoot, ['.vite', 'assets', 'index.html'], 'Client build');
const viteRoot = resolve(clientRoot, '.vite');
const assetsRoot = resolve(clientRoot, 'assets');
await assertExactDirectoryMembers(viteRoot, ['manifest.json'], 'Client Vite metadata');
await captureDirectoryIdentity(assetsRoot);
const template = (await readSafeRegularFile(resolve(clientRoot, 'index.html'), 'Built client template')).toString('utf8');
const manifestSource = (await readSafeRegularFile(resolve(viteRoot, 'manifest.json'), 'Vite manifest')).toString('utf8');
const manifest = JSON.parse(manifestSource);
const closure = resolveViteAssetClosure(manifest);
const clientPaths = [...new Set([
...closure.executableModules,
...closure.styles,
...closure.viteAssets,
])].sort();
await assertExactDirectoryMembers(
assetsRoot,
clientPaths.map((path) => path.slice('assets/'.length)),
'Client assets',
);
const bytesByPath = {};
for (const path of clientPaths) {
bytesByPath[path] = await readSafeRegularFile(resolve(clientRoot, path), `Client asset ${path}`);
}
return { template, manifest, closure, bytesByPath };
};
const loadPreparedPublicAssets = async (inputs) => {
const preparedRoot = resolve(repositoryRoot, 'inputs/prepared-assets');
await captureDirectoryIdentity(preparedRoot);
const bytesByPath = {};
for (const asset of inputs.manifest.assets) {
bytesByPath[`assets/${asset.fileName}`] = await readSafeRegularFile(
resolve(preparedRoot, asset.fileName),
`Prepared public asset ${asset.fileName}`,
);
}
return bytesByPath;
};
const writeFreshStagingArtifacts = async (stagingRoot, html, artifacts, bytesByPath) => {
await mkdir(resolve(stagingRoot, 'assets'));
for (const [path, bytes] of Object.entries(bytesByPath)) {
await writeFile(resolve(stagingRoot, path), bytes, { flag: 'wx' });
}
await Promise.all([
writeFile(resolve(stagingRoot, 'index.html'), html, { flag: 'wx' }),
writeFile(resolve(stagingRoot, 'robots.txt'), artifacts.robots, { flag: 'wx' }),
writeFile(resolve(stagingRoot, '_headers.example'), artifacts.headers, { flag: 'wx' }),
writeFile(resolve(stagingRoot, '_redirects.example'), artifacts.redirects, { flag: 'wx' }),
...(artifacts.sitemap === undefined
? []
: [writeFile(resolve(stagingRoot, 'sitemap.xml'), artifacts.sitemap, { flag: 'wx' })]),
]);
};
const removeOwnedStaging = async (stagingRoot) => {
if (dirname(stagingRoot) !== repositoryRoot || !stagingRoot.startsWith(resolve(repositoryRoot, '.dist-stage-'))) {
throw new Error('Refusing to remove an unowned staging path.');
}
try {
await captureDirectoryIdentity(stagingRoot);
} catch (error) {
if (isMissing(error) || /existing safe directory/iu.test(String(error))) return;
throw error;
}
await rm(stagingRoot, { recursive: true });
};
const writeAuthorityAtomically = async (authority) => {
const serverRoot = resolve(repositoryRoot, 'dist-ssr');
await captureDirectoryIdentity(serverRoot);
const authorityPath = resolve(serverRoot, 'artifact-authority.json');
try {
await assertRegularFile(authorityPath, 'Existing artifact authority');
} catch (error) {
if (!isMissing(error)) throw error;
}
const temporaryPath = resolve(serverRoot, `.artifact-authority-${randomUUID()}.tmp`);
try {
await writeFile(temporaryPath, serializeBuildAuthority(authority), { flag: 'wx' });
await assertRegularFile(temporaryPath, 'Temporary artifact authority');
await rename(temporaryPath, authorityPath);
} catch (error) {
await rm(temporaryPath, { force: true });
throw error;
}
};
export const prerender = async (configPath) => {
const distRoot = resolve(repositoryRoot, 'dist');
const clientRoot = resolve(distRoot, 'client');
const clientRoot = resolve(repositoryRoot, 'dist-client');
const configAbsolutePath = resolve(repositoryRoot, configPath);
assertInsideRepository(configAbsolutePath);
await assertExpectedDistInputs(distRoot);
if (await realpath(repositoryRoot) !== repositoryRoot) throw new Error('Repository root has a symlinked ancestor.');
const inputs = await loadProductionInputs({
contentJson: pathToFileURL(resolve(repositoryRoot, 'content/public/portfolio-content.json')),
@@ -165,32 +234,47 @@ export const prerender = async (configPath) => {
preparedAssetRoot: pathToFileURL(`${resolve(repositoryRoot, 'inputs/prepared-assets')}${sep}`),
siteConfigJson: pathToFileURL(configAbsolutePath),
});
const [{ render }, template, manifestSource] = await Promise.all([
import(pathToFileURL(resolve(repositoryRoot, 'dist-ssr/entry-server.js')).href),
readFile(resolve(clientRoot, 'index.html'), 'utf8'),
readFile(resolve(clientRoot, '.vite/manifest.json'), 'utf8'),
]);
const clientBuild = await loadTrustedClientBuild(clientRoot);
const serverEntryPath = resolve(repositoryRoot, 'dist-ssr/entry-server.js');
const serverEntryIdentity = await assertRegularFile(serverEntryPath, 'SSR entry');
const { render } = await import(`${pathToFileURL(serverEntryPath).href}?build=${encodeURIComponent(inputs.site.buildId)}`);
if (typeof render !== 'function') throw new Error('SSR entry does not export render().');
const manifest = JSON.parse(manifestSource);
if (!sameFileIdentity(serverEntryIdentity, await assertRegularFile(serverEntryPath, 'SSR entry'))) {
throw new Error('SSR entry identity changed during import.');
}
const markup = await render('/', inputs);
const html = assemblePrerenderedHtml(template, manifest, markup, inputs);
const html = assemblePrerenderedHtml(clientBuild.template, clientBuild.manifest, markup, inputs);
const metadata = createPageMetadata(inputs.content, inputs.manifest, inputs.site);
const artifacts = createStaticHostArtifacts(metadata, inputs.site);
for (const path of ['assets', 'index.html', 'robots.txt', 'sitemap.xml', '_headers.example', '_redirects.example']) {
await rm(resolve(distRoot, path), { recursive: true, force: true });
const publicBytes = await loadPreparedPublicAssets(inputs);
const bytesByPath = { ...clientBuild.bytesByPath, ...publicBytes };
const digestByPath = Object.fromEntries(Object.entries(bytesByPath).map(([path, bytes]) => [
path,
createHash('sha256').update(bytes).digest('hex'),
]));
const contract = createArtifactContract(
clientBuild.closure,
inputs.manifest,
digestByPath,
inputs.site.buildId,
);
const authority = createBuildAuthority(clientBuild.manifest, contract);
const stagingRoot = await mkdtemp(resolve(repositoryRoot, '.dist-stage-'));
try {
await writeFreshStagingArtifacts(stagingRoot, html, artifacts, bytesByPath);
await verifyArtifactDirectory(stagingRoot, inputs, contract);
const stagingIdentity = await captureDirectoryIdentity(stagingRoot);
await publishStagedDirectory({
repositoryRoot,
finalRoot: distRoot,
stagingRoot,
expectedStagingIdentity: stagingIdentity,
beforeCommit: async () => writeAuthorityAtomically(authority),
});
} catch (error) {
await removeOwnedStaging(stagingRoot);
throw error;
}
await cp(resolve(clientRoot, 'assets'), resolve(distRoot, 'assets'), { recursive: true, errorOnExist: true });
await Promise.all([
writeFile(resolve(distRoot, 'index.html'), html),
writeFile(resolve(distRoot, 'robots.txt'), artifacts.robots),
writeFile(resolve(distRoot, '_headers.example'), artifacts.headers),
writeFile(resolve(distRoot, '_redirects.example'), artifacts.redirects),
...(artifacts.sitemap === undefined
? []
: [writeFile(resolve(distRoot, 'sitemap.xml'), artifacts.sitemap)]),
]);
await rm(clientRoot, { recursive: true });
};
if (process.argv[1] !== undefined && resolve(process.argv[1]) === scriptPath) {

View File

@@ -0,0 +1,14 @@
export interface DirectoryIdentity {
dev: number;
ino: number;
}
export function captureDirectoryIdentity(path: string): Promise<DirectoryIdentity>;
export function publishStagedDirectory(options: {
repositoryRoot: string;
finalRoot: string;
stagingRoot: string;
expectedStagingIdentity: DirectoryIdentity;
renameDirectory?: (from: string, to: string) => Promise<void>;
beforeCommit?: () => Promise<void>;
}): Promise<void>;

View File

@@ -0,0 +1,125 @@
import { randomUUID } from 'node:crypto';
import { lstat, realpath, rename, rm } from 'node:fs/promises';
import { basename, dirname, relative, resolve, sep } from 'node:path';
const isMissing = (error) =>
typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
const assertInsideRepository = (repositoryRoot, path, label) => {
const pathRelativeToRoot = relative(repositoryRoot, path);
if (pathRelativeToRoot === '' || pathRelativeToRoot === '..' || pathRelativeToRoot.startsWith(`..${sep}`)) {
throw new Error(`${label} is outside the repository.`);
}
};
const assertCanonicalDirectory = async (path, label) => {
const resolvedPath = resolve(path);
let canonicalPath;
try {
canonicalPath = await realpath(resolvedPath);
} catch (error) {
if (isMissing(error)) throw new Error(`${label} must be an existing safe directory.`);
throw error;
}
if (canonicalPath !== resolvedPath) throw new Error(`${label} or an ancestor is symlinked or non-canonical.`);
const stats = await lstat(resolvedPath);
if (stats.isSymbolicLink() || !stats.isDirectory()) throw new Error(`${label} must be a safe directory.`);
return stats;
};
export const captureDirectoryIdentity = async (path) => {
const stats = await assertCanonicalDirectory(path, 'Directory');
return { dev: stats.dev, ino: stats.ino };
};
const sameIdentity = (left, right) => left.dev === right.dev && left.ino === right.ino;
const assertOptionalFinalDirectory = async (repositoryRoot, finalRoot) => {
assertInsideRepository(repositoryRoot, finalRoot, 'Final output');
if (dirname(finalRoot) !== repositoryRoot || basename(finalRoot) !== 'dist') {
throw new Error('Final output must be the repository-contained dist sibling.');
}
try {
const stats = await lstat(finalRoot);
if (stats.isSymbolicLink() || !stats.isDirectory() || await realpath(finalRoot) !== finalRoot) {
throw new Error('Final output must be an existing safe directory.');
}
return { exists: true, identity: { dev: stats.dev, ino: stats.ino } };
} catch (error) {
if (isMissing(error)) return { exists: false, identity: undefined };
throw error;
}
};
const removeOwnedBackup = async (repositoryRoot, backupRoot) => {
assertInsideRepository(repositoryRoot, backupRoot, 'Backup');
if (dirname(backupRoot) !== repositoryRoot || !basename(backupRoot).startsWith('.dist-backup-')) {
throw new Error('Refusing to remove an unowned backup path.');
}
const stats = await lstat(backupRoot);
if (stats.isSymbolicLink() || !stats.isDirectory() || await realpath(backupRoot) !== backupRoot) {
throw new Error('Refusing to remove an unsafe backup path.');
}
await rm(backupRoot, { recursive: true });
};
export const publishStagedDirectory = async ({
repositoryRoot: repositoryRootInput,
finalRoot: finalRootInput,
stagingRoot: stagingRootInput,
expectedStagingIdentity,
renameDirectory = rename,
beforeCommit = async () => {},
}) => {
const repositoryRoot = resolve(repositoryRootInput);
const finalRoot = resolve(finalRootInput);
const stagingRoot = resolve(stagingRootInput);
await assertCanonicalDirectory(repositoryRoot, 'Repository root');
assertInsideRepository(repositoryRoot, stagingRoot, 'Staging output');
if (dirname(stagingRoot) !== repositoryRoot || !basename(stagingRoot).startsWith('.dist-stage-')) {
throw new Error('Staging output must be a repository-contained owned sibling.');
}
const stagingIdentity = await captureDirectoryIdentity(stagingRoot);
if (!sameIdentity(stagingIdentity, expectedStagingIdentity)) {
throw new Error('Staging directory identity changed after verification.');
}
const previous = await assertOptionalFinalDirectory(repositoryRoot, finalRoot);
await assertCanonicalDirectory(repositoryRoot, 'Repository root');
if (!sameIdentity(await captureDirectoryIdentity(stagingRoot), expectedStagingIdentity)) {
throw new Error('Staging directory identity changed before publication.');
}
if (previous.exists) {
const currentFinal = await captureDirectoryIdentity(finalRoot);
if (!sameIdentity(currentFinal, previous.identity)) throw new Error('Final output identity changed before publication.');
}
const backupRoot = resolve(repositoryRoot, `.dist-backup-${randomUUID()}`);
let backupCreated = false;
let stagedPublished = false;
try {
if (previous.exists) {
await renameDirectory(finalRoot, backupRoot);
backupCreated = true;
}
await renameDirectory(stagingRoot, finalRoot);
stagedPublished = true;
await beforeCommit();
} catch (error) {
try {
if (stagedPublished) {
await renameDirectory(finalRoot, stagingRoot);
stagedPublished = false;
}
if (backupCreated) {
await renameDirectory(backupRoot, finalRoot);
backupCreated = false;
}
} catch (rollbackError) {
throw new AggregateError([error, rollbackError], 'Publication failed and rollback could not restore the previous output.');
}
throw error;
}
if (backupCreated) await removeOwnedBackup(repositoryRoot, backupRoot);
};

View File

@@ -3,5 +3,6 @@ import type { ProductionInputs } from '../src/content/types.ts';
export function verifyArtifactDirectory(
root: string,
inputs: Readonly<ProductionInputs>,
contract: unknown,
): Promise<void>;
export function verifyArtifacts(configPath: string): Promise<void>;

View File

@@ -3,7 +3,10 @@ import { lstat, readFile, readdir } from 'node:fs/promises';
import { dirname, relative, resolve, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { loadProductionInputs } from '../src/content/load-production-inputs.ts';
import { parseExactConfigArgument } from './prerender.mjs';
import { createPageMetadata, createStaticHostArtifacts } from '../src/content/metadata.ts';
import { Window } from 'happy-dom';
import { parseArtifactContract, parseBuildAuthority } from './build-contract.mjs';
import { parseExactConfigArgument } from './config-argument.mjs';
const scriptPath = fileURLToPath(import.meta.url);
const repositoryRoot = resolve(dirname(scriptPath), '..');
@@ -59,18 +62,20 @@ const listArtifacts = async (root) => {
return files.sort();
};
const assertArtifactInventory = (files, html, inputs) => {
const assertArtifactInventory = (files, inputs, contract) => {
const expectedRootFiles = new Set(['index.html', 'robots.txt', '_headers.example', '_redirects.example']);
if (inputs.site.environment === 'production') expectedRootFiles.add('sitemap.xml');
const referencedAssets = new Set(
[...html.matchAll(/(?:src|href)="\/assets\/([a-zA-Z0-9._-]+)"/gu)].map((match) => `assets/${match[1]}`),
);
for (const asset of inputs.manifest.assets) referencedAssets.add(`assets/${asset.fileName}`);
const expectedAssets = new Set(contract.files.map((record) => record.path));
const expectedPublicAssets = inputs.manifest.assets.map((asset) => `assets/${asset.fileName}`).sort();
if (contract.buildId !== inputs.site.buildId ||
JSON.stringify(contract.publicAssets) !== JSON.stringify(expectedPublicAssets)) {
throw new Error('Artifact contract does not match the validated production inputs.');
}
for (const file of files) {
if (file.endsWith('.map')) throw new Error(`Source map artifact is prohibited: ${file}`);
if (file.startsWith('assets/')) {
if (!referencedAssets.has(file)) throw new Error(`Unexpected artifact: ${file}`);
if (!expectedAssets.has(file)) throw new Error(`Unexpected artifact outside trusted contract: ${file}`);
} else if (!expectedRootFiles.has(file)) {
if (inputs.site.environment === 'preview' && file === 'sitemap.xml') {
throw new Error('Preview artifacts must not publish a sitemap.');
@@ -78,11 +83,48 @@ const assertArtifactInventory = (files, html, inputs) => {
throw new Error(`Unexpected artifact: ${file}`);
}
}
for (const required of [...expectedRootFiles, ...referencedAssets]) {
for (const required of [...expectedRootFiles, ...expectedAssets]) {
if (!files.includes(required)) throw new Error(`Missing required artifact: ${required}`);
}
};
const assertArtifactDigests = async (root, contract) => {
for (const record of contract.files) {
const digest = createHash('sha256').update(await readFile(resolve(root, record.path))).digest('hex');
if (digest !== record.sha256) throw new Error(`Artifact digest does not match trusted contract: ${record.path}`);
}
};
const sortedAttributeValues = (elements, attribute) =>
[...elements].map((element) => element.getAttribute(attribute)).filter((value) => value !== null).sort();
const assertTrustedHtmlAssets = (html, contract) => {
const window = new Window();
try {
window.document.write(html);
const scripts = [...window.document.querySelectorAll('script')];
const jsonLd = scripts.filter((script) => script.type === 'application/ld+json' && !script.hasAttribute('src'));
const snapshot = scripts.filter((script) =>
script.id === 'portfolio-inputs' && script.type === 'application/json' && !script.hasAttribute('src'));
const modules = scripts.filter((script) => script.type === 'module' && script.getAttribute('src') === `/${contract.entryModule}`);
if (scripts.length !== 3 || jsonLd.length !== 1 || snapshot.length !== 1 || modules.length !== 1) {
throw new Error('Unexpected executable script outside the exact JSON-LD, inert snapshot, and trusted client module set.');
}
const preloads = sortedAttributeValues(window.document.querySelectorAll('link[rel="modulepreload"]'), 'href');
const expectedPreloads = contract.modulePreloads.map((path) => `/${path}`).sort();
if (JSON.stringify(preloads) !== JSON.stringify(expectedPreloads)) {
throw new Error('Final HTML module preload set does not match the trusted contract.');
}
const styles = sortedAttributeValues(window.document.querySelectorAll('link[rel="stylesheet"]'), 'href');
const expectedStyles = contract.styles.map((path) => `/${path}`).sort();
if (JSON.stringify(styles) !== JSON.stringify(expectedStyles)) {
throw new Error('Final HTML style set does not match the trusted contract.');
}
} finally {
window.close();
}
};
const assertNoProhibitedSurfaces = async (root, files) => {
for (const file of files) {
const bytes = await readFile(resolve(root, file));
@@ -99,56 +141,110 @@ const exactlyOneMatch = (text, expression, label) => {
return matches[0];
};
const assertSemanticHtml = (html, inputs) => {
if (!/<html lang="en">/u.test(html)) throw new Error('Final HTML must declare lang=en.');
const h1 = exactlyOneMatch(html, /<h1\b[^>]*>([\s\S]*?)<\/h1>/gu, 'H1');
if (h1[1] !== escapeHtml(inputs.content.identity.name)) throw new Error('H1 identity does not match public content.');
const adjacentIdentity = `<h1>${escapeHtml(inputs.content.identity.name)}</h1><p>${escapeHtml(inputs.content.identity.role)}</p>`;
if (!html.includes(adjacentIdentity)) throw new Error('The canonical role must be immediately adjacent to the H1.');
const excludedSemanticContainers = new Set(['SCRIPT', 'STYLE', 'TEMPLATE', 'NOSCRIPT']);
for (const id of canonicalSectionIds) {
const sectionMatches = html.match(new RegExp(`<section id="${escapeRegularExpression(id)}"(?:\\s|>)`, 'gu')) ?? [];
if (sectionMatches.length !== 1) throw new Error(`Expected exactly one section: ${id}`);
const navigationMatches = html.match(new RegExp(`href="#${escapeRegularExpression(id)}"`, 'gu')) ?? [];
if (navigationMatches.length !== 1) throw new Error(`Expected one native navigation link for section: ${id}`);
const label = inputs.content.navigation.find((item) => item.id === id)?.label;
if (label === undefined || !html.includes(`<h2 id="${id}-heading">${escapeHtml(label.toUpperCase())}</h2>`)) {
throw new Error(`Missing canonical section heading: ${id}`);
const isVisibleSemanticNode = (node, appRoot) => {
if (!node.isConnected || !appRoot.contains(node)) return false;
let element = node.nodeType === 1 ? node : node.parentElement;
while (element !== null) {
if (excludedSemanticContainers.has(element.tagName) || element.hasAttribute('hidden') ||
element.hasAttribute('inert') || element.getAttribute('aria-hidden') === 'true' ||
element.style?.display === 'none' || element.style?.visibility === 'hidden') {
return false;
}
if (element === appRoot) return true;
element = element.parentElement;
}
return false;
};
const requiredVisibleCopy = [
inputs.content.systemsOperations.title,
inputs.content.infrastructure.title,
...inputs.content.infrastructure.bands.flatMap((band) => [band.title, band.summary]),
...inputs.content.projects.selected.flatMap((project) => [project.title, project.summary]),
...inputs.content.toolkit.flatMap((group) => [group.title, ...group.technologies.map((technology) => technology.name)]),
inputs.content.contact.email,
];
for (const copy of requiredVisibleCopy) {
if (!html.includes(escapeHtml(copy))) throw new Error(`Missing required semantic evidence: ${copy}`);
}
if (!html.includes(`href="mailto:${escapeHtml(inputs.content.contact.email)}"`)) {
throw new Error('Missing visible mailto contact link.');
}
const visibleText = (node, appRoot) => {
if (node.nodeType === 3) return isVisibleSemanticNode(node, appRoot) ? node.textContent ?? '' : '';
if (node.nodeType === 1 && !isVisibleSemanticNode(node, appRoot)) return '';
return [...node.childNodes].map((child) => visibleText(child, appRoot)).join('');
};
const moduleScript = exactlyOneMatch(html, /<script type="module"[^>]*src="\/assets\/[a-zA-Z0-9._-]+\.js"[^>]*><\/script>/gu, 'client module script');
const snapshot = exactlyOneMatch(
html,
/<script id="portfolio-inputs" type="application\/json">([\s\S]*?)<\/script>/gu,
'inert input snapshot',
);
let parsedSnapshot;
const assertSemanticHtml = (html, inputs, contract) => {
const window = new Window();
try {
parsedSnapshot = JSON.parse(snapshot[1]);
} catch {
throw new Error('The inert input snapshot is malformed.');
}
if (JSON.stringify(parsedSnapshot) !== JSON.stringify(inputs)) {
throw new Error('Hydration snapshot does not match the server input bytes and meaning.');
}
if (snapshot.index >= moduleScript.index || h1.index >= moduleScript.index) {
throw new Error('Semantic HTML and inert snapshot must precede the client module script.');
window.document.write(html);
const document = window.document;
if (document.documentElement.lang !== 'en') throw new Error('Final HTML must declare lang=en.');
const appRoots = [...document.querySelectorAll('#app')];
if (appRoots.length !== 1 || !isVisibleSemanticNode(appRoots[0], appRoots[0])) {
throw new Error('Expected one connected visible semantic #app root.');
}
const appRoot = appRoots[0];
const h1s = [...appRoot.querySelectorAll('h1')].filter((node) => isVisibleSemanticNode(node, appRoot));
if (h1s.length !== 1 || h1s[0].textContent !== inputs.content.identity.name) {
throw new Error('Expected exactly one visible H1 with the canonical identity.');
}
const role = h1s[0].nextElementSibling;
if (role === null || !isVisibleSemanticNode(role, appRoot) || role.textContent !== inputs.content.identity.role) {
throw new Error('Expected the visible canonical role immediately after the H1.');
}
const navigations = [...appRoot.querySelectorAll('nav[aria-label="Portfolio sections"]')]
.filter((node) => isVisibleSemanticNode(node, appRoot));
if (navigations.length !== 1) throw new Error('Expected exactly one visible navigation.');
const navigationLinks = [...navigations[0].querySelectorAll('a')]
.filter((node) => isVisibleSemanticNode(node, appRoot));
if (JSON.stringify(navigationLinks.map((link) => link.getAttribute('href'))) !==
JSON.stringify(canonicalSectionIds.map((id) => `#${id}`))) {
throw new Error('Visible navigation does not contain the exact canonical native links.');
}
const sections = [...appRoot.querySelectorAll('main > section')]
.filter((node) => isVisibleSemanticNode(node, appRoot));
if (JSON.stringify(sections.map((section) => section.id)) !== JSON.stringify(canonicalSectionIds)) {
throw new Error('Visible sections do not match the eight canonical sections.');
}
for (const section of sections) {
const label = inputs.content.navigation.find((item) => item.id === section.id)?.label;
const headings = [...section.querySelectorAll(':scope > h2')]
.filter((node) => isVisibleSemanticNode(node, appRoot));
if (label === undefined || headings.length !== 1 || headings[0].id !== `${section.id}-heading` ||
headings[0].textContent !== label.toUpperCase()) {
throw new Error(`Missing visible section heading: ${section.id}`);
}
}
const semanticText = visibleText(appRoot, appRoot);
const requiredVisibleCopy = [
inputs.content.systemsOperations.title,
inputs.content.infrastructure.title,
...inputs.content.infrastructure.bands.flatMap((band) => [band.title, band.summary]),
...inputs.content.projects.selected.flatMap((project) => [project.title, project.summary]),
...inputs.content.toolkit.flatMap((group) => [group.title, ...group.technologies.map((technology) => technology.name)]),
inputs.content.contact.email,
];
for (const copy of requiredVisibleCopy) {
if (!semanticText.includes(copy)) throw new Error(`Missing visible semantic evidence: ${copy}`);
}
const mailLinks = [...appRoot.querySelectorAll(`a[href="mailto:${inputs.content.contact.email}"]`)]
.filter((node) => isVisibleSemanticNode(node, appRoot));
if (mailLinks.length !== 1 || mailLinks[0].textContent !== inputs.content.contact.email) {
throw new Error('Missing visible semantic mailto contact link.');
}
const moduleScript = document.querySelector(`script[type="module"][src="/${contract.entryModule}"]`);
const snapshot = document.querySelector('script#portfolio-inputs[type="application/json"]');
if (moduleScript === null || snapshot === null ||
(appRoot.compareDocumentPosition(moduleScript) & window.Node.DOCUMENT_POSITION_FOLLOWING) === 0 ||
(snapshot.compareDocumentPosition(moduleScript) & window.Node.DOCUMENT_POSITION_FOLLOWING) === 0) {
throw new Error('Connected visible semantic content and snapshot must precede the trusted module script.');
}
let parsedSnapshot;
try {
parsedSnapshot = JSON.parse(snapshot.textContent ?? '');
} catch {
throw new Error('The inert input snapshot is malformed.');
}
if (JSON.stringify(parsedSnapshot) !== JSON.stringify(inputs)) {
throw new Error('Hydration snapshot does not match the server input bytes and meaning.');
}
} finally {
window.close();
}
};
@@ -256,13 +352,34 @@ const assertCrawlArtifacts = async (root, inputs, headers) => {
}
};
export const verifyArtifactDirectory = async (root, inputs) => {
const assertExactHostArtifacts = async (root, inputs) => {
const expected = createStaticHostArtifacts(
createPageMetadata(inputs.content, inputs.manifest, inputs.site),
inputs.site,
);
const actualHeaders = await readRequiredText(root, '_headers.example');
if (actualHeaders !== expected.headers) throw new Error('Headers must exactly equal the generated expected headers.');
const actualRobots = await readRequiredText(root, 'robots.txt');
if (actualRobots !== expected.robots) throw new Error('Robots must exactly equal the generated expected robots policy.');
const actualRedirects = await readRequiredText(root, '_redirects.example');
if (actualRedirects !== expected.redirects) throw new Error('Redirects must exactly equal the generated expected redirect example.');
if (expected.sitemap !== undefined) {
const actualSitemap = await readRequiredText(root, 'sitemap.xml');
if (actualSitemap !== expected.sitemap) throw new Error('Sitemap must exactly equal the generated expected sitemap.');
}
};
export const verifyArtifactDirectory = async (root, inputs, contractInput) => {
const contract = parseArtifactContract(contractInput);
const html = await readRequiredText(root, 'index.html');
const files = await listArtifacts(root);
assertArtifactInventory(files, html, inputs);
assertArtifactInventory(files, inputs, contract);
await assertArtifactDigests(root, contract);
await assertNoProhibitedSurfaces(root, files);
assertSemanticHtml(html, inputs);
assertTrustedHtmlAssets(html, contract);
assertSemanticHtml(html, inputs, contract);
const serializedJsonLd = assertMetadata(html, inputs);
await assertExactHostArtifacts(root, inputs);
const headers = await readRequiredText(root, '_headers.example');
assertSecurityHeaders(headers, inputs, serializedJsonLd);
await assertCrawlArtifacts(root, inputs, headers);
@@ -284,7 +401,14 @@ export const verifyArtifacts = async (configPath) => {
preparedAssetRoot: pathToFileURL(`${resolve(repositoryRoot, 'inputs/prepared-assets')}${sep}`),
siteConfigJson: pathToFileURL(configAbsolutePath),
});
await verifyArtifactDirectory(resolve(repositoryRoot, 'dist'), inputs);
const authoritySource = await readRequiredText(resolve(repositoryRoot, 'dist-ssr'), 'artifact-authority.json');
let authority;
try {
authority = parseBuildAuthority(JSON.parse(authoritySource));
} catch (error) {
throw new Error('Persisted build authority is invalid.', { cause: error });
}
await verifyArtifactDirectory(resolve(repositoryRoot, 'dist'), inputs, authority.artifactContract);
};
if (process.argv[1] !== undefined && resolve(process.argv[1]) === scriptPath) {

View File

@@ -160,6 +160,9 @@ export const createStaticHostArtifacts = (
'',
].join('\n'),
headers: [
'/assets/*',
' Cache-Control: public, max-age=31536000, immutable',
'',
'/*',
...commonHeaders(metadata),
' Cache-Control: public, max-age=0, must-revalidate',

View File

@@ -0,0 +1,19 @@
import { parseProductionInputSnapshot } from './schemas';
import type { ProductionInputs } from './types';
const deepFreeze = <T>(value: T, seen = new WeakSet<object>()): Readonly<T> => {
if (value !== null && typeof value === 'object') {
const object = value as object;
if (!seen.has(object)) {
seen.add(object);
for (const key of Reflect.ownKeys(object)) {
deepFreeze(Reflect.get(object, key), seen);
}
Object.freeze(object);
}
}
return value;
};
export const validateAndFreezeProductionInputSnapshot = (input: unknown): Readonly<ProductionInputs> =>
deepFreeze(parseProductionInputSnapshot(input));

View File

@@ -225,15 +225,14 @@ export const parsePortfolioContent = (input: unknown): PortfolioContent => portf
export const parsePublicAssetManifest = (input: unknown): PublicAssetManifest => publicAssetManifestSchema.parse(input) as PublicAssetManifest;
export const parseSiteConfig = (input: unknown): SiteConfig => siteConfigSchema.parse(input) as SiteConfig;
export const validateProductionInputs = (
contentInput: unknown,
manifestInput: unknown,
siteInput: unknown,
_preparedAssetRoot: URL,
): ProductionInputs => {
const content = parsePortfolioContent(contentInput);
const manifest = parsePublicAssetManifest(manifestInput);
const site = parseSiteConfig(siteInput);
const productionInputsSchema = z.object({
content: portfolioContentSchema,
manifest: publicAssetManifestSchema,
site: siteConfigSchema,
}).strict();
export const parseProductionInputSnapshot = (input: unknown): ProductionInputs => {
const { content, manifest, site } = productionInputsSchema.parse(input);
const manifestIds = new Set(manifest.assets.map((asset) => asset.id));
const referencedAssetIds = [
...content.projects.selected.flatMap((project) => project.assetIds),
@@ -247,5 +246,16 @@ export const validateProductionInputs = (
throw new Error(`Unmanifested asset reference: ${assetId}`);
}
}
return { content, manifest, site };
return { content, manifest, site } as ProductionInputs;
};
export const validateProductionInputs = (
contentInput: unknown,
manifestInput: unknown,
siteInput: unknown,
_preparedAssetRoot: URL,
): ProductionInputs => parseProductionInputSnapshot({
content: contentInput,
manifest: manifestInput,
site: siteInput,
});

View File

@@ -1,4 +1,5 @@
import { createApp } from './app';
import { validateAndFreezeProductionInputSnapshot } from './content/production-input-snapshot';
import type { ProductionInputs } from './content/types';
export const readProductionInputsSnapshot = (source: Document): Readonly<ProductionInputs> => {
@@ -16,7 +17,7 @@ export const readProductionInputsSnapshot = (source: Document): Readonly<Product
throw new Error('The inert portfolio input snapshot is empty.');
}
return JSON.parse(serializedInputs) as Readonly<ProductionInputs>;
return validateAndFreezeProductionInputSnapshot(JSON.parse(serializedInputs) as unknown);
};
const appRoot = document.getElementById('app');

View File

@@ -1,23 +1,17 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { afterEach, describe, expect, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import { loadProductionInputs } from '@/content/load-production-inputs';
import { readProductionInputsSnapshot } from '@/entry-client';
import { render } from '@/entry-server';
import {
assemblePrerenderedHtml,
assertExpectedDistInputs,
parseExactConfigArgument,
type ViteManifest,
} from '../../scripts/prerender.mjs';
const repositoryRoot = process.cwd();
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
});
const locations = {
contentJson: pathToFileURL(resolve(repositoryRoot, 'content/public/portfolio-content.json')),
assetManifestJson: pathToFileURL(resolve(repositoryRoot, 'content/public/asset-manifest.json')),
@@ -138,26 +132,3 @@ describe('approved build template', () => {
expect(template).not.toContain('window.__PORTFOLIO_INPUTS__');
});
});
describe('deterministic staging inventory', () => {
it.each([
{ location: 'client/unknown.txt' },
{ location: 'assets/unknown.txt' },
])('rejects an unknown staged output at $location', async ({ location }) => {
const directory = await mkdtemp(join(tmpdir(), 'portfolio-dist-'));
temporaryDirectories.push(directory);
await mkdir(join(directory, 'client/.vite'), { recursive: true });
await mkdir(join(directory, 'client/assets'), { recursive: true });
await mkdir(join(directory, 'assets'), { recursive: true });
await writeFile(join(directory, 'client/index.html'), builtTemplate);
await writeFile(join(directory, 'client/.vite/manifest.json'), JSON.stringify(manifest));
await writeFile(join(directory, 'client/assets/index-test.js'), 'export {};');
await writeFile(join(directory, 'client/assets/index-test.css'), 'body {}');
await writeFile(join(directory, 'index.html'), builtTemplate);
await writeFile(join(directory, 'assets/index-test.js'), 'export {};');
await writeFile(join(directory, 'assets/index-test.css'), 'body {}');
await writeFile(join(directory, location), 'unknown');
await expect(assertExpectedDistInputs(directory)).rejects.toThrow(/unexpected build output/i);
});
});

View File

@@ -0,0 +1,139 @@
import { mkdtemp, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
captureDirectoryIdentity,
publishStagedDirectory,
} from '../../scripts/safe-publication.mjs';
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
});
const repository = async () => {
const root = await mkdtemp(join(tmpdir(), 'portfolio-publication-'));
temporaryDirectories.push(root);
await mkdir(join(root, 'dist'));
await mkdir(join(root, '.dist-stage-test'));
await writeFile(join(root, 'dist/old.txt'), 'old');
await writeFile(join(root, '.dist-stage-test/new.txt'), 'new');
return {
root,
finalRoot: join(root, 'dist'),
stagingRoot: join(root, '.dist-stage-test'),
};
};
describe('safe artifact publication', () => {
it('rejects a symlinked final output before mutating its target', async () => {
const { root, finalRoot, stagingRoot } = await repository();
const outside = await mkdtemp(join(tmpdir(), 'portfolio-publication-outside-'));
temporaryDirectories.push(outside);
await writeFile(join(outside, 'protected.txt'), 'protected');
await rm(finalRoot, { recursive: true });
await symlink(outside, finalRoot);
await expect(publishStagedDirectory({
repositoryRoot: root,
finalRoot,
stagingRoot,
expectedStagingIdentity: await captureDirectoryIdentity(stagingRoot),
})).rejects.toThrow(/symlink|safe directory/i);
expect(await readFile(join(outside, 'protected.txt'), 'utf8')).toBe('protected');
});
it('rejects an output path outside the repository', async () => {
const { root, stagingRoot } = await repository();
const outside = await mkdtemp(join(tmpdir(), 'portfolio-publication-outside-'));
temporaryDirectories.push(outside);
await expect(publishStagedDirectory({
repositoryRoot: root,
finalRoot: outside,
stagingRoot,
expectedStagingIdentity: await captureDirectoryIdentity(stagingRoot),
})).rejects.toThrow(/outside.*repository/i);
});
it('rejects a repository path with a symlinked ancestor', async () => {
const { root, stagingRoot } = await repository();
const parent = await mkdtemp(join(tmpdir(), 'portfolio-publication-link-'));
temporaryDirectories.push(parent);
const linkedRoot = join(parent, 'linked-root');
await symlink(root, linkedRoot);
await expect(publishStagedDirectory({
repositoryRoot: linkedRoot,
finalRoot: join(linkedRoot, 'dist'),
stagingRoot,
expectedStagingIdentity: await captureDirectoryIdentity(stagingRoot),
})).rejects.toThrow(/ancestor|canonical|symlink/i);
});
it('rejects a staging directory replaced after verification without touching the final output', async () => {
const { root, finalRoot, stagingRoot } = await repository();
const expectedStagingIdentity = await captureDirectoryIdentity(stagingRoot);
await rm(stagingRoot, { recursive: true });
await mkdir(stagingRoot);
await writeFile(join(stagingRoot, 'attacker.txt'), 'attacker');
await expect(publishStagedDirectory({ repositoryRoot: root, finalRoot, stagingRoot, expectedStagingIdentity }))
.rejects.toThrow(/identity changed/i);
expect(await readFile(join(finalRoot, 'old.txt'), 'utf8')).toBe('old');
});
it('rolls back the previous output when the staged rename fails mid-publication', async () => {
const { root, finalRoot, stagingRoot } = await repository();
let renameCount = 0;
const failSecondRename = async (from: string, to: string) => {
renameCount += 1;
if (renameCount === 2) throw new Error('injected staged rename failure');
await rename(from, to);
};
await expect(publishStagedDirectory({
repositoryRoot: root,
finalRoot,
stagingRoot,
expectedStagingIdentity: await captureDirectoryIdentity(stagingRoot),
renameDirectory: failSecondRename,
})).rejects.toThrow(/injected staged rename failure/i);
expect(await readFile(join(finalRoot, 'old.txt'), 'utf8')).toBe('old');
await expect(readFile(join(finalRoot, 'new.txt'), 'utf8')).rejects.toThrow();
expect((await readdir(root)).some((name) => name.startsWith('.dist-backup-'))).toBe(false);
});
it('rolls back the previous output when the pre-commit authority update fails', async () => {
const { root, finalRoot, stagingRoot } = await repository();
await expect(publishStagedDirectory({
repositoryRoot: root,
finalRoot,
stagingRoot,
expectedStagingIdentity: await captureDirectoryIdentity(stagingRoot),
beforeCommit: async () => {
throw new Error('injected authority update failure');
},
})).rejects.toThrow(/injected authority update failure/i);
expect(await readFile(join(finalRoot, 'old.txt'), 'utf8')).toBe('old');
await expect(readFile(join(finalRoot, 'new.txt'), 'utf8')).rejects.toThrow();
expect((await readdir(root)).some((name) => name.startsWith('.dist-backup-'))).toBe(false);
});
it('publishes the verified staging directory and removes the superseded output', async () => {
const { root, finalRoot, stagingRoot } = await repository();
await publishStagedDirectory({
repositoryRoot: root,
finalRoot,
stagingRoot,
expectedStagingIdentity: await captureDirectoryIdentity(stagingRoot),
});
expect(await readFile(join(finalRoot, 'new.txt'), 'utf8')).toBe('new');
await expect(readFile(join(finalRoot, 'old.txt'), 'utf8')).rejects.toThrow();
expect((await readdir(root)).some((name) => name.startsWith('.dist-backup-'))).toBe(false);
});
});

View File

@@ -1,3 +1,4 @@
import { createHash } from 'node:crypto';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
@@ -31,6 +32,9 @@ const template = `<!doctype html><html lang="en"><head><meta charset="UTF-8" />
<script type="module" crossorigin src="/assets/index-test.js"></script>
<link rel="stylesheet" href="/assets/index-test.css"></head><body><div id="app"></div>
<!--portfolio-body--></body></html>`;
const clientJavaScript = 'export {};\n';
const clientStylesheet = 'body { margin: 0; }\n';
const sha256 = (value: string): string => createHash('sha256').update(value).digest('hex');
const temporaryDirectories: string[] = [];
afterEach(async () => {
@@ -45,25 +49,39 @@ const writeValidArtifacts = async (site?: SiteConfig) => {
await mkdir(join(directory, 'assets'));
const html = assemblePrerenderedHtml(template, manifest, await render('/', inputs), inputs);
const hostArtifacts = createStaticHostArtifacts(createPageMetadata(inputs.content, inputs.manifest, inputs.site), inputs.site);
const contract = {
schema: 'portfolio-artifact-contract/v1',
buildId: inputs.site.buildId,
entryModule: 'assets/index-test.js',
executableModules: ['assets/index-test.js'],
modulePreloads: ['assets/index-test.js'],
styles: ['assets/index-test.css'],
viteAssets: [],
publicAssets: [],
files: [
{ path: 'assets/index-test.css', sha256: sha256(clientStylesheet) },
{ path: 'assets/index-test.js', sha256: sha256(clientJavaScript) },
],
} as const;
await Promise.all([
writeFile(join(directory, 'index.html'), html),
writeFile(join(directory, 'robots.txt'), hostArtifacts.robots),
writeFile(join(directory, '_headers.example'), hostArtifacts.headers),
writeFile(join(directory, '_redirects.example'), hostArtifacts.redirects),
writeFile(join(directory, 'assets/index-test.js'), 'export {};\n'),
writeFile(join(directory, 'assets/index-test.css'), 'body { margin: 0; }\n'),
writeFile(join(directory, 'assets/index-test.js'), clientJavaScript),
writeFile(join(directory, 'assets/index-test.css'), clientStylesheet),
...(hostArtifacts.sitemap === undefined
? []
: [writeFile(join(directory, 'sitemap.xml'), hostArtifacts.sitemap)]),
]);
return { directory, html, inputs };
return { directory, html, inputs, contract };
};
describe('artifact verification', () => {
it('accepts the complete private preview artifact set', async () => {
const { directory, inputs } = await writeValidArtifacts();
const { directory, inputs, contract } = await writeValidArtifacts();
await expect(verifyArtifactDirectory(directory, inputs)).resolves.toBeUndefined();
await expect(verifyArtifactDirectory(directory, inputs, contract)).resolves.toBeUndefined();
});
it('accepts the pure production artifact path with canonical sitemap and allow policy', async () => {
@@ -72,9 +90,9 @@ describe('artifact verification', () => {
canonicalUrl: 'https://portfolio.example.test/',
buildId: 'production-test',
};
const { directory, inputs } = await writeValidArtifacts(productionSite);
const { directory, inputs, contract } = await writeValidArtifacts(productionSite);
await expect(verifyArtifactDirectory(directory, inputs)).resolves.toBeUndefined();
await expect(verifyArtifactDirectory(directory, inputs, contract)).resolves.toBeUndefined();
});
it.each([
@@ -96,13 +114,13 @@ describe('artifact verification', () => {
{
name: 'a prohibited private surface',
mutate: async (directory: string) => writeFile(join(directory, 'assets/index-test.js'), 'const evidenceId = "fixture";'),
error: /prohibited surface/i,
error: /prohibited surface|digest does not match/i,
},
])('rejects $name', async ({ mutate, error }) => {
const { directory, inputs } = await writeValidArtifacts();
const { directory, inputs, contract } = await writeValidArtifacts();
await mutate(directory);
await expect(verifyArtifactDirectory(directory, inputs)).rejects.toThrow(error);
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(error);
});
it.each([
@@ -114,7 +132,7 @@ describe('artifact verification', () => {
{
name: 'duplicate H1 identity',
mutate: (html: string) => html.replace('</main>', '<h1>Luca Pîslaru</h1></main>'),
error: /exactly one h1/i,
error: /exactly one (?:visible )?h1/i,
},
{
name: 'malformed JSON-LD',
@@ -124,7 +142,7 @@ describe('artifact verification', () => {
{
name: 'missing infrastructure evidence',
mutate: (html: string) => html.replace('id="infrastructure"', 'id="removed-infrastructure"'),
error: /section.*infrastructure/i,
error: /section.*infrastructure|visible sections/i,
},
{
name: 'executable snapshot assignment',
@@ -132,25 +150,145 @@ describe('artifact verification', () => {
error: /inert input snapshot|prohibited surface/i,
},
])('rejects raw HTML with $name', async ({ mutate, error }) => {
const { directory, html, inputs } = await writeValidArtifacts();
const { directory, html, inputs, contract } = await writeValidArtifacts();
await writeFile(join(directory, 'index.html'), mutate(html));
await expect(verifyArtifactDirectory(directory, inputs)).rejects.toThrow(error);
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(error);
});
it('rejects a missing crawl artifact', async () => {
const { directory, inputs } = await writeValidArtifacts();
const { directory, inputs, contract } = await writeValidArtifacts();
await rm(join(directory, 'robots.txt'));
await expect(verifyArtifactDirectory(directory, inputs)).rejects.toThrow(/robots/i);
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/robots/i);
});
it('checks the CSP hash against the actual serialized JSON-LD bytes', async () => {
const { directory, inputs } = await writeValidArtifacts();
const { directory, inputs, contract } = await writeValidArtifacts();
const headersPath = join(directory, '_headers.example');
const headers = await readFile(headersPath, 'utf8');
await writeFile(headersPath, headers.replace(/sha256-[A-Za-z0-9+/=]+/u, 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='));
await expect(verifyArtifactDirectory(directory, inputs)).rejects.toThrow(/csp.*json-ld|json-ld.*csp/i);
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/csp.*json-ld|json-ld.*csp|exact.*headers/i);
});
it.each([
{
name: 'a final-HTML-authorized substitute module',
mutate: async (directory: string, html: string) => {
await rm(join(directory, 'assets/index-test.js'));
await writeFile(join(directory, 'assets/substitute.js'), clientJavaScript);
return html.replaceAll('assets/index-test.js', 'assets/substitute.js');
},
error: /trusted.*module|artifact contract|unexpected artifact/i,
},
{
name: 'an extra inline executable script',
mutate: async (_directory: string, html: string) => html.replace('</body>', '<script>void 0</script></body>'),
error: /unexpected executable script/i,
},
{
name: 'an extra external executable script',
mutate: async (_directory: string, html: string) => html.replace('</body>', '<script src="https://example.test/evil.js"></script></body>'),
error: /unexpected executable script|external executable/i,
},
{
name: 'a final-HTML-authorized extra stylesheet',
mutate: async (directory: string, html: string) => {
await writeFile(join(directory, 'assets/extra.css'), 'body {}');
return html.replace('</head>', '<link rel="stylesheet" href="/assets/extra.css" /></head>');
},
error: /trusted.*style|artifact contract|unexpected artifact/i,
},
{
name: 'a missing trusted module preload',
mutate: async (_directory: string, html: string) => html.replace(/<link rel="modulepreload"[^>]+>\n?/u, ''),
error: /trusted.*preload|module preload/i,
},
])('rejects $name instead of trusting final HTML', async ({ mutate, error }) => {
const { directory, html, inputs, contract } = await writeValidArtifacts();
await writeFile(join(directory, 'index.html'), await mutate(directory, html));
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(error);
});
it.each([
{
name: 'an appended CSP directive',
mutate: (headers: string) => `${headers} script-src https://example.test\n`,
},
{
name: 'an external script-src relaxation',
mutate: (headers: string) => headers.replace("script-src 'self'", "script-src 'self' https://example.test"),
},
{
name: 'a duplicate partial CSP policy',
mutate: (headers: string) => `${headers} Content-Security-Policy: default-src 'self'\n`,
},
{
name: 'commented header impostors',
mutate: (headers: string) => headers.split('\n').map((line) => `# ${line}`).join('\n'),
},
])('rejects $name by exact header bytes', async ({ mutate }) => {
const { directory, inputs, contract } = await writeValidArtifacts();
const path = join(directory, '_headers.example');
await writeFile(path, mutate(await readFile(path, 'utf8')));
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/exact.*headers|headers.*exact/i);
});
it('rejects modified redirect example bytes', async () => {
const { directory, inputs, contract } = await writeValidArtifacts();
const path = join(directory, '_redirects.example');
await writeFile(path, `${await readFile(path, 'utf8')}# appended\n`);
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/exact.*redirect|redirect.*exact/i);
});
it('rejects modified production sitemap bytes', async () => {
const productionSite: SiteConfig = {
environment: 'production',
canonicalUrl: 'https://portfolio.example.test/',
buildId: 'production-test',
};
const { directory, inputs, contract } = await writeValidArtifacts(productionSite);
const path = join(directory, 'sitemap.xml');
await writeFile(path, `${await readFile(path, 'utf8')}<!-- appended -->\n`);
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/exact.*sitemap|sitemap.*exact/i);
});
it.each([
{
name: 'a hidden application root',
mutate: (html: string) => html.replace('<div id="app">', '<div id="app" hidden>'),
},
{
name: 'identity rendered only inside a template',
mutate: (html: string) => html.replace(
'<h1>Luca Pîslaru</h1><p>System Administrator &amp; Full-Stack Developer</p>',
'<template><h1>Luca Pîslaru</h1><p>System Administrator &amp; Full-Stack Developer</p></template>',
),
},
{
name: 'an aria-hidden native navigation',
mutate: (html: string) => html.replace('<nav class="site-navigation"', '<nav aria-hidden="true" class="site-navigation"'),
},
{
name: 'an inert canonical section',
mutate: (html: string) => html.replace('<section id="infrastructure"', '<section id="infrastructure" inert'),
},
{
name: 'infrastructure evidence present only in hydration JSON',
mutate: (html: string) => {
const snapshotAt = html.indexOf('<script id="portfolio-inputs"');
return `${html.slice(0, snapshotAt).replaceAll('Secure self-hosted platform and lifecycle', 'Removed evidence')}${html.slice(snapshotAt)}`;
},
},
])('rejects $name as non-visible semantic evidence', async ({ mutate }) => {
const { directory, html, inputs, contract } = await writeValidArtifacts();
await writeFile(join(directory, 'index.html'), mutate(html));
await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(/visible semantic|visible h1|visible navigation|visible section|connected/i);
});
});

View File

@@ -0,0 +1,92 @@
import { describe, expect, it } from 'vitest';
import { resolveViteAssetClosure } from '../../scripts/build-contract.mjs';
import type { ViteManifest } from '../../scripts/prerender.mjs';
const recursiveManifest = (): ViteManifest => ({
'index.html': {
file: 'assets/entry-a1.js',
src: 'index.html',
isEntry: true,
css: ['assets/entry-a1.css'],
assets: ['assets/entry-logo-a1.webp'],
imports: ['src/shared.ts'],
dynamicImports: ['src/lazy.ts'],
},
'src/shared.ts': {
file: 'assets/shared-b2.js',
src: 'src/shared.ts',
css: ['assets/shared-b2.css'],
imports: ['src/deep.ts'],
},
'src/deep.ts': {
file: 'assets/deep-c3.js',
src: 'src/deep.ts',
},
'src/lazy.ts': {
file: 'assets/lazy-d4.js',
src: 'src/lazy.ts',
css: ['assets/lazy-d4.css'],
},
});
describe('trusted Vite asset closure', () => {
it('resolves the exact recursive executable, preload, style, and Vite asset sets', () => {
expect(resolveViteAssetClosure(recursiveManifest())).toEqual({
entryModule: 'assets/entry-a1.js',
executableModules: [
'assets/deep-c3.js',
'assets/entry-a1.js',
'assets/lazy-d4.js',
'assets/shared-b2.js',
],
modulePreloads: [
'assets/deep-c3.js',
'assets/entry-a1.js',
'assets/shared-b2.js',
],
styles: [
'assets/entry-a1.css',
'assets/lazy-d4.css',
'assets/shared-b2.css',
],
viteAssets: ['assets/entry-logo-a1.webp'],
});
});
it.each([
{
name: 'a missing exact index.html entry',
mutate: (manifest: ViteManifest) => {
manifest['src/entry-client.ts'] = manifest['index.html']!;
delete manifest['index.html'];
},
error: /exact index\.html/i,
},
{
name: 'an unreachable manifest record',
mutate: (manifest: ViteManifest) => {
manifest['src/orphan.ts'] = { file: 'assets/orphan.js', src: 'src/orphan.ts' };
},
error: /unreachable manifest/i,
},
{
name: 'an unsafe emitted path',
mutate: (manifest: ViteManifest) => {
manifest['src/shared.ts']!.file = '../outside.js';
},
error: /unsafe.*path/i,
},
{
name: 'a missing recursive import',
mutate: (manifest: ViteManifest) => {
manifest['src/shared.ts']!.imports = ['src/missing.ts'];
},
error: /missing manifest import/i,
},
])('rejects $name', ({ mutate, error }) => {
const manifest = recursiveManifest();
mutate(manifest);
expect(() => resolveViteAssetClosure(manifest)).toThrow(error);
});
});

View File

@@ -0,0 +1,46 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { readProductionInputsSnapshot } from '@/entry-client';
import { serializeJsonLd } from '@/security/serialize-json-ld';
import type { ProductionInputs } from '@/content/types';
const validInputs = (): ProductionInputs => ({
content: JSON.parse(readFileSync(resolve(process.cwd(), 'content/public/portfolio-content.json'), 'utf8')),
manifest: JSON.parse(readFileSync(resolve(process.cwd(), 'content/public/asset-manifest.json'), 'utf8')),
site: JSON.parse(readFileSync(resolve(process.cwd(), 'config/site.preview.json'), 'utf8')),
}) as ProductionInputs;
const snapshotDocument = (value: unknown): Document => new DOMParser().parseFromString(
`<script id="portfolio-inputs" type="application/json">${serializeJsonLd(value)}</script>`,
'text/html',
);
describe('client production-input snapshot', () => {
it('schema-validates and recursively freezes the complete browser snapshot', () => {
const inputs = readProductionInputsSnapshot(snapshotDocument(validInputs()));
expect(Object.isFrozen(inputs)).toBe(true);
expect(Object.isFrozen(inputs.content.projects.selected)).toBe(true);
expect(Object.isFrozen(inputs.content.projects.selected[0]?.technicalDecisions)).toBe(true);
expect(Object.isFrozen(inputs.manifest.assets)).toBe(true);
expect(Object.isFrozen(inputs.site)).toBe(true);
expect(() => {
(inputs.content.identity as { role: string }).role = 'mutated';
}).toThrow();
});
it('rejects JSON that does not satisfy the production-input schema', () => {
const invalid = validInputs() as ProductionInputs & { unexpected?: string };
invalid.unexpected = 'not allowed';
expect(() => readProductionInputsSnapshot(snapshotDocument(invalid))).toThrow(/unrecognized|invalid|expected/i);
});
it('rejects a supplied asset reference absent from the supplied public manifest', () => {
const invalid = validInputs();
invalid.content.projects.selected[0]!.assetIds = ['unmanifested-snapshot-asset'];
expect(() => readProductionInputsSnapshot(snapshotDocument(invalid))).toThrow(/unmanifested asset reference/i);
});
});

View File

@@ -95,7 +95,8 @@ describe('environment-aware crawl and response artifacts', () => {
});
it('generates production-only sitemap, allow policy, HSTS, and no preview directives', () => {
const artifacts = createStaticHostArtifacts(createPageMetadata(content, manifest, productionSite), productionSite);
const metadata = createPageMetadata(content, manifest, productionSite);
const artifacts = createStaticHostArtifacts(metadata, productionSite);
expect(artifacts.robots).toBe(
'User-agent: *\nAllow: /\nSitemap: https://portfolio.example.test/sitemap.xml\n',
@@ -104,6 +105,20 @@ describe('environment-aware crawl and response artifacts', () => {
expect(artifacts.headers).toContain('Strict-Transport-Security: max-age=31536000');
expect(artifacts.headers).not.toContain('X-Robots-Tag');
expect(artifacts.headers).not.toContain('private, no-store');
expect(artifacts.headers).toBe([
'/assets/*',
' Cache-Control: public, max-age=31536000, immutable',
'',
'/*',
` Content-Security-Policy: default-src 'self'; script-src 'self' '${metadata.jsonLdCspHash}'; style-src 'self'; img-src 'self'; media-src 'self'; font-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'; upgrade-insecure-requests`,
' X-Content-Type-Options: nosniff',
' Referrer-Policy: no-referrer',
' Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()',
' X-Frame-Options: DENY',
' Cache-Control: public, max-age=0, must-revalidate',
' Strict-Transport-Security: max-age=31536000',
'',
].join('\n'));
});
it('emits the least-privilege browser security baseline', () => {