Files
arah2/docs/PRIVACY-AUDIT.md
T
araharah-executor 8229e3656e docs(07-03): the signed privacy-audit record — real run evidence, D-01/D-02 (PRIV-02 + GEO-03 re-verified end-to-end)
- 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)
2026-10-25 04:40:02 +08:00

9.9 KiB
Raw Blame History

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

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 <script> tag in web/index.html
the app's own Worker origin fetch allowlist (relative) /api/session, /api/location, /api/location-fallback, /api/guidance, /api/route — RELATIVE paths, not hosts
maplibre.org, wiki.openstreetmap.org, github.com, maplibre.invalid documented static-text class MapLibre's own attribution/diagnostic constants inside the bundled library — displayed text, never fetched
opendatacommons.org documented static-text class our own gazetteer/fixture ODbL license metadata strings
www.w3.org documented spec identifiers SVG/XHTML namespace constants — never fetched

ZERO telemetry (D-02): no analytics, no beacons, no tracking pixels, no third-party CDN, no error-reporting host, no social pixels — the shipped bundle contains no analytics or beacon token of any kind, and no fetch to any host outside the three-row fetch allowlist above. v1 has nothing to opt into: opt-in telemetry stays an IDEAS.md aspiration post-validation.

Statement

PRIV-02 (storage is TTL-only, per-user data never outlives its window) and GEO-03 (the client persists nothing; coordinates live in memory client-side and in a 300 s TTL pin server-side) are re-verified END-TO-END by this audit, not asserted: the storage-api gate scans all 72 files across the five production trees (comments stripped) for any persistence API or service-worker surface; the kv-ttl gate proves all 4 KV write paths carry a same-line expirationTtl referencing a named constant; the log-vocabulary gate machine-verifies that none of the 28 log call sites can carry a coordinate, an identifier, or a payload; the runtime-hosts gate proves the source tree only ever fetches the app's own Worker; and the bundle-telemetry gate proves the SHIPPED bundle — the artifact that actually decides the app's runtime network — contains zero analytics, zero beacons, zero storage APIs, zero deep-link crypto, and no fetch host beyond the OpenFreeMap tiles, the Telegram SDK, and the Worker itself. The live legs (KV listing with expiry metadata, deployed cache headers) were observed NOT RUN in this environment and are re-run with npm run privacy-audit -- --live at the launch gate — exit 0 only when they PASS, never when they are skipped.

Signature

The audit record is SIGNED BY THE HUMAN at the launch gate (the tool produces the evidence; the human signs the claim):

Maintainer: ________________________________

Date:       ________________________________