165 lines
11 KiB
JavaScript
165 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
const sha256Pattern = /^[a-f0-9]{64}$/u;
|
|
const privateIdentifiers = /(?:simpastil|simpasteel|neon[ -]?debugger|yourhealth)/iu;
|
|
const sourceMapReference = /(?:sourceMappingURL\s*=|["'(]\/?[^"')\s]+\.map(?:[?"')\s]|$))/iu;
|
|
|
|
function rootHttpsUrl(value, label) {
|
|
let url;
|
|
try { url = new URL(value); } catch { throw new Error(`${label} must be a valid URL.`); }
|
|
if (url.protocol !== 'https:' || url.username || url.password || url.port || url.pathname !== '/' || url.search || url.hash) {
|
|
throw new Error(`${label} must be one canonical root HTTPS origin without credentials, port, query, or fragment.`);
|
|
}
|
|
return url;
|
|
}
|
|
|
|
function requireHeader(response, name, expected) {
|
|
const value = response.headers.get(name);
|
|
if (value === null || (expected instanceof RegExp ? !expected.test(value) : value !== expected)) throw new Error(`${name} response header is missing or invalid.`);
|
|
return value;
|
|
}
|
|
|
|
function allMatches(source, expression) {
|
|
return [...source.matchAll(expression)].map((match) => match[1]);
|
|
}
|
|
|
|
function decodedText(value) {
|
|
return value.replace(/&/gu, '&').replace(/</gu, '<').replace(/>/gu, '>').replace(/"/gu, '"').replace(/&#(?:39|x27);/giu, "'");
|
|
}
|
|
|
|
function visibleText(html) {
|
|
return decodedText(html.replace(/<script\b[\s\S]*?<\/script>/giu, ' ').replace(/<style\b[\s\S]*?<\/style>/giu, ' ').replace(/<[^>]+>/gu, ' ').replace(/\s+/gu, ' ').trim());
|
|
}
|
|
|
|
export function verifyNoJavaScriptDocument(html) {
|
|
if (typeof html !== 'string' || sourceMapReference.test(html)) throw new Error('JavaScript-disabled document contains a source map reference.');
|
|
if (privateIdentifiers.test(html)) throw new Error('JavaScript-disabled document contains a private identifier.');
|
|
const text = visibleText(html);
|
|
const headings = allMatches(html, /<h[1-6]\b[^>]*>([\s\S]*?)<\/h[1-6]>/giu).length;
|
|
const links = allMatches(html, /<a\b[^>]*href=["'][^"']+["'][^>]*>([\s\S]*?)<\/a>/giu).length;
|
|
if (!/Luca Pîslaru/u.test(text) || !/System Administrator\s*&\s*Full-Stack Developer/u.test(text) ||
|
|
!/Selected builds/iu.test(text) || !/Contact/iu.test(text) || !/mailto:/iu.test(html) || headings < 3 || links < 1 || text.length < 120) {
|
|
throw new Error('JavaScript-disabled document lacks substantive semantic identity, work evidence, or contact content.');
|
|
}
|
|
return { headings, links, visibleCharacters: text.length };
|
|
}
|
|
|
|
function verifySecurityHeaders(response, expectedDigest) {
|
|
requireHeader(response, 'x-portfolio-manifest-digest', expectedDigest);
|
|
const csp = requireHeader(response, 'content-security-policy', /default-src 'self'/u);
|
|
for (const directive of ["connect-src 'self'", "object-src 'none'", "base-uri 'none'", "frame-ancestors 'none'", "form-action 'self'", 'upgrade-insecure-requests']) {
|
|
if (!csp.includes(directive)) throw new Error(`Content-Security-Policy lacks ${directive}.`);
|
|
}
|
|
if (/unsafe-inline|unsafe-eval|\*/u.test(csp)) throw new Error('Content-Security-Policy contains an unsafe source expression.');
|
|
requireHeader(response, 'strict-transport-security', /^max-age=31536000(?:; preload)?$/u);
|
|
requireHeader(response, 'x-content-type-options', 'nosniff');
|
|
requireHeader(response, 'referrer-policy', 'no-referrer');
|
|
requireHeader(response, 'permissions-policy', /camera=\(\).*microphone=\(\).*geolocation=\(\)/u);
|
|
requireHeader(response, 'x-frame-options', 'DENY');
|
|
}
|
|
|
|
function exactTagContent(html, expression, label) {
|
|
const values = allMatches(html, expression);
|
|
if (values.length !== 1) throw new Error(`Expected exactly one ${label}.`);
|
|
return decodedText(values[0]);
|
|
}
|
|
|
|
function verifyMetadata(html, canonicalUrl) {
|
|
const canonical = exactTagContent(html, /<link\b(?=[^>]*\brel=["']canonical["'])[^>]*\bhref=["']([^"']+)["'][^>]*>/giu, 'canonical link');
|
|
const ogUrl = exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:url["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'og:url');
|
|
const robots = exactTagContent(html, /<meta\b(?=[^>]*\bname=["']robots["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'robots meta');
|
|
exactTagContent(html, /<meta\b(?=[^>]*\bname=["']description["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'description meta');
|
|
exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:title["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'og:title');
|
|
exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:description["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'og:description');
|
|
if (canonical !== canonicalUrl || ogUrl !== canonicalUrl || !/^index\s*,\s*follow$/iu.test(robots)) throw new Error('Canonical, Open Graph, or robots metadata is inconsistent.');
|
|
const jsonLdText = exactTagContent(html, /<script\b(?=[^>]*\btype=["']application\/ld\+json["'])[^>]*>([\s\S]*?)<\/script>/giu, 'JSON-LD block');
|
|
let jsonLd;
|
|
try { jsonLd = JSON.parse(jsonLdText); } catch { throw new Error('JSON-LD is not valid JSON.'); }
|
|
if (jsonLd?.['@context'] !== 'https://schema.org' || jsonLd?.name !== 'Luca Pîslaru' || jsonLd?.url !== canonicalUrl) throw new Error('JSON-LD identity or canonical URL is inconsistent.');
|
|
return allMatches(html, /<(?:script|link)\b(?=[^>]*(?:src|href)=["'](\/assets\/[^"']+)["'])[^>]*>/giu);
|
|
}
|
|
|
|
async function request(fetchImpl, url, init = {}) {
|
|
const response = await fetchImpl(url, { ...init, redirect: 'manual', signal: init.signal ?? AbortSignal.timeout(15_000) });
|
|
return response;
|
|
}
|
|
|
|
export async function verifyProductionSmoke(options) {
|
|
const canonical = rootHttpsUrl(options.canonicalUrl, 'Canonical URL');
|
|
if (!sha256Pattern.test(options.expectedManifestDigest)) throw new Error('Expected signed manifest digest must be lowercase SHA-256.');
|
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
for (const alternateValue of options.alternateUrls ?? []) {
|
|
let alternate;
|
|
try { alternate = new URL(alternateValue); } catch { throw new Error('Alternate URL is invalid.'); }
|
|
if (alternate.username || alternate.password || alternate.search || alternate.hash || alternate.pathname !== '/' || alternate.origin === canonical.origin) throw new Error('Alternate URL must be a distinct credential-free root origin.');
|
|
const response = await request(fetchImpl, alternate);
|
|
if (response.status !== 308 || response.headers.get('location') !== canonical.href) throw new Error('Alternate host must redirect to the canonical HTTPS URL in one permanent hop.');
|
|
}
|
|
const rootResponse = await request(fetchImpl, canonical, { headers: { 'accept-encoding': 'br, gzip' } });
|
|
if (rootResponse.status !== 200) throw new Error('Canonical HTTPS URL did not return 200.');
|
|
verifySecurityHeaders(rootResponse, options.expectedManifestDigest);
|
|
requireHeader(rootResponse, 'content-type', /^text\/html(?:;|$)/u);
|
|
requireHeader(rootResponse, 'cache-control', /max-age=0.*must-revalidate/u);
|
|
requireHeader(rootResponse, 'content-encoding', /^(?:br|gzip)$/u);
|
|
const html = await rootResponse.text();
|
|
if (privateIdentifiers.test(html) || sourceMapReference.test(html)) throw new Error('Production HTML exposes a private identifier or source map.');
|
|
const noJs = verifyNoJavaScriptDocument(html);
|
|
const assetPaths = verifyMetadata(html, canonical.href);
|
|
if (assetPaths.length === 0 || assetPaths.some((path) => path.endsWith('.map'))) throw new Error('Production HTML lacks a safe hashed asset reference.');
|
|
const assetResponse = await request(fetchImpl, new URL(assetPaths[0], canonical), { headers: { 'accept-encoding': 'br, gzip' } });
|
|
if (assetResponse.status !== 200) throw new Error('Production asset did not return 200.');
|
|
verifySecurityHeaders(assetResponse, options.expectedManifestDigest);
|
|
requireHeader(assetResponse, 'cache-control', /max-age=31536000.*immutable/u);
|
|
requireHeader(assetResponse, 'content-type', /^(?:text\/javascript|application\/javascript|text\/css)(?:;|$)/u);
|
|
requireHeader(assetResponse, 'content-encoding', /^(?:br|gzip)$/u);
|
|
const assetText = await assetResponse.text();
|
|
if (privateIdentifiers.test(assetText) || sourceMapReference.test(assetText)) throw new Error('Production asset exposes a private identifier or source map.');
|
|
for (const path of ['/robots.txt', '/sitemap.xml']) {
|
|
const response = await request(fetchImpl, new URL(path, canonical));
|
|
if (response.status !== 200) throw new Error(`${path} did not return 200.`);
|
|
verifySecurityHeaders(response, options.expectedManifestDigest);
|
|
requireHeader(response, 'cache-control', /max-age=0.*must-revalidate/u);
|
|
const text = await response.text();
|
|
if (path === '/robots.txt' && (!/Allow:\s*\//iu.test(text) || !text.includes(`${canonical.href}sitemap.xml`))) throw new Error('robots.txt is inconsistent with the canonical sitemap.');
|
|
if (path === '/sitemap.xml' && !text.includes(`<loc>${canonical.href}</loc>`)) throw new Error('sitemap.xml is inconsistent with the canonical URL.');
|
|
}
|
|
let videoRangeVerified = false;
|
|
if (options.videoPath !== undefined) {
|
|
if (!/^\/assets\/[a-z0-9][a-z0-9._-]*\.(?:mp4|webm)$/u.test(options.videoPath)) throw new Error('Video path must be one safe public MP4/WebM asset.');
|
|
const response = await request(fetchImpl, new URL(options.videoPath, canonical), { headers: { range: 'bytes=0-1023' } });
|
|
if (response.status !== 206) throw new Error('Production video range request did not return 206.');
|
|
verifySecurityHeaders(response, options.expectedManifestDigest);
|
|
requireHeader(response, 'accept-ranges', 'bytes'); requireHeader(response, 'content-range', /^bytes 0-1023\/\d+$/u);
|
|
requireHeader(response, 'content-type', /^video\/(?:mp4|webm)$/u); requireHeader(response, 'cache-control', /max-age=31536000.*immutable/u);
|
|
videoRangeVerified = true;
|
|
}
|
|
return { schema: 'portfolio-production-smoke/v1', canonicalUrl: canonical.href, manifestDigest: options.expectedManifestDigest,
|
|
jsDisabledContentVerified: true, headings: noJs.headings, assetCompressionVerified: true, videoRangeVerified };
|
|
}
|
|
|
|
function parseArguments(args) {
|
|
const values = { alternateUrls: [] };
|
|
for (let index = 0; index < args.length; index += 2) {
|
|
const key = args[index]; const value = args[index + 1];
|
|
if (!key?.startsWith('--') || !value) throw new Error('Smoke arguments must be complete name/value pairs.');
|
|
const name = key.slice(2);
|
|
if (name === 'alternate-url') values.alternateUrls.push(value);
|
|
else if (Object.hasOwn(values, name)) throw new Error(`Duplicate smoke argument: ${key}`);
|
|
else values[name] = value;
|
|
}
|
|
if (!values['canonical-url'] || !values['manifest-digest']) throw new Error('Usage: smoke-production.mjs --canonical-url <https-root> --manifest-digest <sha256> [--alternate-url <url>] [--video-path <path>].');
|
|
return values;
|
|
}
|
|
|
|
async function main() {
|
|
const values = parseArguments(process.argv.slice(2));
|
|
const result = await verifyProductionSmoke({ canonicalUrl: values['canonical-url'], expectedManifestDigest: values['manifest-digest'],
|
|
alternateUrls: values.alternateUrls, videoPath: values['video-path'] });
|
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
main().catch((error) => { process.stderr.write(`Production smoke failed: ${error instanceof Error ? error.message : 'unknown error'}\n`); process.exitCode = 1; });
|
|
}
|