Files
Portfolio/scripts/verify-artifacts.mjs

430 lines
21 KiB
JavaScript

import { createHash } from 'node:crypto';
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 { 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), '..');
const canonicalSectionIds = [
'start', 'profile', 'experience', 'infrastructure', 'selected-builds', 'additional-work', 'toolkit', 'contact',
];
const prohibitedSurfaces = [
/__PORTFOLIO_INPUTS__/iu,
/__PORTFOLIO_TEST_HOOKS__/iu,
/__PORTFOLIO_PERFORMANCE__/iu,
/__portfolio_performance/iu,
/PORTFOLIO_TEST_TOKEN/iu,
/__PORTFOLIO_(?:TEST|INSTRUMENTATION)__/iu,
/__portfolio(?:Journey)?Instrumentation/iu,
/(?:evidenceId|sourcePath|sourceFingerprint|approvalOwner|privacyClassification|denylist|originalFilename)/iu,
/sourceMappingURL/iu,
];
const escapeRegularExpression = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
const escapeHtml = (value) => value
.replace(/&/gu, '&')
.replace(/</gu, '&lt;')
.replace(/>/gu, '&gt;')
.replace(/"/gu, '&quot;');
const readRequiredText = async (root, name) => {
try {
return await readFile(resolve(root, name), 'utf8');
} catch (error) {
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
throw new Error(`Missing required artifact: ${name}`);
}
throw error;
}
};
const listArtifacts = async (root) => {
const rootStats = await lstat(root);
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
throw new Error('Artifact root must be a non-symlink directory.');
}
const files = [];
for (const entry of await readdir(root, { withFileTypes: true })) {
const entryPath = resolve(root, entry.name);
if (entry.isSymbolicLink()) throw new Error(`Unexpected linked artifact: ${entry.name}`);
if (entry.isDirectory()) {
if (entry.name !== 'assets') throw new Error(`Unexpected artifact directory: ${entry.name}`);
for (const asset of await readdir(entryPath, { withFileTypes: true })) {
if (asset.isSymbolicLink() || !asset.isFile()) {
throw new Error(`Unexpected linked or nested artifact: assets/${asset.name}`);
}
files.push(`assets/${asset.name}`);
}
continue;
}
if (!entry.isFile()) throw new Error(`Unexpected special artifact: ${entry.name}`);
files.push(entry.name);
}
return files.sort();
};
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 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 (!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.');
}
throw new Error(`Unexpected artifact: ${file}`);
}
}
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));
const text = bytes.toString('utf8');
for (const pattern of prohibitedSurfaces) {
if (pattern.test(text)) throw new Error(`Prohibited surface detected in ${file}.`);
}
}
};
const exactlyOneMatch = (text, expression, label) => {
const matches = [...text.matchAll(expression)];
if (matches.length !== 1) throw new Error(`Expected exactly one ${label}.`);
return matches[0];
};
const excludedSemanticContainers = new Set(['SCRIPT', 'STYLE', 'TEMPLATE', 'NOSCRIPT']);
const isVisibleSemanticNode = (node, appRoot) => {
if (!node.isConnected || !appRoot.contains(node)) return false;
const originElement = node.nodeType === 1 ? node : node.parentElement;
let element = originElement;
while (element !== null) {
if (excludedSemanticContainers.has(element.tagName) || element.hasAttribute('hidden') ||
element.hasAttribute('inert') || element.getAttribute('aria-hidden')?.toLowerCase() === 'true' ||
(element.tagName === 'DIALOG' && !element.hasAttribute('open')) ||
element.style?.display === 'none' || element.style?.visibility === 'hidden') {
return false;
}
if (element.tagName === 'DETAILS' && !element.hasAttribute('open') && originElement !== element) {
const summary = [...element.children].find((child) => child.tagName === 'SUMMARY');
if (summary === undefined || (originElement !== summary && !summary.contains(originElement))) return false;
}
if (element === appRoot) return true;
element = element.parentElement;
}
return false;
};
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 assertSemanticHtml = (html, inputs, contract) => {
const window = new Window();
try {
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();
}
};
const assertMetadata = (html, inputs) => {
const expectedTitle = escapeHtml(inputs.content.metadata.title);
const title = exactlyOneMatch(html, /<title>([\s\S]*?)<\/title>/gu, 'page title');
if (title[1] !== expectedTitle) throw new Error('Page title does not match public content.');
const canonical = exactlyOneMatch(html, /<link rel="canonical" href="([^"]+)" \/>/gu, 'canonical link');
if (canonical[1] !== inputs.site.canonicalUrl) throw new Error('Canonical URL does not match site config.');
const description = exactlyOneMatch(html, /<meta name="description" content="([^"]+)" \/>/gu, 'meta description');
if (description[1] !== escapeHtml(inputs.content.metadata.description)) {
throw new Error('Meta description does not match public content.');
}
for (const property of ['og:type', 'og:title', 'og:description', 'og:url']) {
exactlyOneMatch(html, new RegExp(`<meta property="${property}" content="[^"]+" \\/>`, 'gu'), `${property} metadata`);
}
const ogImages = [...html.matchAll(/<meta property="og:image"/gu)];
if (inputs.content.metadata.socialPreviewAssetId === undefined && ogImages.length !== 0) {
throw new Error('Open Graph image must be omitted without an approved manifest asset.');
}
const jsonLdMatch = exactlyOneMatch(
html,
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/gu,
'JSON-LD block',
);
let jsonLd;
try {
jsonLd = JSON.parse(jsonLdMatch[1]);
} catch {
throw new Error('JSON-LD is malformed.');
}
if (jsonLd['@type'] !== 'ProfilePage' || jsonLd.mainEntity?.['@type'] !== 'Person') {
throw new Error('JSON-LD must contain one ProfilePage with one nested Person.');
}
const person = jsonLd.mainEntity;
if (
person.name !== inputs.content.identity.name ||
person.jobTitle !== inputs.content.identity.role ||
person.url !== inputs.site.canonicalUrl ||
jsonLd.url !== inputs.site.canonicalUrl
) {
throw new Error('JSON-LD identity or canonical URL is inconsistent.');
}
const expectedSameAs = inputs.content.contact.social.map((link) => link.url);
if (JSON.stringify(person.sameAs ?? []) !== JSON.stringify(expectedSameAs)) {
throw new Error('JSON-LD sameAs contains an unverified or missing profile.');
}
if (jsonLdMatch[1].toLowerCase().includes('</script')) throw new Error('JSON-LD contains a script-closing hazard.');
return jsonLdMatch[1];
};
const assertSecurityHeaders = (headers, inputs, serializedJsonLd) => {
const required = [
"default-src 'self'", "script-src 'self'", "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', 'X-Frame-Options: DENY',
'Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()',
];
for (const value of required) {
if (!headers.includes(value)) throw new Error(`Missing security header policy: ${value}`);
}
const expectedHash = `sha256-${createHash('sha256').update(serializedJsonLd).digest('base64')}`;
const hashes = headers.match(/sha256-[A-Za-z0-9+/=]+/gu) ?? [];
if (hashes.length !== 1 || hashes[0] !== expectedHash) {
throw new Error('CSP hash does not match the actual JSON-LD bytes.');
}
if (inputs.site.environment === 'preview') {
if (!headers.includes('Cache-Control: private, no-store') ||
!headers.includes('X-Robots-Tag: noindex, nofollow, noarchive')) {
throw new Error('Preview headers must be private and noindex.');
}
if (headers.includes('Strict-Transport-Security')) throw new Error('Preview headers must omit HSTS.');
} else {
if (!headers.includes('Strict-Transport-Security: max-age=31536000')) {
throw new Error('Production headers must include HSTS.');
}
if (headers.includes('X-Robots-Tag') || headers.includes('private, no-store')) {
throw new Error('Production headers must omit preview directives.');
}
}
};
const assertCrawlArtifacts = async (root, inputs, headers) => {
const robots = await readRequiredText(root, 'robots.txt');
const redirects = await readRequiredText(root, '_redirects.example');
if (/https?:\/\//iu.test(redirects)) throw new Error('Redirect example must not invent a host.');
if (inputs.site.environment === 'preview') {
if (robots !== 'User-agent: *\nDisallow: /\n' || /sitemap/iu.test(robots)) {
throw new Error('Preview robots must disallow crawling without advertising a sitemap.');
}
return;
}
const sitemapUrl = new URL('/sitemap.xml', inputs.site.canonicalUrl).href;
if (robots !== `User-agent: *\nAllow: /\nSitemap: ${sitemapUrl}\n`) {
throw new Error('Production robots policy must allow the canonical page and advertise its sitemap.');
}
const sitemap = await readRequiredText(root, 'sitemap.xml');
if (!sitemap.includes(`<loc>${inputs.site.canonicalUrl}</loc>`)) {
throw new Error('Production sitemap must contain the canonical site URL.');
}
if (!headers.includes('Cache-Control: public, max-age=0, must-revalidate')) {
throw new Error('Production HTML requires revalidation caching.');
}
};
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, inputs, contract);
await assertArtifactDigests(root, contract);
await assertNoProhibitedSurfaces(root, files);
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);
};
const assertInsideRepository = (path) => {
const pathRelativeToRoot = relative(repositoryRoot, path);
if (pathRelativeToRoot === '..' || pathRelativeToRoot.startsWith(`..${sep}`)) {
throw new Error('Verification input escaped the repository root.');
}
};
export const verifyArtifacts = async (configPath) => {
const configAbsolutePath = resolve(repositoryRoot, configPath);
assertInsideRepository(configAbsolutePath);
const inputs = await loadProductionInputs({
contentJson: pathToFileURL(resolve(repositoryRoot, 'content/public/portfolio-content.json')),
assetManifestJson: pathToFileURL(resolve(repositoryRoot, 'content/public/asset-manifest.json')),
preparedAssetRoot: pathToFileURL(`${resolve(repositoryRoot, 'inputs/prepared-assets')}${sep}`),
siteConfigJson: pathToFileURL(configAbsolutePath),
});
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) {
const configPath = parseExactConfigArgument(process.argv.slice(2));
await verifyArtifacts(configPath);
}