release: verify production portfolio deployment

This commit is contained in:
2026-09-02 10:45:41 +03:00
parent d3e746b905
commit 0119e425ec
10 changed files with 742 additions and 0 deletions

4
.gitignore vendored
View File

@@ -7,6 +7,10 @@ playwright-report/
test-results/ test-results/
.env* .env*
config/site.production.json config/site.production.json
release-evidence/
*.release.tar
*.release.tar.partial-*
docs/deployment/rollback-records/*.json
content/private/ content/private/
inputs/original-assets/ inputs/original-assets/
*.map *.map

View File

@@ -0,0 +1,100 @@
# Portfolio release checklist
This checklist separates deterministic local evidence from changes that require Luca's explicit production authorization. A green local build is not permission to connect to a server, change DNS, deploy, or submit anything to a search engine.
## Release inputs blocked on Luca
Do not create `config/site.production.json` and do not start production work until Luca supplies and approves all of the following:
- [ ] Canonical root HTTPS URL.
- [ ] Immutable production build ID.
- [ ] Target server and fixed release root/path.
- [ ] Deployment credentials through an external secret mechanism.
- [ ] Exact release activation approval after reviewing the signed digest and predecessor.
- [ ] Search Console account/property access and separate approval for submission.
`config/site.production.json` remains ignored. When authorized, it may contain exactly `environment`, `canonicalUrl`, and `buildId`; it must never contain credentials or private server paths.
## Local, credentialless evidence
Run collection in a disposable, credentialless sandbox whose network access is limited to the configured package registry. The explicit network flag is an operator acknowledgement; it is intentionally not part of the normal verification command.
```text
node scripts/supply-chain-check.mjs collect \
--project . \
--evidence /external/write-once/release-evidence/supply-chain \
--triage APPROVED_COMMITTED_MEDIUM_TRIAGE.json \
--allow-registry-network yes \
--today YYYY-MM-DD
```
The collector runs `npm ci --ignore-scripts`, production and full development audits, the pinned license checker, and the pinned CycloneDX generator. It strips the environment to a minimal credential-free set. The verifier fails when package metadata or the lockfile changes, evidence digests do not match, a license is not allowlisted, an SBOM is malformed, or a Critical/High finding exists. Every Medium finding must have one current, exact, committed triage entry; stale extras also fail.
- [ ] `npm run verify:supply-chain` passes against the selected immutable evidence directory.
- [ ] `npm audit` evidence covers production dependencies and the complete development tree.
- [ ] Critical findings: 0. High findings: 0.
- [ ] Every Medium finding has a current committed owner, rationale, and review date.
- [ ] CycloneDX SBOM is bound by the evidence manifest and packaged with the release.
- [ ] License inventory contains no unknown, copyleft, or unapproved expression.
- [ ] `npm run build:preview` passes.
- [ ] Complete production build, unit/browser/accessibility/performance gates pass using the approved production config.
- [ ] Privacy reconciliation and signed-release admission pass against external trust state.
- [ ] Deterministic release package extracts and verifies in a temporary root.
- [ ] Local Nginx config check, security headers, MIME, compression, and byte range checks pass.
- [ ] No source maps, original/private assets, private product names, credentials, scanner payloads, or private server paths appear in the release closure.
Record for Luca's release checkpoint:
- Signed manifest digest:
- Immutable release/build ID:
- Package SHA-256 and byte size:
- Approval/provenance root:
- Supply-chain evidence digest:
- Privacy evidence digest:
- Manifest expiry:
- Expected current predecessor digest and fence:
- Planned canonical host:
- Planned target release root (share privately, not in Git):
- Exact prepare and activation commands:
- Exact signed rollback target/command:
- Local verification summary:
## Explicit remote authorization checkpoint
- [ ] Luca reviewed the values above and explicitly authorized this exact signed release.
- [ ] The authorization has not expired and still names the current predecessor/fence.
Without both checks, stop. Do not connect to the server.
## Authorized staging and activation
- [ ] Transfer only the immutable verified package to the approved target.
- [ ] Run server-side preparation; confirm it changes no `current` pointer.
- [ ] Verify the readiness receipt digest, expiry, predecessor, and expected fence.
- [ ] Activate with the fenced atomic pointer switch.
- [ ] Preserve the previous immutable release and receipt for rollback.
- [ ] Run the public smoke command with the approved canonical URL, alternate hosts, and signed manifest digest:
```text
npm run smoke:production -- \
--canonical-url https://APPROVED-HOST/ \
--manifest-digest LOWERCASE_SHA256 \
--alternate-url http://APPROVED-ALTERNATE/
```
Add `--video-path /assets/APPROVED-HASHED.webm` when the deployed public closure contains video. A deployed response must expose the signed digest through `X-Portfolio-Manifest-Digest`. The smoke gate checks the canonical 200 response, one-hop 308 redirects, security/cache/compression/MIME/range behavior, canonical/OG/JSON-LD/robots/sitemap agreement, substantive server-rendered content without JavaScript, and absence of source maps/private product identifiers.
- [ ] Public smoke report digest equals the locally approved signed manifest digest.
- [ ] If any activation or public smoke check fails, stop traffic changes and follow the signed rollback procedure.
## Post-launch SEO (separate external action)
- [ ] Confirm the canonical page and sitemap are publicly indexable and alternate hosts still redirect in one hop.
- [ ] After Luca authorizes Search Console access, add/verify the exact canonical property.
- [ ] Submit the exact canonical sitemap and request indexing for the canonical home page.
- [ ] Record the property, sitemap URL, submission timestamp, operator, and result outside secrets.
- [ ] Monitor branded-name indexing and crawl issues. Do not promise a ranking position; SEO gates establish correctness and discoverability, not ranking guarantees.
## Completion record
Phase 4 is remotely complete only when the deployed digest equals the locally approved signed digest, public smoke checks pass, the previous release remains recoverable, the Search Console action is recorded, and no private source, private identifier, or credential entered the public closure.

View File

@@ -0,0 +1,55 @@
# Portfolio rollback record
Create one record per rollback attempt. Keep credentials, secret key material, private scanner evidence, and private server paths out of this document and Git. A rollback uses a newly signed rollback manifest; an old release manifest is never replayed.
## Authorization
- Incident/change reference:
- Authorized by:
- Authorization timestamp (UTC):
- Operator:
- Reason and observed failure:
## Fenced release state
- Canonical public URL:
- Current signed manifest digest:
- Current immutable release ID:
- Current fence:
- Approved rollback target artifact digest:
- Newly signed rollback manifest digest:
- Rollback manifest expiry:
- Expected predecessor digest:
- Expected fence:
- External approval/ledger revision:
## Verification before pointer change
- [ ] Rollback target is an existing immutable release directory.
- [ ] Target artifact hashes match the newly signed rollback manifest.
- [ ] Signature, scope, issuer epoch, approval/ledger state, expiry, predecessor, and fence verify.
- [ ] Readiness and internal smoke checks pass.
- [ ] A concurrent activation has not changed the current digest or fence.
## Outcome
- Activation start timestamp (UTC):
- Activation finish timestamp (UTC):
- Result: succeeded / failed without pointer change / failed after pointer change
- New current signed manifest digest:
- New fence:
- Atomic activation receipt digest:
- Public smoke report digest:
- Public smoke result:
- Previous/current release still recoverable:
- Additional containment or follow-up:
## Post-rollback checks
- [ ] Canonical HTTPS response is 200 and reports the expected manifest digest.
- [ ] Alternate hosts redirect to the canonical URL in one permanent hop.
- [ ] Security, cache, compression, MIME, and range headers pass.
- [ ] Canonical/OG/JSON-LD/robots/sitemap agree.
- [ ] JavaScript-disabled content remains substantive and accessible.
- [ ] No source maps, private identifiers, or credentials are public.
- [ ] Incident and Search Console/crawl implications are recorded if applicable.

View File

@@ -10,6 +10,7 @@
"dev": "vite --host 127.0.0.1", "dev": "vite --host 127.0.0.1",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
"test:unit": "vitest run", "test:unit": "vitest run",
"test:deployment": "vitest run --config tests/deployment/vitest.config.ts",
"test:browser": "playwright test", "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", "build:ssr": "vite build --ssr src/entry-server.ts --outDir dist-ssr",
@@ -22,6 +23,8 @@
"capture:runtime-budgets": "playwright test tests/performance/runtime-budget-trace.spec.ts --project=runtime-budget-desktop --workers=1", "capture:runtime-budgets": "playwright test tests/performance/runtime-budget-trace.spec.ts --project=runtime-budget-desktop --workers=1",
"verify:runtime-budgets": "node scripts/verify-runtime-budgets.mjs", "verify:runtime-budgets": "node scripts/verify-runtime-budgets.mjs",
"verify:phase3": "npm run verify:runtime-budgets && npm run test:browser", "verify:phase3": "npm run verify:runtime-budgets && npm run test:browser",
"verify:supply-chain": "node scripts/supply-chain-check.mjs verify --project . --evidence release-evidence/supply-chain",
"smoke:production": "node scripts/smoke-production.mjs",
"aggregate:performance": "node scripts/aggregate-drive-traces.mjs", "aggregate:performance": "node scripts/aggregate-drive-traces.mjs",
"sign:performance": "node scripts/sign-drive-trace.mjs", "sign:performance": "node scripts/sign-drive-trace.mjs",
"verify:approved-assets": "node scripts/materialize-empty-asset-set.mjs", "verify:approved-assets": "node scripts/materialize-empty-asset-set.mjs",

View File

@@ -0,0 +1,164 @@
#!/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(/&amp;/gu, '&').replace(/&lt;/gu, '<').replace(/&gt;/gu, '>').replace(/&quot;/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 'none'", "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; });
}

View File

@@ -0,0 +1,233 @@
#!/usr/bin/env node
import { createHash } from 'node:crypto';
import { execFileSync, spawnSync } from 'node:child_process';
import { lstat, mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
import { pathToFileURL } from 'node:url';
const sha256Pattern = /^[a-f0-9]{64}$/u;
const datePattern = /^\d{4}-\d{2}-\d{2}$/u;
const reportNames = [
'npm-audit-production.json',
'npm-audit-development.json',
'licenses.json',
'sbom.cdx.json',
'medium-triage.json',
];
const allowedLicenses = new Set([
'0BSD', 'Apache-2.0', 'BlueOak-1.0.0', 'BSD-2-Clause', 'BSD-3-Clause', 'CC0-1.0', 'ISC', 'MIT', 'Python-2.0', 'Unlicense',
]);
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
const exactKeys = (value, keys) => isObject(value) && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
async function safeRead(path, label) {
const stats = await lstat(path);
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) throw new Error(`${label} must be one regular non-linked file.`);
return readFile(path);
}
function parseJson(bytes, label) {
try { return JSON.parse(bytes.toString('utf8')); } catch { throw new Error(`${label} is not valid JSON.`); }
}
function parseAudit(report, scope) {
if (!isObject(report) || report.auditReportVersion !== 2 || !isObject(report.vulnerabilities)) {
throw new Error(`${scope} npm audit evidence has an unsupported schema.`);
}
const findings = [];
for (const [key, raw] of Object.entries(report.vulnerabilities)) {
if (!isObject(raw) || raw.name !== key || !['info', 'low', 'moderate', 'high', 'critical'].includes(raw.severity) || typeof raw.range !== 'string') {
throw new Error(`${scope} npm audit evidence contains an invalid finding.`);
}
findings.push({ scope, package: key, severity: raw.severity, range: raw.range });
}
return findings.sort((left, right) => `${left.scope}\0${left.package}`.localeCompare(`${right.scope}\0${right.package}`, 'en'));
}
function parseTriage(value, today) {
if (!exactKeys(value, ['schema', 'findings']) || value.schema !== 'portfolio-medium-vulnerability-triage/v1' || !Array.isArray(value.findings)) {
throw new Error('Medium vulnerability triage has an invalid envelope.');
}
const records = new Map();
for (const finding of value.findings) {
if (!exactKeys(finding, ['scope', 'package', 'severity', 'range', 'disposition', 'rationale', 'owner', 'reviewBy']) ||
!['production', 'development'].includes(finding.scope) || finding.severity !== 'moderate' || finding.disposition !== 'accepted' ||
typeof finding.package !== 'string' || finding.package.length === 0 || typeof finding.range !== 'string' || finding.range.length === 0 ||
typeof finding.rationale !== 'string' || finding.rationale.trim().length < 20 || typeof finding.owner !== 'string' || finding.owner.trim().length === 0 ||
typeof finding.reviewBy !== 'string' || !datePattern.test(finding.reviewBy) || finding.reviewBy < today) {
throw new Error('Medium vulnerability triage contains an invalid, expired, or incomplete record.');
}
const key = `${finding.scope}\0${finding.package}\0${finding.range}`;
if (records.has(key)) throw new Error('Medium vulnerability triage contains duplicate records.');
records.set(key, finding);
}
return records;
}
function verifyLicenses(value, allowlist) {
if (!isObject(value) || Object.keys(value).length === 0) throw new Error('License evidence is empty or invalid.');
for (const [dependency, record] of Object.entries(value)) {
if (!isObject(record) || typeof record.licenses !== 'string' || !allowlist.has(record.licenses)) {
throw new Error(`Dependency ${dependency} has a missing or forbidden license.`);
}
}
return Object.keys(value).length;
}
function verifySbom(value, packageJson) {
if (!isObject(value) || value.bomFormat !== 'CycloneDX' || !/^1\.(?:5|6)$/u.test(value.specVersion) || value.version !== 1 ||
!isObject(value.metadata) || !isObject(value.metadata.component) || value.metadata.component.type !== 'application' ||
value.metadata.component.name !== packageJson.name || value.metadata.component.version !== packageJson.version ||
(value.components !== undefined && !Array.isArray(value.components))) {
throw new Error('CycloneDX SBOM is invalid or does not describe this package.');
}
const identities = new Set();
for (const component of value.components ?? []) {
if (!isObject(component) || typeof component.type !== 'string' || typeof component.name !== 'string' || typeof component.version !== 'string') {
throw new Error('CycloneDX SBOM contains an invalid component.');
}
const identity = typeof component['bom-ref'] === 'string' ? component['bom-ref'] : `${component.type}\0${component.name}\0${component.version}`;
if (identities.has(identity)) throw new Error('CycloneDX SBOM contains duplicate component identities.');
identities.add(identity);
}
return identities.size;
}
function verifyToolProvenance(toolVersions, packageJson, packageLock) {
if (!exactKeys(toolVersions, ['npm', 'cyclonedxNpm', 'licenseChecker']) ||
typeof toolVersions.npm !== 'string' || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(toolVersions.npm)) {
throw new Error('Supply-chain tool provenance is incomplete.');
}
const tools = [
['cyclonedxNpm', '@cyclonedx/cyclonedx-npm'],
['licenseChecker', 'license-checker-rseidelsohn'],
];
for (const [field, dependency] of tools) {
const locked = packageLock.packages?.[`node_modules/${dependency}`];
if (typeof toolVersions[field] !== 'string' || toolVersions[field] !== locked?.version ||
packageJson.devDependencies?.[dependency] !== locked.version || typeof locked.integrity !== 'string' || !locked.integrity.startsWith('sha512-')) {
throw new Error(`Pinned ${dependency} tool provenance does not match the integrity-bound lockfile.`);
}
}
}
function assertTracked(projectRoot, triagePath) {
const rel = relative(projectRoot, triagePath);
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error('Committed Medium triage must be inside the project root.');
try {
execFileSync('git', ['ls-files', '--error-unmatch', '--', rel], { cwd: projectRoot, stdio: 'ignore' });
} catch {
throw new Error('Medium vulnerability triage must be committed to Git before release.');
}
}
export async function verifySupplyChainEvidence(options) {
const projectRoot = resolve(options.projectRoot);
const evidenceDir = resolve(options.evidenceDir);
const today = options.today ?? new Date().toISOString().slice(0, 10);
if (!datePattern.test(today)) throw new Error('Trusted supply-chain date must be YYYY-MM-DD.');
const packageJsonBytes = await safeRead(resolve(projectRoot, 'package.json'), 'package.json');
const packageLockBytes = await safeRead(resolve(projectRoot, 'package-lock.json'), 'package-lock.json');
const packageJson = parseJson(packageJsonBytes, 'package.json');
const packageLock = parseJson(packageLockBytes, 'package-lock.json');
if (!isObject(packageJson) || !isObject(packageLock) || packageLock.lockfileVersion !== 3 || packageLock.name !== packageJson.name || packageLock.version !== packageJson.version) {
throw new Error('package.json and package-lock.json are not a clean matching npm lock pair.');
}
const manifest = parseJson(await safeRead(resolve(evidenceDir, 'evidence-manifest.json'), 'Supply-chain evidence manifest'), 'Supply-chain evidence manifest');
if (!exactKeys(manifest, ['schema', 'packageJsonSha256', 'packageLockSha256', 'reports', 'toolVersions']) ||
manifest.schema !== 'portfolio-supply-chain-evidence/v1' || manifest.packageJsonSha256 !== sha256(packageJsonBytes) ||
manifest.packageLockSha256 !== sha256(packageLockBytes) || !isObject(manifest.reports) || !isObject(manifest.toolVersions) ||
JSON.stringify(Object.keys(manifest.reports).sort()) !== JSON.stringify([...reportNames].sort())) {
throw new Error('Supply-chain evidence manifest or lockfile digest is invalid.');
}
verifyToolProvenance(manifest.toolVersions, packageJson, packageLock);
const reports = {};
for (const name of reportNames) {
const bytes = await safeRead(resolve(evidenceDir, name), `Supply-chain report ${name}`);
if (!sha256Pattern.test(manifest.reports[name]) || manifest.reports[name] !== sha256(bytes)) throw new Error(`Supply-chain report digest mismatch: ${name}`);
reports[name] = parseJson(bytes, name);
}
const production = parseAudit(reports['npm-audit-production.json'], 'production');
const development = parseAudit(reports['npm-audit-development.json'], 'development');
const all = [...production, ...development];
const critical = all.filter((finding) => finding.severity === 'critical').length;
const high = all.filter((finding) => finding.severity === 'high').length;
if (critical > 0 || high > 0) throw new Error(`Supply chain is blocked by ${critical} Critical and ${high} High finding(s).`);
const mediumFindings = all.filter((finding) => finding.severity === 'moderate');
const triage = parseTriage(reports['medium-triage.json'], today);
const expectedTriage = new Set(mediumFindings.map((finding) => `${finding.scope}\0${finding.package}\0${finding.range}`));
if (triage.size !== expectedTriage.size || [...expectedTriage].some((key) => !triage.has(key))) {
throw new Error('Every Medium finding requires one exact current triage record, with no stale extras.');
}
if (options.requireTrackedTriage !== false && mediumFindings.length > 0) assertTracked(projectRoot, resolve(evidenceDir, 'medium-triage.json'));
const licenseCount = verifyLicenses(reports['licenses.json'], options.allowedLicenses ?? allowedLicenses);
const componentCount = verifySbom(reports['sbom.cdx.json'], packageJson);
return { schema: 'portfolio-supply-chain-verification/v1', critical, high, medium: mediumFindings.length, licenseCount, componentCount,
packageLockSha256: manifest.packageLockSha256, evidenceManifestSha256: sha256(await safeRead(resolve(evidenceDir, 'evidence-manifest.json'), 'Supply-chain evidence manifest')) };
}
function runTool(command, args, cwd, options = {}) {
const result = spawnSync(command, args, { cwd, encoding: 'utf8', env: { PATH: process.env.PATH ?? '', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', NO_COLOR: '1' }, maxBuffer: 64 * 1024 * 1024 });
if (result.error || (!options.allowAuditExit && result.status !== 0) || (options.allowAuditExit && ![0, 1].includes(result.status))) {
throw new Error(`${command} ${args[0] ?? ''} failed without producing trusted evidence.`);
}
return result.stdout;
}
async function collectEvidence(projectRoot, evidenceDir, triagePath) {
await mkdir(evidenceDir, { recursive: false });
const beforePackage = await safeRead(resolve(projectRoot, 'package.json'), 'package.json');
const beforeLock = await safeRead(resolve(projectRoot, 'package-lock.json'), 'package-lock.json');
const npmVersion = runTool('npm', ['--version'], projectRoot).trim();
runTool('npm', ['ci', '--ignore-scripts'], projectRoot);
const productionAudit = runTool('npm', ['audit', '--omit=dev', '--json'], projectRoot, { allowAuditExit: true });
const developmentAudit = runTool('npm', ['audit', '--json'], projectRoot, { allowAuditExit: true });
const licenseBin = resolve(projectRoot, 'node_modules/.bin/license-checker-rseidelsohn');
const cyclonedxBin = resolve(projectRoot, 'node_modules/.bin/cyclonedx-npm');
const licenses = runTool(licenseBin, ['--json'], projectRoot);
const sbomPath = resolve(evidenceDir, 'sbom.cdx.json');
runTool(cyclonedxBin, ['--output-file', sbomPath, '--output-format', 'JSON', '--spec-version', '1.6'], projectRoot);
const afterPackage = await safeRead(resolve(projectRoot, 'package.json'), 'package.json');
const afterLock = await safeRead(resolve(projectRoot, 'package-lock.json'), 'package-lock.json');
if (sha256(beforePackage) !== sha256(afterPackage) || sha256(beforeLock) !== sha256(afterLock)) throw new Error('npm ci changed package metadata or lockfile.');
const triage = await safeRead(triagePath, 'Committed Medium triage');
const output = {
'npm-audit-production.json': Buffer.from(productionAudit), 'npm-audit-development.json': Buffer.from(developmentAudit),
'licenses.json': Buffer.from(licenses), 'sbom.cdx.json': await safeRead(sbomPath, 'Generated CycloneDX SBOM'), 'medium-triage.json': triage,
};
for (const [name, bytes] of Object.entries(output)) if (name !== 'sbom.cdx.json') await writeFile(resolve(evidenceDir, name), bytes, { flag: 'wx', mode: 0o400 });
const lock = parseJson(beforeLock, 'package-lock.json');
const manifest = { schema: 'portfolio-supply-chain-evidence/v1', packageJsonSha256: sha256(beforePackage), packageLockSha256: sha256(beforeLock),
reports: Object.fromEntries(reportNames.map((name) => [name, sha256(output[name])])), toolVersions: { npm: npmVersion,
cyclonedxNpm: lock.packages?.['node_modules/@cyclonedx/cyclonedx-npm']?.version, licenseChecker: lock.packages?.['node_modules/license-checker-rseidelsohn']?.version } };
await writeFile(resolve(evidenceDir, 'evidence-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx', mode: 0o400 });
}
function parseArguments(args) {
const command = args.shift();
const values = { alternate: [] };
for (let index = 0; index < args.length; index += 2) {
const key = args[index]; const value = args[index + 1];
if (!key?.startsWith('--') || !value || Object.hasOwn(values, key.slice(2))) throw new Error('Supply-chain arguments must be unique complete name/value pairs.');
values[key.slice(2)] = value;
}
return { command, values };
}
async function main() {
const { command, values } = parseArguments(process.argv.slice(2));
const projectRoot = resolve(values.project ?? '.');
const evidenceDir = resolve(values.evidence ?? 'release-evidence/supply-chain');
if (command === 'collect') {
if (values['allow-registry-network'] !== 'yes') throw new Error('Collection requires --allow-registry-network yes in a credentialless registry-only sandbox.');
if (!values.triage) throw new Error('Collection requires --triage <committed-record>.');
await collectEvidence(projectRoot, evidenceDir, resolve(values.triage));
} else if (command !== 'verify') throw new Error('Usage: supply-chain-check.mjs <collect|verify> --project <root> --evidence <dir>.');
process.stdout.write(`${JSON.stringify(await verifySupplyChainEvidence({ projectRoot, evidenceDir, today: values.today }))}\n`);
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => { process.stderr.write(`Supply-chain verification failed: ${error instanceof Error ? error.message : 'unknown error'}\n`); process.exitCode = 1; });
}

View File

@@ -0,0 +1,76 @@
import { createHash } from 'node:crypto';
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
// @ts-expect-error The production release checker is an executable ESM module.
import { verifySupplyChainEvidence } from '../../scripts/supply-chain-check.mjs';
const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex');
async function evidenceFixture(overrides: Record<string, unknown> = {}) {
const root = await mkdtemp(join(tmpdir(), 'portfolio-supply-chain-'));
const evidence = join(root, 'evidence');
await mkdir(evidence);
const packageJson = JSON.stringify({ name: 'portfolio', version: '1.0.0', devDependencies: { '@cyclonedx/cyclonedx-npm': '6.0.1', 'license-checker-rseidelsohn': '4.4.2' } });
const packageLock = JSON.stringify({ name: 'portfolio', version: '1.0.0', lockfileVersion: 3, packages: {
'': { name: 'portfolio', version: '1.0.0' },
'node_modules/@cyclonedx/cyclonedx-npm': { version: '6.0.1', integrity: 'sha512-test' },
'node_modules/license-checker-rseidelsohn': { version: '4.4.2', integrity: 'sha512-test' },
} });
await writeFile(join(root, 'package.json'), packageJson);
await writeFile(join(root, 'package-lock.json'), packageLock);
const reports = {
'npm-audit-production.json': JSON.stringify(overrides.productionAudit ?? { auditReportVersion: 2, vulnerabilities: {}, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 0, critical: 0, total: 0 } } }),
'npm-audit-development.json': JSON.stringify(overrides.developmentAudit ?? { auditReportVersion: 2, vulnerabilities: {}, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 0, critical: 0, total: 0 } } }),
'licenses.json': JSON.stringify(overrides.licenses ?? { 'portfolio@1.0.0': { licenses: 'ISC' } }),
'sbom.cdx.json': JSON.stringify(overrides.sbom ?? { bomFormat: 'CycloneDX', specVersion: '1.6', version: 1, metadata: { component: { type: 'application', name: 'portfolio', version: '1.0.0' } }, components: [] }),
'medium-triage.json': JSON.stringify(overrides.triage ?? { schema: 'portfolio-medium-vulnerability-triage/v1', findings: [] }),
};
for (const [name, contents] of Object.entries(reports)) await writeFile(join(evidence, name), contents);
const manifest = {
schema: 'portfolio-supply-chain-evidence/v1',
packageJsonSha256: sha256(packageJson),
packageLockSha256: sha256(packageLock),
reports: Object.fromEntries(Object.entries(reports).map(([name, contents]) => [name, sha256(contents)])),
toolVersions: { npm: '11.0.0', cyclonedxNpm: '6.0.1', licenseChecker: '4.4.2' },
};
await writeFile(join(evidence, 'evidence-manifest.json'), JSON.stringify(manifest));
return { root, evidence };
}
describe('supply-chain evidence', () => {
it('accepts a digest-bound clean audit, allowlisted licenses, and CycloneDX SBOM', async () => {
const fixture = await evidenceFixture();
await expect(verifySupplyChainEvidence({ projectRoot: fixture.root, evidenceDir: fixture.evidence, requireTrackedTriage: false })).resolves.toMatchObject({
critical: 0, high: 0, medium: 0, componentCount: 0,
});
});
it('fails closed on altered evidence, Critical/High findings, or forbidden licenses', async () => {
const fixture = await evidenceFixture();
await writeFile(join(fixture.evidence, 'licenses.json'), JSON.stringify({ 'portfolio@1.0.0': { licenses: 'AGPL-3.0' } }));
await expect(verifySupplyChainEvidence({ projectRoot: fixture.root, evidenceDir: fixture.evidence, requireTrackedTriage: false })).rejects.toThrow(/digest/u);
const high = await evidenceFixture({
productionAudit: { auditReportVersion: 2, vulnerabilities: { unsafe: { name: 'unsafe', severity: 'high', via: [], effects: [], range: '*', nodes: [] } }, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 0, high: 1, critical: 0, total: 1 } } },
});
await expect(verifySupplyChainEvidence({ projectRoot: high.root, evidenceDir: high.evidence, requireTrackedTriage: false })).rejects.toThrow(/High|high/u);
const license = await evidenceFixture({ licenses: { 'portfolio@1.0.0': { licenses: 'AGPL-3.0-only' } } });
await expect(verifySupplyChainEvidence({ projectRoot: license.root, evidenceDir: license.evidence, requireTrackedTriage: false })).rejects.toThrow(/license/u);
});
it('requires an exact, current triage record for every Medium finding', async () => {
const audit = { auditReportVersion: 2, vulnerabilities: { packageA: { name: 'packageA', severity: 'moderate', via: [], effects: [], range: '<2', nodes: ['node_modules/packageA'] } }, metadata: { vulnerabilities: { info: 0, low: 0, moderate: 1, high: 0, critical: 0, total: 1 } } };
const missing = await evidenceFixture({ developmentAudit: audit });
await expect(verifySupplyChainEvidence({ projectRoot: missing.root, evidenceDir: missing.evidence, requireTrackedTriage: false })).rejects.toThrow(/triage/u);
const accepted = await evidenceFixture({
developmentAudit: audit,
triage: { schema: 'portfolio-medium-vulnerability-triage/v1', findings: [{ scope: 'development', package: 'packageA', severity: 'moderate', range: '<2', disposition: 'accepted', rationale: 'Build-only tool; no deployed runtime exposure.', owner: 'Luca Pîslaru', reviewBy: '2027-01-01' }] },
});
await expect(verifySupplyChainEvidence({ projectRoot: accepted.root, evidenceDir: accepted.evidence, requireTrackedTriage: false, today: '2026-09-02' })).resolves.toMatchObject({ medium: 1 });
});
});

View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
// @ts-expect-error The production smoke checker is an executable ESM module.
import { verifyNoJavaScriptDocument } from '../../scripts/smoke-production.mjs';
describe('JavaScript-disabled production document', () => {
it('requires substantive semantic identity, work evidence, navigation, and contact content', () => {
const html = '<!doctype html><html><head><title>Luca Pîslaru</title></head><body><nav><a href="#selected-builds">Selected builds</a><a href="#contact">Contact</a></nav><main><h1>Luca Pîslaru</h1><p>System Administrator &amp; Full-Stack Developer</p><section id="selected-builds"><h2>Selected builds</h2><p>Project evidence and technical decisions.</p></section><section id="contact"><h2>Contact</h2><a href="mailto:portfolio@example.test">Email Luca</a></section></main><script src="/assets/app.12345678.js"></script></body></html>';
expect(verifyNoJavaScriptDocument(html)).toMatchObject({ headings: expect.any(Number), links: expect.any(Number) });
});
it('rejects a JavaScript-only shell, source maps, and private identifiers', () => {
expect(() => verifyNoJavaScriptDocument('<div id="app"></div><script src="app.js"></script>')).toThrow(/substantive|identity/u);
expect(() => verifyNoJavaScriptDocument('<main><h1>Luca Pîslaru</h1><p>System Administrator & Full-Stack Developer</p><h2>Selected builds</h2><p>Evidence</p><h2>Contact</h2><a href="mailto:x@y.z">Email</a><script src="app.js.map"></script></main>')).toThrow(/source map/u);
expect(() => verifyNoJavaScriptDocument('<main><h1>Luca Pîslaru</h1><p>System Administrator & Full-Stack Developer</p><h2>Selected builds</h2><p>SimpaSteel is private.</p><h2>Contact</h2><a href="mailto:x@y.z">Email</a></main>')).toThrow(/private identifier/u);
});
});

View File

@@ -0,0 +1,82 @@
import { createServer } from 'node:http';
import { brotliCompressSync, gzipSync } from 'node:zlib';
import { afterEach, describe, expect, it } from 'vitest';
// @ts-expect-error The production smoke checker is an executable ESM module.
import { verifyProductionSmoke } from '../../scripts/smoke-production.mjs';
const digest = 'a'.repeat(64);
const canonical = 'https://portfolio.example/';
const alternate = 'http://www.portfolio.example/';
const html = `<!doctype html><html><head>
<title>Luca Pîslaru — System Administrator &amp; Full-Stack Developer</title>
<meta name="description" content="Systems administration and full-stack development portfolio." />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="${canonical}" />
<meta property="og:title" content="Luca Pîslaru" />
<meta property="og:description" content="Systems administration and full-stack development portfolio." />
<meta property="og:url" content="${canonical}" />
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","name":"Luca Pîslaru","url":"${canonical}"}</script>
<script type="module" src="/assets/app.12345678.js"></script></head>
<body><main><h1>Luca Pîslaru</h1><p>System Administrator &amp; Full-Stack Developer</p><section><h2>Selected builds</h2><p>Evidence-led project work.</p></section><section><h2>Contact</h2><p>Discuss systems and software work.</p><a href="mailto:hello@example.com">Contact</a></section></main></body></html>`;
const servers: ReturnType<typeof createServer>[] = [];
afterEach(async () => Promise.all(servers.splice(0).map((server) => new Promise<void>((resolve) => server.close(() => resolve())))));
async function fixtureFetch() {
const server = createServer((request, response) => {
const original = request.headers['x-original-url'];
const url = new URL(String(original));
if (url.origin === new URL(alternate).origin) {
response.writeHead(308, { location: canonical }); response.end(); return;
}
const base = {
'x-portfolio-manifest-digest': digest,
'content-security-policy': "default-src 'self'; script-src 'self' 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='; style-src 'self'; img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'none'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests",
'strict-transport-security': 'max-age=31536000',
'x-content-type-options': 'nosniff',
'referrer-policy': 'no-referrer',
'permissions-policy': 'camera=(), microphone=(), geolocation=()',
'x-frame-options': 'DENY',
};
if (url.pathname === '/') {
response.writeHead(200, { ...base, 'content-type': 'text/html; charset=utf-8', 'cache-control': 'public, max-age=0, must-revalidate', 'content-encoding': 'br' }); response.end(brotliCompressSync(html)); return;
}
if (url.pathname === '/robots.txt') {
response.writeHead(200, { ...base, 'content-type': 'text/plain', 'cache-control': 'public, max-age=0, must-revalidate' }); response.end(`User-agent: *\nAllow: /\nSitemap: ${canonical}sitemap.xml\n`); return;
}
if (url.pathname === '/sitemap.xml') {
response.writeHead(200, { ...base, 'content-type': 'application/xml', 'cache-control': 'public, max-age=0, must-revalidate' }); response.end(`<?xml version="1.0"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>${canonical}</loc></url></urlset>`); return;
}
if (url.pathname === '/assets/app.12345678.js') {
response.writeHead(200, { ...base, 'content-type': 'text/javascript', 'cache-control': 'public, max-age=31536000, immutable', 'content-encoding': 'gzip' }); response.end(gzipSync('console.log("portfolio")')); return;
}
if (url.pathname === '/assets/demo.12345678.webm') {
if (request.headers.range !== 'bytes=0-1023') { response.writeHead(416, base); response.end(); return; }
response.writeHead(206, { ...base, 'content-type': 'video/webm', 'cache-control': 'public, max-age=31536000, immutable', 'accept-ranges': 'bytes', 'content-range': 'bytes 0-1023/2048', 'content-length': '1024' }); response.end(Buffer.alloc(1024)); return;
}
response.writeHead(404, base); response.end();
});
servers.push(server);
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
if (address === null || typeof address === 'string') throw new Error('fixture server did not bind');
return async (input: string | URL | Request, init?: RequestInit) => {
const requested = input instanceof Request ? input.url : input.toString();
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
headers.set('x-original-url', requested);
return fetch(`http://127.0.0.1:${address.port}${new URL(requested).pathname}`, { ...init, redirect: 'manual', headers });
};
}
describe('production smoke and SEO contract', () => {
it('verifies canonical metadata, response policy, compression, MIME, ranges, and one-hop redirects', async () => {
const fetchImpl = await fixtureFetch();
await expect(verifyProductionSmoke({ canonicalUrl: canonical, alternateUrls: [alternate], expectedManifestDigest: digest, videoPath: '/assets/demo.12345678.webm', fetchImpl })).resolves.toMatchObject({ manifestDigest: digest, canonicalUrl: canonical, jsDisabledContentVerified: true });
});
it('rejects a deployed digest mismatch and redirect chains', async () => {
const fetchImpl = await fixtureFetch();
await expect(verifyProductionSmoke({ canonicalUrl: canonical, alternateUrls: [alternate], expectedManifestDigest: 'b'.repeat(64), videoPath: '/assets/demo.12345678.webm', fetchImpl })).rejects.toThrow(/manifest-digest|manifest digest/u);
});
});

View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['tests/deployment/**/*.spec.ts'],
},
});