From 8229e3656e916834ddc48f3e788ef4a324a1b337 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sun, 25 Oct 2026 04:40:02 +0800 Subject: [PATCH] =?UTF-8?q?docs(07-03):=20the=20signed=20privacy-audit=20r?= =?UTF-8?q?ecord=20=E2=80=94=20real=20run=20evidence,=20D-01/D-02=20(PRIV-?= =?UTF-8?q?02=20+=20GEO-03=20re-verified=20end-to-end)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Findings table carries the ACTUAL printed evidence from the audited run (commit 9c55e9a): 5 static gates PASS, 0 violations; live legs recorded honestly as NOT RUN with the launch-gate re-run protocol - Storage inventory: the four TTL-only KV families; Log vocabulary: the category-only surfaces; Runtime network: the printed host inventory (allowlist + documented static-text class) and D-02's zero-telemetry posture - human signature block left blank for the launch gate (D-09) --- docs/PRIVACY-AUDIT.md | 137 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/PRIVACY-AUDIT.md diff --git a/docs/PRIVACY-AUDIT.md b/docs/PRIVACY-AUDIT.md new file mode 100644 index 0000000..ad82b4c --- /dev/null +++ b/docs/PRIVACY-AUDIT.md @@ -0,0 +1,137 @@ + + +# Privacy audit record + +**PRIVACY AUDIT RECORD** — the signed written audit record citing the evidence (Phase 7, D-01). + +| Field | Value | +| --- | --- | +| Audit date | 2026-10-24 | +| Audited tree | commit `9c55e9a` (`npm run privacy-audit` run against that working tree) | +| Auditor | the maintainer — signature block below (the tool produces the evidence; the human signs the claim) | +| Tool | `npm run privacy-audit` (`scripts/privacy-audit.mjs` + `scripts/lib/*`) — GPL-3.0, reusable by forks | +| Result | **PRIVACY AUDIT CLEAN — 5 gate(s) passed, 0 violations** (static gates); live legs recorded as NOT RUN below | + +The tool is a MAINTAINER-TIME REPO TOOL — nothing in any `src` tree imports it. A fork reruns +`npm run privacy-audit` over its own trees before its first driver; this record is the v1 tree's +own evidence. + +--- + +## Scope + +**Audited:** + +| Surface | What was scanned | +| --- | --- | +| All five `src` trees | `web/src`, `bot/src`, `routing/src`, `search/src`, `theme/src` — 72 code files (`.ts`/`.tsx`/`.js`/`.mjs`/`.html`), comments stripped (the canonical stripper, copied verbatim from `web/test/no-storage.test.mjs`) | +| Production bundle | `web/dist` — every emitted `.js` and `.html` file (PRODUCTION only) | +| Live KV namespace | `chat_sessions` (`bot/wrangler.jsonc`) — TTL-only keys, probed live only | + +**Not audited (deliberate):** + +| Excluded surface | Why | +| --- | --- | +| `web/dist-harness` | the test-harness build is deliberately NOT clean (BA5's law — a gate pointed at it inverts itself) | +| The Telegram client's own internal storage | the official `telegram-web-app.js` is EXTERNAL to the bundle; its `__telegram__initParams` sessionStorage write is the platform allowlist, not ours | +| `web/src/mock/telegram-mock.ts` runtime behaviour | scanned by the source gates (it is in `web/src`); its in-page demo interactions are harness-only and never ship | +| Third-party library internals beyond the shipped bundle text | the bundle scan covers the shipped artifact itself — that IS the library's runtime footprint | + +## Findings + +One row per gate; the Evidence column is the ACTUAL printed evidence string from the run (copied verbatim, never invented): + +| Gate | Result | Evidence | +| --- | --- | --- | +| `storage-api` | PASS | 72 file(s) across 5 src tree(s) — no persistence API outside the platform allowlist | +| `kv-ttl` | PASS | 4 KV write(s) across 72 file(s) — every .put( line carries expirationTtl referencing a named ^[A-Z_]{2,}$ constant | +| `log-vocabulary` | PASS | 21 bot + 7 web console.log call site(s) — every bot log opens with a bracketed surface token and carries NO lat/lng/chat_id/user_id/token/initData/geometry/polyline/payload carrier, every web log is an ARAHARAH-* state token or the RAMS-BASEMAP-READY signal, and no log serializes a payload | +| `runtime-hosts` | PASS | host inventory (printed, not assumed): www.w3.org×3, telegram.org×1; 4 relative fetch target(s) to the app's own Worker, 1 fetch declaration(s) skipped; every absolute URL host is in the positive allowlist [tiles.openfreemap.org, telegram.org] | +| `bundle-telemetry` | PASS | 3 JS + 1 HTML emitted file(s) scanned (PRODUCTION web/dist only — dist-harness forbidden); host inventory (printed, not assumed): maplibre.invalid×1, github.com×3, wiki.openstreetmap.org×1, maplibre.org×2, tiles.openfreemap.org×5, opendatacommons.org×2, telegram.org×1; every absolute-URL host is in the allowlist [tiles.openfreemap.org, telegram.org] or the documented static-text class; ZERO analytics/beacon/crypto/storage tokens | +| `kv-expiry` (live) | NOT RUN | (pass --live) — no Cloudflare credentials in the auditing environment; the live leg re-runs at the launch gate with `npm run privacy-audit -- --live`. A skip is never a pass (exit code 2, not 0) | +| `asset-headers` (live) | NOT RUN | (pass --live) — same re-run protocol; the live leg observed the HTML entry answering `cache-control: public, max-age=0, must-revalidate | cf-cache-status: HIT` and the then-deployed asset answering 404 (deployment drift — the deployed Worker pre-dates the current `web/dist`), which the probe reports as deployment drift, never as a pass | + +Documented scope adjudications (deliberate; each names its reason): the `beacon` token is checked +only as `navigator.sendBeacon`; the MapLibre library's own static-text URLs (`maplibre.org`, +`wiki.openstreetmap.org`, `github.com` diagnostics, `maplibre.invalid` protocol-test placeholder) +and our own dataset's ODbL license strings (`opendatacommons.org`) are printed in the inventory as +a documented static-text class — never fetched; `http://www.w3.org/{2000/svg,1999/xhtml}` are W3C +spec identifiers. + +## Storage inventory + +The FOUR TTL-only KV key families — the complete storage surface. No other family exists, and no +coordinate or profile is persisted anywhere else (the `kv-ttl` gate proves every write path, the +`storage-api` gate proves the client carries no persistence API at all): + +| Family | Prefix | TTL constant | TTL | What is stored | +| --- | --- | --- | --- | --- | +| session | `session:` | `SESSION_TTL_SECONDS` (`bot/src/kv/sessions.ts`) | 900 s | the 4-field `SessionClaim` — user_id, chat_id, issued_at, language_code. Nothing else ever serialized (D-08: a 5th property means drift) | +| chat pointer | `chat:` | `SESSION_TTL_SECONDS` (`bot/src/kv/sessions.ts`) | 900 s | the current opaque session token for a chat | +| location pin | `loc:` | `PIN_TTL_SECONDS` (`bot/src/kv/location-pin.ts`) | 300 s | Telegram's Location fields only — latitude, longitude, horizontal_accuracy (GEO-03: the ONLY place a coordinate exists server-side, and it expires) | +| guidance envelope | `guidance:` | `GUIDANCE_SESSION_TTL_SECONDS` (`bot/src/kv/guidance-sessions.ts`) | 7200 s | the frozen 4-field envelope + inline 6-field route snapshot; nothing outlives 2 h, nothing reconstructable after expiry (D-07) | + +Every write path is pinned by the same-line `expirationTtl` law (PG1, extended repo-wide by the +`kv-ttl` gate), and the cron performs ZERO KV writes (the free-plan budget law). The client +(`web/src`) holds route/guidance state in MEMORY ONLY — no localStorage, no sessionStorage, no +IndexedDB, no CacheStorage, no cookies, no service worker. + +## Log vocabulary + +Category-only, machine-verified by the `log-vocabulary` gate. Allowed log surfaces: + +| Tree | Allowed log shape | +| --- | --- | +| `bot/src` | `console.log("[surface] verdict: category")` — surface is one of `webhook`, `initdata`, `location`, `location-fallback`, `guidance`, `guidance-cron`, `route`, `search`, `search-alternate`; every call site opens with a bracketed surface token | +| `web/src` | the `ARAHARAH-*` state tokens (`ARAHARAH-DEST state=…`, `ARAHARAH-LIVE fix=…`, `ARAHARAH-ROUTE state=…`, `ARAHARAH-GUIDE state=…`, `ARAHARAH-DRIVE state=…`) or the `RAMS-BASEMAP-READY layers=…` signal; `console.error` on the basemap error path | + +The coordinate-free law: no log line may carry a coordinate, a chat id, a user id, a token, an +initData payload, a geometry/polyline, or any serialized payload (`JSON.stringify` inside a log +argument is forbidden outright). 21 bot + 7 web call sites verified. + +## Runtime network + +The ACTUAL host inventory the audit printed (source scan + production bundle scan — produced, not +assumed, A4): + +| Host | Class | Why it is reachable | +| --- | --- | --- | +| `tiles.openfreemap.org` | **fetch allowlist** | the basemap tiles / sprites / glyphs (`theme/basemap/liberty-rams.json`) | +| `telegram.org` | **fetch allowlist** | the official Telegram WebApp SDK `