import { createHash, randomUUID } from 'node:crypto'; import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; import { parseSiteConfig } from '../src/content/schemas.ts'; import { serializeStaticHostHeaders } from '../src/security/security-policy.ts'; import { parseBuildAuthority } from './build-contract.mjs'; import { prerender } from './prerender.mjs'; import { verifyArtifacts } from './verify-artifacts.mjs'; const scriptPath = fileURLToPath(import.meta.url); const root = resolve(dirname(scriptPath), '..'); const configPath = 'config/site.production.build.json'; const output = resolve(root, 'dist-cloudflare'); const rootFiles = ['index.html', 'robots.txt', 'sitemap.xml']; const prohibited = /__portfolio(?:_|JourneyInstrumentation)|PORTFOLIO_TEST_(?:BUILD|TOKEN)|drive-trace\.v1|sourceMappingURL|-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----|\bgh[pousr]_[A-Za-z0-9]{36,}\b|\bgithub_pat_[A-Za-z0-9_]{40,}\b|\bAKIA[A-Z0-9]{16}\b|\/home\/luca\//iu; export const assertProductionEnvironment = (environment) => { if (Object.entries(environment).some(([key, value]) => value && (/^(?:PORTFOLIO_TEST_|VITE_)/u.test(key) || (key === 'NODE_ENV' && value !== 'production')))) { throw new Error('Production export rejects test flags, VITE_* overrides and non-production NODE_ENV.'); } }; export const assertPublicFiles = (files, expectedPaths) => { const expected = [...new Set(expectedPaths)].sort(); if (JSON.stringify([...files.keys()].sort()) !== JSON.stringify(expected)) { throw new Error('Cloudflare export inventory differs from the verified public closure.'); } if (files.size > 20_000) throw new Error('Cloudflare free static file limit exceeded.'); for (const [path, bytes] of files) { if (bytes.length > 25 * 1024 * 1024) throw new Error(`Cloudflare asset exceeds 25 MiB: ${path}`); if (prohibited.test(bytes.toString('utf8'))) throw new Error(`Prohibited production surface in ${path}.`); } }; // Revalidate all URLs: approved media have stable, unhashed names. One rule avoids // Cloudflare combining contradictory Cache-Control values from matching rules. export const renderCloudflareHeaders = (site, hash) => serializeStaticHostHeaders(site, hash); const regularBytes = async (path) => { const stats = await lstat(path); if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) throw new Error('Export input must be a regular, non-linked file.'); return readFile(path); }; const expectedExport = async () => { await verifyArtifacts(configPath); const site = parseSiteConfig(JSON.parse(await regularBytes(resolve(root, configPath)))); if (site.environment !== 'production') throw new Error('Cloudflare export requires production inputs.'); const authority = parseBuildAuthority(JSON.parse(await regularBytes(resolve(root, 'dist-ssr/artifact-authority.json')))); const files = new Map(); for (const path of [...rootFiles, ...authority.artifactContract.files.map((file) => file.path)]) { files.set(path, await regularBytes(resolve(root, 'dist', path))); } const html = files.get('index.html').toString('utf8'); const jsonLd = html.match(/