// Scrub for the public /bench page (review round 4, R4.6.1). The same rules the public repository export applies // (its sync.sh generic scrub and bench-log rules, and the maintainers' local rules), plus site-only ones, in plain Node // so the build runs the same on this Mac, on Vercel and on the CI runner. The build fails if any pattern in // site/forbidden-strings.txt survives, so a private name, host or address can never reach the page. import { readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const utc = (h, m, s) => `${String((Number(h) + 23) % 24).padStart(2, '0')}:${m}${s || ''} UTC`; const RULES = [ // machines: model names only (6 October 2026, site audit: the two Windows machines are described, never numbered) [/\bPC 1's\b/g, "the three-card Windows rig's"], [/\bPC 1\b/g, 'the three-card Windows rig (RTX 5090, RTX 4070, RX 9070 XT)'], [/\bPC 2's\b/g, "the RTX 5090 Windows rig's"], [/\bPC 2\b/g, 'the RTX 5090 Windows rig'], [/the PC node at 192\.168\.[0-9.]+/g, 'the RTX 5090 node on the LAN'], [/\bthe PC node\b/g, 'the RTX 5090 node'], [/\bWindows PC\b/g, 'an RTX 5090 on Windows'], [/\bthe PC's\b/g, "the RTX 5090 machine's"], [/\bthe PC\b/g, 'the RTX 5090 machine'], [/\bPC (joins|start|period)\b/g, 'RTX 5090 $1'], [/\(Mac side only;/g, '(Apple M5 Max side only;'], [/\bthe Mac's\b/g, "the Apple M5 Max's"], [/\bthe Mac\b/g, 'the Apple M5 Max'], [/\bJosh's\b/g, "the maintainers'"], [/\bJosh\b/g, 'the maintainers'], [/ Not deployed to Vercel tonight\./g, ''], // addresses, hosts, paths [/192\.168\.[0-9]+\.[0-9]+/g, ''], [/\b100\.[0-9]+\.[0-9]+\.[0-9]+\b/g, ''], [/DESKTOP-[A-Z0-9]{7}/g, ''], [/igneum-seed-\d+/g, 'the seed node'], [/\bHetzner\b/g, 'the cloud provider'], [/\bhcloud\b/g, 'the cloud CLI'], [/\/root\/[A-Za-z0-9_./-]*/g, ''], [/\/opt\/igneum[A-Za-z0-9_./-]*/g, ''], [/dl\.igneum\.network[^ )`]*/g, 'the downloads host'], [/,? ?log intake id \d+/g, ''], [/\bintake id \d+\b/g, 'intake'], // the site audit's page-leak shapes (6 October 2026): process ids, listen addresses, config paths, the node's listen flag [/\bpid \d+\b/g, 'pid '], [/0\.0\.0\.0:\d+/g, ''], [/--rpclisten=\S*/g, 'the RPC listen flag'], [/~\/\.config\/[A-Za-z0-9_./-]*/g, ''], [/~\/\.config/g, ''], [/~\/Desktop\//g, '`'], [/``/g, '`'], [/~\/\.cargo\/bin\/cargo/g, 'cargo'], [/\/Users\/[A-Za-z0-9_.-]+/g, '~'], [/C:\\Users\\[A-Za-z0-9_.-]+/g, '%USERPROFILE%'], // unpublished documents [/`docs\/fud-ledger\.md`/g, 'the break ledger (kept by the maintainers, not yet published)'], [/\(CLAUDE\.md, /g, '(the design document, '], [/\bCLAUDE\.md\b/g, 'the design document'], // local times to UTC (the log is written in UTC+1) [/2,193 blocks at 22:35;/g, '2,193 blocks at 21:35 UTC;'], [/(\d{1,2}:\d{2}(?::\d{2})? UTC) = \d{1,2}:\d{2} B[S]T/g, '$1'], [/(\d{1,2}):(\d{2})(:\d{2})? to (\d{1,2}):(\d{2})(:\d{2})? B[S]T/g, (_, h1, m1, s1, h2, m2, s2) => `${utc(h1, m1, s1).replace(' UTC', '')} to ${utc(h2, m2, s2)}`], [/(\d{1,2}):(\d{2})(:\d{2})? B[S]T/g, (_, h, m, s) => utc(h, m, s)], [/\(local time, UTC\+1\)/g, '(UTC)'], [/\bB[S]T\b/g, 'UTC'], ]; export function scrubBench(text) { let out = text; for (const [re, rep] of RULES) out = out.replace(re, rep); const pats = readFileSync(join(here, 'forbidden-strings.txt'), 'utf8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')); const hits = []; out.split('\n').forEach((line, i) => { for (const p of pats) if (new RegExp(p).test(line)) { hits.push(`${i + 1}: ${p}`); break; } }); if (hits.length) throw new Error(`scrub: forbidden strings remain on the bench page:\n${hits.slice(0, 20).join('\n')}`); return out; }