--- phase: 07-hardening-private-launch plan: 03 subsystem: testing tags: [privacy-audit, grep-gates, comment-stripping, kv-ttl, zero-telemetry, allowlist, wrangler, cloudflare-kv, curl-probe] # Dependency graph requires: - phase: 03-mini-app-shell-geolocation provides: the memory-only client law + the canonical comment-stripper (web/test/no-storage.test.mjs, D-07 origin) - phase: 02-bot-skeleton-initdata-security provides: the PG1/PG2 TTL-only KV write law (bot/test/privacy.test.mjs) and the D-14 category-only log vocabulary (bot/src/webhook.ts) - phase: 05/06 routing + guidance provides: the route-privacy RP1/RP2 allowlist style and the TTL key families the audit probes (bot/src/kv/*) provides: - "The ONE consolidated audit CLI: npm run privacy-audit (scripts/privacy-audit.mjs) — 5 static gates + 2 --live probes, exit codes 0/1/2 (never a silent pass)" - "scripts/lib/source-gates.mjs — storage-api / kv-ttl / log-vocabulary / runtime-hosts gates extended repo-wide over ALL five src trees" - "scripts/lib/bundle-gates.mjs — PRODUCTION web/dist telemetry scan (analytics/beacon tokens, positive host allowlist, deep-link crypto, storage tokens)" - "scripts/lib/cloudflare-probes.mjs — live kv-expiry (list_complete/cursor/expiration parsing) + live asset-headers probes, degrading to notRun without credentials" - "docs/PRIVACY-AUDIT.md — the signed written audit record citing the ACTUAL run evidence (D-01), with a human signature block for the launch gate" affects: [07-04 (budget-audit shares the _headers/live-probe surface), 07-05 (limitations doc), 07-06 (launch gate — the privacy-audit-signed checklist leg), any fork onboarding] # Tech tracking tech-stack: added: [] # zero new dependencies — node:fs/node:child_process/curl/npx wrangler only patterns: - "Gate-as-function returning {id, pass, evidence, violations} (07-RESEARCH Pattern 4 shape)" - "Canonical comment-stripped source introspection with documented prefer-fail adjudications (theme-audit 'Scope adjudications' precedent)" - "Positive host allowlist with the inventory PRINTED, never assumed (A4)" - "notRun as the exit-2 class: a skipped probe is never a pass (T-07-16)" - "PRIVACY_AUDIT_ROOT env override — cwd-independent gates, fork/test injectability" key-files: created: - scripts/privacy-audit.mjs - scripts/lib/source-gates.mjs - scripts/lib/bundle-gates.mjs - scripts/lib/cloudflare-probes.mjs - scripts/test/privacy-audit.test.mjs - docs/PRIVACY-AUDIT.md modified: - package.json (the root "privacy-audit" script) key-decisions: - "Bundle host scan adjudication: MapLibre's own static-text URLs (maplibre.org, wiki.openstreetmap.org, github.com diagnostics, maplibre.invalid) + our ODbL license strings (opendatacommons.org) form a DOCUMENTED static-text class alongside the 2-host fetch allowlist — each host enumerated with its reason; any other host fails" - "W3C namespace identifiers (svg/xhtml) excepted as spec constants, never fetched — documented in both gate files" - "Line-numbered gates scan raw lines with only the // tail removed (honest line numbers); block-comment prose mentioning a scanned token fails prefer-fail" - "Asset-headers probe reads HTTP status BEFORE interpreting headers — a stale-deploy 404 is deployment drift (notRun, exit-2 class), never a cache violation" - "The audit suite is a hand-rolled 12-vector TDD file (scripts/test/privacy-audit.test.mjs) with fixture-root injection, proving each gate can FAIL — a gate that cannot fail is decoration" patterns-established: - "PRIVACY AUDIT CLEAN/FAILED/NOT RUN — the consolidated audit's summary vocabulary + 0/1/2 exit contract" - "Gates are cwd-INDEPENDENT (module-URL root) so a stray subdirectory run cannot false-pass" requirements-completed: [PX-04] # PX-04 claimed here: SC3 is a Phase-7 success criterion under the PX-04 phase; PRIV-02 + GEO-03 are re-verified end-to-end (D-01), not re-claimed # Metrics duration: 9min completed: 2026-10-24 --- # Phase 7 Plan 03: Consolidated privacy audit + signed record Summary **One consolidated `npm run privacy-audit` CLI — 5 static gates over all five src trees + the production bundle, 2 credential-gated live probes, honest 0/1/2 exit codes — plus `docs/PRIVACY-AUDIT.md`, the signed audit record citing the real run's evidence (D-01/D-02).** ## Performance - **Duration:** 9 min (20:32Z → 20:41Z) - **Started:** 2026-10-24T20:32:07Z - **Completed:** 2026-10-24T20:41:00Z - **Tasks:** 3 (TDD RED→GREEN) - **Files modified:** 7 (6 created, 1 modified) ## Accomplishments - The repo's gate lineage consolidated, not duplicated: the phase-3 memory-only law, the PG1 same-line TTL law, the D-14 category-only log law, and the bundle laws are now enforced REPO-WIDE by one tool — the existing unit-level suites stay as the pins - All 5 static gates PASS on the audited tree (commit `9c55e9a`): 72 files scanned, 4 KV writes all TTL-named, 28 log call sites category-verified, host inventories printed (source + bundle), ZERO analytics/beacon/crypto/storage tokens in `web/dist` - `docs/PRIVACY-AUDIT.md` carries the ACTUAL evidence strings from the run, the four KV-family storage inventory, the log vocabulary, the runtime-network inventory with D-02's zero-telemetry posture, and the blank human signature block (D-09's launch gate signs it) - Live probes verified honest-by-construction: without credentials both return notRun and the CLI exits 2 — never a silent pass (T-07-16) ## task Commits Each task was committed atomically (TDD: RED gate commit → GREEN feat commits): 1. **task 1: the source-gates library** — `87fcb28` (test: RED — 12-vector suite) + `430993b` (feat) 2. **task 2: bundle-gates + cloudflare-probes** — `b959e23` (feat) 3. **task 3: the CLI + npm script + signed record** — `9c55e9a` (feat) + `8229e36` (docs) ## Files Created/Modified - `scripts/lib/source-gates.mjs` — runStorageGate / runKvTtlGate / runLogVocabularyGate / runRuntimeHostGate over web/bot/routing/search/theme src - `scripts/lib/bundle-gates.mjs` — runBundleTelemetryGate (PRODUCTION `web/dist` ONLY; `dist-harness` forbidden) - `scripts/lib/cloudflare-probes.mjs` — runKvExpiryProbe / runAssetHeaderProbe / probeUnavailable (namespace id read from `bot/wrangler.jsonc` at runtime) - `scripts/privacy-audit.mjs` — the orchestrator: Pattern-4 ordering, per-gate evidence lines, CLEAN/FAILED/NOT RUN summaries, process.exit 0/1/2 - `scripts/test/privacy-audit.test.mjs` — the 12-vector TDD suite (SA1-5, BG1-2, CP1-2, AU1-3) - `package.json` — root `"privacy-audit": "node scripts/privacy-audit.mjs"` - `docs/PRIVACY-AUDIT.md` — the signed audit record (D-01) ## Decisions Made - **Bundle static-text adjudication:** the plan's 2-host allowlist fails on MapLibre's own embedded attribution/diagnostic URLs in the shipped bundle (they are library constants and our dataset's ODbL metadata — never fetched). Followed the repo's own "Scope adjudications (documented, deliberate)" precedent (theme/scripts/audit.mjs): an explicitly enumerated static-text class, each host with its reason, full inventory printed as evidence. Any host outside both lists fails. - **Header probe honesty:** the first live run exposed the probe interpreting a 404 as "no cache-control" — the probe now reads HTTP status first and treats origin/build mismatch as deployment drift (exit-2 class). The immutable `/assets/*` policy ASSERTION belongs to 07-04's budget-audit (its `_headers` file); this probe quotes, never interprets. - **Test injectability:** gates accept an opts-only roots/dist override (production callers pass nothing), letting the suite prove each gate FAILS on synthetic violations in temp fixtures. - **cwd-independent root** with the `PRIVACY_AUDIT_ROOT` override — a stray subdirectory run must not scan the wrong tree and false-pass. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Asset-header probe misread a stale-deploy 404 as a cache violation** - **Found during:** task 3 (first `--live` run) - **Issue:** the probe fetched the local dist's hashed asset from the deployed origin without checking HTTP status; a 404 printed "no cache-control header observed" and FAILED the gate with `undefined violation(s)` (also a `${violations.length}` bug on a number) - **Fix:** status line read before header interpretation; non-2xx = notRun (deployment drift) with observed headers carried in evidence; the count bug fixed - **Files modified:** scripts/lib/cloudflare-probes.mjs - **Verification:** `npm run privacy-audit -- --live` now exits 2 with `PRIVACY AUDIT NOT RUN — deployment drift …`; CP2 vector pins the shape - **Committed in:** b959e23/9c55e9a follow-ups (folded into the task-2/3 commits) --- **Total deviations:** 1 auto-fixed (1 bug) **Impact on plan:** The fix makes the live probe's honesty contract hold end-to-end. The live legs were recorded NOT RUN in docs/PRIVACY-AUDIT.md — honest, exit-2 class, re-run at the launch gate. ## Issues Encountered - The deployed Worker (`araharah-bot.workers.dev`) serves a bundle older than the current `web/dist` (its hashed asset 404s) — correctly reported as deployment drift by the probe; a redeploy (07-04/07-06) plus shipped `_headers` will turn the live legs green. ## Known Stubs None — every gate/probe is fully wired; no placeholder data flows anywhere. ## User Setup Required None — no external service configuration required. (The `--live` probes need `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` in the environment; absent credentials degrade to exit 2 by design, never a pass.) ## Next Phase Readiness - 07-04 (budget-audit) re-runs the live header probe against a deployed build carrying its `_headers` policy — the two plans share the live-probe surface - 07-06 (launch gate) signs `docs/PRIVACY-AUDIT.md`'s signature block as the privacy-audit checklist leg; the `--live` re-run protocol is documented in the record - Baselines intact: bot 125/125, web 165/165; `npm run privacy-audit` exits 0 (PRIVACY AUDIT CLEAN — 5 gate(s) passed, 0 violations) --- *Phase: 07-hardening-private-launch* *Completed: 2026-10-24* ## Self-Check: PASSED All 7 created files verified on disk; all 5 task commits verified in git log (`87fcb28` test-RED, `430993b`, `b959e23`, `9c55e9a`, `8229e36`). STATE.md and ROADMAP.md untouched (orchestrator-owned).