#!/usr/bin/env node /* * privacy-audit.mjs — the ONE consolidated privacy audit (07-03, D-01/D-02). * --------------------------------------------------------------------------- * A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src, * web/src, routing/src, or search/src imports it. GPL-3.0: a fork reruns * `npm run privacy-audit` over its own trees before its FIRST driver — the * audit doubles as the fork onboarding check. * * D-02: v1 carries ZERO telemetry — opt-in telemetry stays an IDEAS.md * aspiration post-validation; there is nothing to opt into yet. This audit * proves an ABSENCE, it does not audit a consent surface that does not exist: * no analytics, no beacon, no fetch to any host outside the positive * allowlist, no persistence outside the TTL-only KV families, no coordinate * or payload in any log line. * * What it runs, in order (07-RESEARCH Pattern 4): * runStorageGate() id: storage-api — the memory-only law, repo-wide * runKvTtlGate() id: kv-ttl — the PG1 same-line TTL law, repo-wide * runLogVocabularyGate() id: log-vocabulary — the D-14 category-only law * runRuntimeHostGate() id: runtime-hosts — the POSITIVE fetch-host allowlist * runBundleTelemetryGate() id: bundle-telemetry — the PRODUCTION web/dist scan * [--live] runKvExpiryProbe() id: kv-expiry — LIVE: expiry metadata + cursor * [--live] runAssetHeaderProbe() id: asset-headers — LIVE: cache headers VERBATIM * * Usage: node scripts/privacy-audit.mjs [--live] (the `privacy-audit` npm script) * * Exit codes (no silent passes — the theme-audit contract, T-1-07): * 0 clean — every required gate PASSed * 1 violations — each printed as a per-gate evidence line above * 2 NOT RUN — environment/credential drift (a --live probe could not run * because credentials are absent, or the build is absent); a skipped * probe is NEVER mistaken for a pass (T-07-16) * * PRIVACY_AUDIT_ROOT: repo root override (test suite / fork checkouts). The * gates are otherwise cwd-INDEPENDENT — a stray subdirectory run must not * scan the wrong tree and false-pass. * * Category-only output law: this script NEVER prints a coordinate, a chat * id, a user id, a token, or an initData payload — its evidence is * category-only, exactly like the bot's logs (D-14). */ import { runStorageGate, runKvTtlGate, runLogVocabularyGate, runRuntimeHostGate, } from "./lib/source-gates.mjs"; import { runBundleTelemetryGate } from "./lib/bundle-gates.mjs"; import { runKvExpiryProbe, runAssetHeaderProbe } from "./lib/cloudflare-probes.mjs"; const live = process.argv.includes("--live"); /** The five static gates — always required. */ const results = [ await runStorageGate(), await runKvTtlGate(), await runLogVocabularyGate(), await runRuntimeHostGate(), await runBundleTelemetryGate(), ]; if (live) { results.push(await runKvExpiryProbe(), await runAssetHeaderProbe()); } else { // Flag-skip placeholders — visible, honest, and NEVER green: the skip is // the operator's explicit choice (no --live), not an environment failure, // so it is excluded from the exit-2 class below. results.push( { id: "kv-expiry", pass: false, notRun: true, flagSkip: true, evidence: "(pass --live)" }, { id: "asset-headers", pass: false, notRun: true, flagSkip: true, evidence: "(pass --live)" }, ); } for (const result of results) { const status = result.pass ? "PASS" : result.notRun ? "NOT RUN" : "FAIL"; console.log(`${result.id}: ${status} — ${result.evidence}`); } const failed = results.filter((r) => !r.pass && !r.notRun); const notRun = results.filter((r) => !r.pass && r.notRun && !r.flagSkip); const passed = results.filter((r) => r.pass).length; const totalViolations = results.reduce((sum, r) => sum + (r.violations ?? 0), 0); if (failed.length > 0) { console.log(`PRIVACY AUDIT FAILED — ${totalViolations} violation(s) across ${failed.length} gate(s)`); process.exit(1); // violations — each printed above } if (notRun.length > 0) { const reasons = notRun.map((r) => `${r.id}: ${r.evidence}`).join("; "); console.log(`PRIVACY AUDIT NOT RUN — ${reasons} (environment drift; never a pass)`); process.exit(2); // environment/credential drift — fix the environment and rerun } console.log(`PRIVACY AUDIT CLEAN — ${passed} gate(s) passed, 0 violations`); process.exit(0); // clean