Files
arah2/docs/PRIVACY-AUDIT.md
araharah-executor 2489cca8ec fix(07-06): the /help doc pointer is a documented static-text class — privacy audit green again
- source-side runtime-hosts gate gains DOC_POINTER_STATIC_TEXT_HOSTS (the
  MapLibre-attribution precedent): the published repo origin may appear
  ONLY as a bare URL literal in bot copy (D-08/D-09), never fetched
- class is NARROW and pinned by SA6: a fetch( target naming the same
  host is still a violation
- Rule 1 (found by SA6's RED): the fetch( handler's declaration
  heuristic misclassified any quoted absolute URL as a TS declaration —
  quoted strings are targets; they now reach the host check
- docs/PRIVACY-AUDIT.md: adjudication documented + dated re-run addendum
- deferred-items.md #1 closed: npm run privacy-audit exits 0 again
  (PRIVACY AUDIT CLEAN — 5 gate(s) passed, 0 violations; suite 13/13)
2026-10-25 05:18:47 +08:00

13 KiB
Raw Permalink 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. The SOURCE-side runtime-hosts gate carries the same kind of adjudication for the /help Known-limits doc pointer (see the 2026-10-25 addendum below): the published repo origin appears ONLY as a bare URL literal in the bot strings — user-clicked navigation, never fetched — and the class is NARROW (a fetch( target naming the same host is still a violation; pinned by the SA6 vector).

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
gitmirah (the published repo origin) documented static-text class (SOURCE-side, URL literals only) the /help Known-limits doc pointer in bot/src/i18n/strings.ts (D-08/D-09) — user-facing copy in a chat card, never fetched; a fetch( to the same host is still a violation (SA6)

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.

2026-10-25 — Phase 7 gate-prep re-run: the /help doc pointer adjudication

Why this addendum exists: plan 07-05's committed /help Known-limits pointer (commit 78482ab) put the repo's actual published-origin URL (http://gitmirah:3000/c-fu/arah2/…, the maintainer's Gitea remote per git remote -v) into the bot strings (en + ms), and the runtime-hosts gate — a positive allowlist with no static-text escape — flagged it as 2 violations (PRIVACY AUDIT FAILED). Logged in the phase's deferred-items.md; fixed by the 07-06 gate prep, following the audit's OWN laws (the MapLibre static-text adjudication precedent), not by weakening them.

The fix (source-side, scripts/lib/source-gates.mjs):

  • A documented DOC_POINTER_STATIC_TEXT_HOSTS class on the SOURCE-side runtime-hosts gate: the published repo origin may appear ONLY as a bare URL literal in bot copy (the /help Known-limits line — user-clicked navigation, never fetched). Each entry names its reason; forks replace it with their own published origin.
  • The class is NARROW by construction and pinned by the SA6 vector: it is consulted ONLY for bare URL literals. A fetch( target naming the same host is STILL a violation. This required fixing a latent gate bug (SA6's RED proved it): the fetch( call-site handler's declaration heuristic (argText.includes(":")) misclassified any QUOTED absolute URL as a TS method declaration and skipped it — a quoted string is always a target, so quoted fetch targets now reach the host check.
  • Result: npm run privacy-audit → runtime-hosts: PASS — host inventory (printed, not assumed): www.w3.org×3, gitmirah×2, 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] or the documented doc-pointer static-text class (URL literals only — never fetched) → PRIVACY AUDIT CLEAN — 5 gate(s) passed, 0 violations; the suite is 13/13 (SA1–6, BG1–2, CP1–2, AU1–3). The Findings table above remains the 07-03 run's historical record; this section is the gate-time re-run record.

Driver-resolution caveat (unchanged from 07-05's decision): the URL resolves for a driver only once the D-09 launch checklist's "repo pushed public" step lands main on the remote — the launch gate verifies the pointer line resolves before signing.

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:       ________________________________