# Phase 7: Hardening + Private Launch - Research **Researched:** 2026-10-25 **Domain:** WebView fullscreen/landscape hardening, failure-state QA, privacy/budget audit, free-tier launch gates **Confidence:** HIGH for repository structure and documented platform limits; MEDIUM for per-client fullscreen behavior until the real-device walk; MEDIUM for battery result because it must be measured ## User Constraints (from CONTEXT.md) ### Locked Decisions ### Privacy audit (SC3) - **D-01:** A **consolidated automated audit script** (`npm run privacy-audit`, repo tool): grep gates over ALL src trees (no storage-API writes outside the platform allowlist — the phase-3 gate's law extended repo-wide; no coordinate/payload logging — the category-only vocabulary verified by schema scan), a KV listing probe (only TTL-keyed namespaces), a bundle scan (**zero telemetry: no analytics, no beacons — the ONLY runtime fetches are OpenFreeMap tiles, the Telegram API, and the app's own Worker endpoints**), plus a **signed written audit record** citing the evidence (PRIV-02/GEO-03 re-verified end-to-end). Reusable by forks (GPL audit tool). - **D-02:** **Zero telemetry in v1** — opt-in telemetry stays an IDEAS.md aspiration post-validation; nothing to opt into yet, nothing to audit but absence. ### Device matrix + battery (SC1, PX-04) - **D-03:** **Fullscreen on guidance start** via `requestFullscreen()` behind the 8.0 capability guard (phase-6 guard precedent), with orientation-lock used where available; landscape usability per device in the QA pass; landscape layout fixes are CSS-only media queries (tokens binding; the cockpit/banner/card geometry adapts, the map stays Rams-constant). - **D-04:** The **battery target is a RECORDED real measurement**: one Android guidance run ≥30 min screen-on, battery % read at start/end (method + caveats written into the gate file); the D-15 distance-throttle is the software half. The <10%/hr target is recorded as measured, never synthesized (no bench harness — over-engineering for one phone). - **D-05:** The QA pass is **one consolidated walk**: Android + iOS + Desktop + browser × (boot, search→route, guidance, minimize/resume, trip-end, theme flip, landscape/fullscreen) — a single checklist in the phase gate file; user walks once; NOT-RUN legs recorded honestly (the phase 3/4 protocol; iOS legs if the device is available — Phase 4 verified iOS present). ### Budgets + deferred debt (SC4) - **D-06:** A **consolidated budget record** (`npm run budget-audit` output + written record checked into verification): Worker CPU/subrequests (06-04's measured ladder re-run against the live graph), bundle sizes (BA9 et al.), KV write counts per request class (cron = 0 writes — the free-plan law), asset cache headers. Current numbers vs free-tier limits, in one document. - **D-07:** **Debts paid here**: (1) 02-REVIEW's 3 deferred INFOs — IN-01 (dead `ParsedInitData` export + unreachable branch + read-helper intent comments), IN-02 (stale wrangler.jsonc comment), IN-03 (exact-900s boundary test — the security-critical comparator pin); (2) the /cari menu: re-run `register-commands.mjs` (scopes were re-armed in phase 4) + record the Telegram non-default-scope defect honestly in the limitations doc; (3) phase 3's stale-fix assumption notes (A1/A3) reviewed and closed or re-pointed. **NOT here**: ALT landmarks (c2) — committed to the v1.x milestone, not this phase. ### Known-limitations doc (SC4) - **D-08:** `docs/KNOWN-LIMITATIONS.md` in-repo, written for **drivers AND forks**: OSM data gaps (no traffic confidence, no lane guidance, speed limits where mapped), cross-metro CPU degradation (the c3 class), the /cari menu defect + its workaround, beta quality statement, and the roadmap pointers (WASM-router track, ALT, engine B). Surfaced from the bot: `/help` gains one line pointing at the doc (strings-module copy). ### Launch definition - **D-09:** **Private launch = checklist-signed private beta**: the user's chosen first drivers (friends/family/testers in KL) receive the bot link; word-of-mouth distribution. No app store, no marketing, no landing page (post-v1 idea). The launch checklist: known-limitations shipped · privacy audit signed · budget record green · full device matrix walked · `/help` + `/privacy` accurate · repo pushed public under GPL-3.0. **The milestone completes when the checklist is signed** — the launch act itself is the user's, not the workflow's. ### OpenCode's Discretion - Audit script file layout (scripts/ + test/ split), gate vocabulary, exit codes - Battery recording method details; landscape media-query breakpoints within tokens - INFO-fix mechanics; the boundary test's fixture form - Known-limitations doc structure/tone; the /help one-liner copy (en/ms) ### Deferred Ideas (OUT OF SCOPE) - ALT landmark preprocessing (c2) — v1.x milestone lever (measured decision, phase 6) - Engine B (fully client-side webview router) — the static-hosted endgame track - WASM-router OSS project — backend #2's standalone evolution - Opt-in telemetry — post-validation (IDEAS.md) - Voice guidance, live trip share, toll toggle, speed-limit display, traffic ETA — v1.x table - Bitmap map snapshots in turn-cards — v1.x - Public launch (landing page, marketing, app-store presence) — post-v1 ## Phase Requirements | ID | Description | Research Support | |----|-------------|------------------| | PX-04 | Guidance supports fullscreen and the screen remains usable in landscape (driving posture) | Telegram Bot API 8.0 offers `requestFullscreen`, `exitFullscreen`, `isFullscreen`, and `fullscreenFailed`; browser Fullscreen API is gesture-gated and can reject; capability wrapper, CSS-only landscape rules, mock/browser tests, and consolidated real-device gate are specified below. | | PRIV-02 (re-verify) | Backend holds no persistent per-user location history/profile; only TTL-expiring state, verifiable by architecture audit | Existing source gates plus a repository-wide audit tool, live KV key/expiry listing probe, log-vocabulary scan, bundle fetch-host allowlist, and signed audit record are specified below. | | GEO-03 (re-verify) | User location is never written to persistent storage; only ephemeral TTL server state | Current client storage gate, Worker KV TTL gate, live key/expiry inspection, and end-to-end boot/guidance/trip-end probe are specified below. | ## Summary Phase 7 is best planned as **four independently verifiable gates joined by one honest launch checklist**, not as a broad feature phase. The code already contains most of the mechanisms that need proving: a narrow Telegram WebApp contract with version guards, memory-only client state, TTL-only KV writes, route/guidance replay fixtures, a deterministic A* CPU measurement, and a capture harness. The work is to consolidate these into reproducible gates, close the explicit UX gap (fullscreen/landscape), exercise every failure class, and preserve an evidence trail that a fork or reviewer can rerun. The platform answer is now clear. Telegram Bot API 8.0 introduced native Mini App fullscreen plus `requestFullscreen`/`exitFullscreen`, `isFullscreen`, `fullscreenChanged`, `fullscreenFailed`, and `lockOrientation`/`unlockOrientation`; the official guidance says Mini Apps can use portrait or landscape fullscreen and that content must respect safe/content safe areas. Therefore the primary path should call **Telegram's Mini App fullscreen API** at guidance start, not the DOM `Element.requestFullscreen()` API: the DOM API is only “Limited availability,” requires transient user activation, and can reject. A DOM fallback is optional and must be silent. Orientation should be locked only through Telegram's `lockOrientation()` when available, because generic `screen.orientation.lock()` is not supported in Android WebView and may reject elsewhere. **Primary recommendation:** implement a single `fullscreen.ts` Telegram capability wrapper that (1) requires Bot API >= 8.0 and method presence, (2) calls `Telegram.WebApp.requestFullscreen()` at the guidance-start edge, (3) observes `fullscreenChanged`/`fullscreenFailed`, and (4) treats failure as a non-error fallback to a fully usable portrait/landscape layout; then prove it through one automated matrix plus one honest four-client walk. ## Architectural Responsibility Map | Capability | Primary Tier | Secondary Tier | Rationale | |------------|-------------|----------------|-----------| | Fullscreen request / orientation lock | Browser / Client | Telegram client | The Mini App owns the capability guard and calls Telegram's Bot API 8.0 methods from the active gesture/start boundary. | | Landscape cockpit geometry | Browser / Client | CDN / Static | CSS media queries and safe-area variables live in the web app; the map cartography remains unchanged. | | Failure-state rendering | Browser / Client | API / Backend | The client must render honest states; backend null/error semantics supply stable categories. | | Version-gated old-client fallback | Browser / Client | Telegram client | Method absence and version checks occur in the web app; the Telegram client decides support. | | Source privacy gates | API / Backend | Browser / Client | One repo tool scans all `src` trees and production bundles; failures must be deterministic exit codes. | | Live KV expiry/absence proof | Database / Storage | API / Backend | Wrangler lists actual namespace keys and expiration metadata; source gates prove every writer carries TTL. | | Coordinate-free logging proof | API / Backend | CDN / Static | Worker logs are category-only; the audit scans log call sites and runtime output. | | Runtime network allowlist proof | Browser / Client | CDN / Static | Production bundle scan proves only approved tile, Telegram, and own-Worker hosts exist. | | Worker CPU/subrequest accounting | API / Backend | Database / Storage | Worker invocation metrics, R2/KV/Telegram subrequests, and cron behavior determine free-tier headroom. | | Golden-route regression | API / Backend | Browser / Client | Routing/search corpus CLIs and in-suite assertions already exist and should be invoked together. | | Static-asset cache policy | CDN / Static | — | `_headers` controls browser cache behavior for fingerprinted Vite assets and HTML. | | Battery measurement | Browser / Client | Human checkpoint | The app controls GPS cadence; the human records phone battery before/after a real run. | | Launch completion | Browser / Client | API / Backend | The human signs the checklist after all automated and physical evidence exists. | ## Project Constraints (from AGENTS.md) - Telegram bot + Telegram Mini App is the product; no independent mobile app in v1. [VERIFIED: AGENTS.md] - One Cloudflare Worker serves static assets, `/telegram/webhook`, and `/api/*`; Workers superseded Pages. [VERIFIED: AGENTS.md; bot/wrangler.jsonc] - Every service must be open-source or genuinely free-tier; no paid APIs. [VERIFIED: AGENTS.md] - Privacy: no location history/profiles without explicit opt-in; v1 has zero telemetry. [VERIFIED: AGENTS.md; D-02] - KL metro is the v1 region. [VERIFIED: AGENTS.md] - Telemetry Rams values are binding: exact palette, Space Grotesk/Geist typography, 8px grid, zero border radius, no shadows/blur/glass. [VERIFIED: AGENTS.md] - Telemetry Rams tokens are the single styling authority; the `design-taste-frontend` skill's existing-design-system special case applies, but its defaults do not override Rams tokens. This phase's UI surface is limited to landscape media queries and failure-state rendering. [VERIFIED: AGENTS.md; project skill note] - Use npm workspaces and the repo's custom Node test harnesses; no third-party test framework is currently established. [VERIFIED: AGENTS.md] - Git hygiene: generated output/local secrets are ignored; public GPL-3.0 publication is part of the launch checklist. [VERIFIED: AGENTS.md; D-09] - Before direct file changes, use the GSD workflow; planning artifacts are the intended output here. [VERIFIED: AGENTS.md] ## Standard Stack ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | Telegram Mini Apps WebApp JS | 8.0 API floor | Native Mini App fullscreen, orientation lock, safe-area fields/events | Official platform API; D-03 already locks the 8.0 guard. [CITED: https://core.telegram.org/bots/webapps] | | Browser Fullscreen API | Platform feature-detect | Optional DOM fallback outside Telegram | Standard web API but limited support and transient-user-activation requirement. [CITED: https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen] | | Screen Orientation API | Platform feature-detect | Diagnostics only; do not make it the primary lock | Not supported in Android WebView; rejects for several reasons. [CITED: https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock; Chromium issue 40939281] | | Cloudflare Workers + KV + R2 | Free plan | Runtime, ephemeral state, road graph | Existing settled stack; official current limits documented below. [VERIFIED: repo; CITED: Cloudflare limits/pricing/KV docs] | | Wrangler | 4.151.3 declared | KV listing, deploy/canary, bundle inspection | Already installed in `bot`; `kv key list` supports namespace id and prefix. [VERIFIED: bot/package.json; CITED: https://developers.cloudflare.com/kv/reference/kv-commands/] | | Node.js test harnesses | Repo-local | Deterministic source/route/guidance gates | Existing `node:test`-style runners avoid a new test dependency. [VERIFIED: repo package scripts] | | Playwright | 1.50.0 declared | Browser viewport/orientation/failure-state captures | Already present in `web`; can exercise landscape media queries. [VERIFIED: web/package.json] | | Vite | 8.3.5 declared | Production bundle and harness bundle | Already the Mini App bundler; output feeds Workers static assets. [VERIFIED: web/package.json] | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | Android Battery Historian / `adb` | Platform tool | Optional corroboration of a recorded phone run | Use only if already available; D-04 requires one simple recorded measurement, not a lab harness. [CITED: https://developer.android.com/topic/performance/power/battery-historian] | | `node:zlib` | Node builtin | Gzip size measurements for budget audit | Use in `budget-audit`; no new dependency. [VERIFIED: routing/scripts/route-corpus.mjs precedent] | | `process.hrtime.bigint()` | Node builtin | Deterministic A* CPU measurement | Reuse existing `routing/scripts/measure-route-cpu.mjs`. [VERIFIED: repo] | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | Telegram `WebApp.requestFullscreen()` | DOM `documentElement.requestFullscreen()` | DOM API may reject without transient activation and has limited iOS/WebView support; use only as optional fallback. [CITED: MDN] | | Telegram `lockOrientation()` | `screen.orientation.lock("landscape")` | Generic lock is unavailable in Android WebView and can reject; Telegram API locks current orientation, not an arbitrary target. [CITED: MDN; Telegram docs] | | A single new audit framework | Existing custom Node runners plus one orchestration script | Fits the repo, GPL-friendly, and avoids a dependency for a finite gate vocabulary. [VERIFIED: repo conventions] | | Full Android battery instrumentation | One human-recorded start/end run | D-04 explicitly forbids over-engineering a bench harness. [VERIFIED: D-04] | **Installation:** ```bash # No new runtime dependency is required for the recommended implementation. # Existing workspace installs should already provide: npm install npm run bot:test npm run web:test npm run routing:test npm run search:test ``` **Version verification:** The environment used for this research could not run `node`/`npm` (`node: command not found`), so current registry versions were not freshly queried. Declared repository versions above were read from package manifests and are current **for this checkout**, not independently registry-verified. [VERIFIED: repo manifests] ## Architecture Patterns ### System Architecture Diagram ```text [Driver starts guidance] | v [web/src/telegram/fullscreen.ts capability wrapper] | | | | | +-- absent/failure --> [portrait + landscape CSS remains usable] | +-- Telegram >= 8.0 --> requestFullscreen() --> fullscreenChanged/failed | | | +-- lockOrientation() where available v [Guidance cockpit + CSS landscape media queries] | | | | | +-- [map cartography stays Rams-constant] | +-- [safe-left/right/top/bottom vars] +-- [route/guidance failure state] --> [designed empty/error/off-road/no-geometry card] [Local automated gates] | +--> npm run privacy-audit | +-- source grep: storage APIs / coordinate logs / runtime fetch hosts | +-- production bundle scan: zero analytics/beacons/unapproved hosts | +-- live KV listing: key families + expirations only | +--> npm run budget-audit | +-- route corpus + search corpus + CPU measurement | +-- bundle bytes + KV write classes + asset cache headers | +--> [one human device matrix + Android battery run] | v [07-LAUNCH-GATE.md signed] | v [private beta links distributed by human] ``` ### Recommended Project Structure ```text scripts/ ├── privacy-audit.mjs # orchestration and human-readable evidence report ├── budget-audit.mjs # orchestration and current-vs-limit report └── lib/ ├── source-gates.mjs # comment-stripped source scans ├── bundle-gates.mjs # production bundle scans └── cloudflare-probes.mjs # KV list + live headers/metrics probes web/src/telegram/ └── fullscreen.ts # Telegram 8.0 capability wrapper + fallback web/test/ └── fullscreen.test.mjs # capability/event/failure vectors web/scripts/ └── capture.mjs # extend existing harness with landscape/failure legs docs/ ├── KNOWN-LIMITATIONS.md └── BUDGETS.md # checked-in current-vs-limit record .planning/phases/07-hardening-private-launch/ └── 07-LAUNCH-GATE.md # signed checklist, battery method/result, device verdicts ``` ### Pattern 1: Capability-Guarded Native Fullscreen with Honest Fallback **What:** At the guidance-start edge, request Telegram Mini App fullscreen only when Bot API 8.0 and the method are present. Listen to `fullscreenChanged` and `fullscreenFailed`. A rejected request is not an app error: guidance continues, and the layout remains usable in either orientation. Use `lockOrientation()` only to lock the **current** orientation; do not attempt to force landscape. **When to use:** Guidance has just started and the driver has initiated the action. This is the only phase where occupying the whole screen is appropriate. **Example:** ```typescript // Source: https://core.telegram.org/bots/webapps export interface TelegramWebAppLike { // existing members... requestFullscreen?(): void; exitFullscreen?(): void; isFullscreen?: boolean; lockOrientation?(): void; unlockOrientation?(): void; onEvent?(event: string, cb: (...args: unknown[]) => void): void; offEvent?(event: string, cb: (...args: unknown[]) => void): void; } export function requestGuidanceFullscreen(wa: TelegramWebAppLike | null): boolean { if ( wa === null || typeof wa.requestFullscreen !== "function" || typeof wa.isVersionAtLeast !== "function" || !wa.isVersionAtLeast("8.0") ) return false; try { wa.requestFullscreen(); return true; } catch { return false; // absence/failure is a fallback, never a broken guidance state } } ``` **Required behavior:** - Extend the existing narrow `TelegramWebAppLike` contract and Mock-Telegram fake in the same change, following the repo's established “contract + fake + test together” law. [VERIFIED: web/src/telegram/webapp.ts] - Treat `fullscreenFailed` as a category event, not a user-facing error. - Do not log coordinates, user identifiers, or route payloads in the fallback path. [VERIFIED: D-01; existing D-14 category-only law] - Do not block guidance start on fullscreen success; the call is best effort. ### Pattern 2: Landscape as CSS Geometry Adaptation, Not a New Map **What:** Keep the map Rams-constant. Adapt only the cockpit/banner/card geometry with media queries and safe-area variables. The existing HTML already has `viewport-fit=cover`, and `safe-area.ts` writes `--safe-top/right/bottom/left` from CSS env and Telegram 8.0 insets, taking the larger per edge. [VERIFIED: web/index.html; web/src/telegram/safe-area.ts] **When to use:** Always. Portrait remains the default; landscape is an additional constraint, not a separate screen. **Example:** ```css /* Source: https://webkit.org/blog/7929/designing-websites-for-iphone-x/ Tokens remain binding; no new colors, radii, or shadows. */ @media (orientation: landscape) and (max-height: 560px) { .guide-banner { min-height: 72px; padding-left: max(var(--spacing-space-md), var(--safe-left)); padding-right: max(var(--spacing-space-md), var(--safe-right)); } .guide-list { max-width: 420px; max-height: calc(100dvh - 104px - var(--safe-top) - var(--safe-bottom)); } } ``` **Cautions:** - `env(safe-area-inset-*)` is not a replacement for margins; combine it with normal token spacing using `max()` and provide fallback declarations. [CITED: WebKit safe-area guidance] - The current shell uses `height: 100%` and `position: fixed`; test whether `100dvh`/`100svh` is needed when Telegram chrome and orientation changes occur. Do not change the sizing model without a failing capture. [VERIFIED: web/src/shell.css] ### Pattern 3: Failure-State Matrix as Data **What:** Represent each failure class as a named vector with its expected DOM/state token and action. The repo already uses named vectors and custom runners. The phase should enumerate empty, error, timeout, off-road, no-geometry, and older-client fallback rather than ad-hoc screenshots. **When to use:** Every failure-state change and every client-matrix rerun. **Example:** ```javascript const FAILURE_VECTORS = [ { id: "empty-route", trigger: "route options=[]", expect: "pending-card" }, { id: "route-error", trigger: "route source throws", expect: "pending-card" }, { id: "no-geometry", trigger: "polyline decodes to <2 points", expect: "pending-card" }, { id: "off-road", trigger: "3 fixes beyond accuracy-aware threshold", expect: "rerouting-or-no-reroute" }, { id: "location-timeout", trigger: "LocationManager returns null after timeout", expect: "denied-or-awaiting-card" }, { id: "old-client", trigger: "WebApp version < 8.0 / methods absent", expect: "guidance-usable-without-fullscreen" }, ]; ``` **Required behavior:** - No blank screen, broken layout, fabricated route, or crash. [VERIFIED: SC2; Phase 3/5/6 conventions] - Off-road is already a calm state machine with grace, storm guard, and `no_reroute` degradation; harden its rendering rather than redesigning it. [VERIFIED: web/src/guidance/guidance.ts] - Route `null`/throw already renders the designed pending card; audit coverage, not behavior. [VERIFIED: web/src/route/route.ts] ### Pattern 4: Consolidated Audit as Deterministic Exit-Code Tool **What:** One orchestration command runs source scans, bundle scans, live probes, and existing corpus gates. It prints evidence and exits 0 only when every required gate passes; 1 for a gate failure; 2 for missing credentials/environment so “not run” is not mistaken for “pass.” **When to use:** Before every phase wave merge and at the launch gate. **Example:** ```javascript // Pattern, not copied from an upstream source. const results = [ await runStorageGate(), await runCoordinateLogGate(), await runRuntimeHostGate(), await runBundleTelemetryGate(), await runKvExpiryProbe(), ]; for (const result of results) { console.log(`${result.id}: ${result.pass ? "PASS" : "FAIL"} — ${result.evidence}`); } process.exit(results.every((r) => r.pass) ? 0 : 1); ``` ### Pattern 5: Reproducible Budget Record **What:** Reuse the existing corpus and CPU measurements, add bundle/KV/cache measurements, and compare every current number with a documented free-plan ceiling. Store both the machine-readable output and a human-written record. **When to use:** Phase close and launch checklist. **Required free-plan reference values:** [CITED: https://developers.cloudflare.com/workers/platform/limits/; https://developers.cloudflare.com/kv/platform/limits/; https://developers.cloudflare.com/workers/platform/pricing/] | Resource | Free-plan ceiling | |----------|-------------------| | Worker HTTP CPU | 10 ms/invocation | | Cron CPU | 10 ms/invocation | | Worker subrequests | 50/request | | Worker requests | 100,000/day | | Worker memory | 128 MB/isolate | | Worker bundle | 64 MiB uncompressed | | Static assets | free and unlimited requests; 20,000 files/version; 25 MiB/file | | KV reads | 100,000/day | | KV writes to different keys | 1,000/day | | KV writes to same key | 1/second | | KV list requests | 1,000/day | | KV storage | 1 GB/account and namespace | | KV operations/invocation | 1,000 | | Workers Logs Free | 200,000 events/day; 3-day retention | ### Anti-Patterns to Avoid - **DOM fullscreen as the primary path:** it is gesture-gated and limited availability; the Telegram-native API is the supported product surface. [CITED: MDN] - **Forcing landscape through `screen.orientation.lock("landscape")`:** unsupported in Android WebView and rejects in several states; Telegram `lockOrientation()` locks the current orientation. [CITED: MDN; Chromium issue] - **Treating a failed fullscreen request as guidance failure:** the fallback is a deliberate supported state. - **Grep-only privacy audit:** source scans do not prove production bundles contain no analytics or unapproved hosts; include a bundle scan. [CITED: D-01] - **Listing KV keys but ignoring `expiration`:** key presence alone does not prove TTL-only; inspect the expiry metadata returned by the list API. [CITED: Cloudflare KV list-keys docs] - **Calling the cron “zero writes” without a runtime probe:** the cron lists/reads and sends to Telegram; D-06 requires measured write counts by request class. - **Synthesizing battery results:** D-04 requires a recorded real run and caveats. - **Marking unavailable device legs PASS:** follow the Phase 3/4/6 honesty protocol; NOT RUN is not PASS. [VERIFIED: existing device gates] - **Redesigning the visual system during hardening:** Rams tokens remain binding; this phase adapts geometry only. ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Mini App fullscreen capability detection | A version-string parser or client-name heuristic | Existing `isVersionAtLeast("8.0")` plus method-presence guard | Official version/floor contract; method absence still matters. [CITED: Telegram docs] | | Orientation locking | A custom native-bridge protocol | Telegram `lockOrientation()`/`unlockOrientation()` with fallback | Native WebView orientation lock is unavailable; Telegram exposes the supported product API. [CITED: MDN; Telegram docs] | | Test framework | A new assertion/lint framework | Repo-local Node runners and named vectors | Existing custom harness is the established convention. [VERIFIED: repo] | | CSS safe-area probing | New per-component inset logic | Existing `safe-area.ts` + CSS `env()` + Telegram `safeAreaInset` union | One authority already exists; extend it rather than duplicating. [VERIFIED: web/src/telegram/safe-area.ts] | | Cloudflare KV inventory | A bespoke dashboard scraper | `wrangler kv key list --namespace-id ... --prefix ...` | Official CLI supports namespace/binding/prefix. [CITED: Cloudflare docs] | | Static cache policy | Worker code that special-cases asset URLs | `web/public/_headers` rules for hashed assets and HTML | Workers static assets natively consume `_headers`; default is `public, max-age=0, must-revalidate`. [CITED: Cloudflare static-assets headers docs] | | Route CPU benchmark | A new profiler | `routing/scripts/measure-route-cpu.mjs` | Deterministic 25-route sample, warm-up, p95<=8 ms gate already exists. [VERIFIED: repo] | | Golden search/route acceptance | New ad-hoc queries | `search:corpus`, `routing:corpus`, and their in-suite gates | Same measurement is already shared by CLI and test suite. [VERIFIED: repo] | | Battery lab harness | A synthetic bench | One recorded human run | D-04 explicitly prohibits over-engineering. | | Launch tracking system | A task tracker or landing page | Signed Markdown gate checklist | D-09 defines the gate file as the completion artifact. | **Key insight:** Nearly every risky part of this phase already has a local authority: source gates, a typed Telegram facade, a safe-area resolver, route/search corpus runners, and a CPU measurement. Consolidation and evidence are lower-risk than new infrastructure. ## Common Pitfalls ### Pitfall 1: Requesting Fullscreen Outside the User-Activation Window **What goes wrong:** A DOM `requestFullscreen()` call rejects with a type/permission error if the document lacks transient user activation or permission policy permits it. [CITED: MDN] **Why it happens:** Guidance activation may happen after asynchronous location/routing work, losing the initiating gesture. **How to avoid:** Prefer Telegram's `requestFullscreen()` at the guidance-start transition. If a DOM fallback is retained, invoke it only in the direct click handler and catch rejection. **Warning signs:** `fullscreenFailed`, `fullscreenerror`, or no `fullscreenChanged` event after guidance starts. ### Pitfall 2: Assuming Orientation Lock Exists Because the Method Name Does **What goes wrong:** `screen.orientation.lock()` is absent or nonfunctional in Android WebView; elsewhere it can reject with `InvalidStateError`, `SecurityError`, `NotSupportedError`, or `AbortError`. [CITED: MDN; Chromium issue 40939281] **Why it happens:** Presence of an API is treated as support. **How to avoid:** Use Telegram `lockOrientation()` only after the Bot API 8.0/method guard. It locks the current orientation; never assume it can force landscape. Treat rejection as non-error. ### Pitfall 3: Ignoring Telegram's `safeAreaInset` in Fullscreen **What goes wrong:** Fullscreen content overlaps Telegram controls or the device notch/status area. Telegram documents separate `safeAreaInset` and `contentSafeAreaInset` fields and events. [CITED: Telegram docs] **Why it happens:** Portrait CSS works, but fullscreen/landscape changes the insets. **How to avoid:** Re-resolve safe insets on `safeAreaChanged` and `contentSafeAreaChanged`; current code computes them once at boot. Add an update path and test a changed-inset event. ### Pitfall 4: Using `100vh` as if It Were Stable **What goes wrong:** Rotating or entering fullscreen changes visible viewport size; fixed elements can be clipped or overlap. **Why it happens:** CSS assumes a static viewport. **How to avoid:** Verify `100dvh`/stable-height behavior in Telegram Android/iOS captures. Keep the current fixed-shell model unless a failing leg proves a change. ### Pitfall 5: Source Privacy Greps Produce False Confidence **What goes wrong:** A source tree is clean, but a production dependency or built bundle contains analytics, a beacon, or a coordinate-bearing payload. **Why it happens:** The scan stops at first-party source. **How to avoid:** Scan production `web/dist` and dependency manifests for analytics/beacon/network hosts, and verify the runtime fetch allowlist: OpenFreeMap tiles, Telegram API, and the app's own Worker. [CITED: D-01] ### Pitfall 6: Naive Comment Stripping or String Matching **What goes wrong:** A prose mention triggers a ban, or a dynamic URL/host evades a literal string scan. **Why it happens:** Grep gates are treated as parsers. **How to avoid:** Reuse the repo's comment-stripped source scan, but parse manifests/imports and enumerate network call sites. Prefer positive allowlists for runtime hosts over negative token lists. ### Pitfall 7: KV Listing Without Expiry or Pagination **What goes wrong:** Keys appear TTL-less because the probe ignores the `expiration` field or stops before `list_complete`. [CITED: Cloudflare KV list docs] **Why it happens:** The command output is treated as a simple key array. **How to avoid:** Parse key name, expiration, pagination cursor, and `list_complete`; assert every live key family has an expiry and report the oldest/newest expiry. ### Pitfall 8: CPU Measurement That Does Not Match the Live Request **What goes wrong:** A local benchmark passes but the deployed `/api/route` still exceeds 10 ms, or vice versa. **Why it happens:** Process wall/CPU time, Worker CPU accounting, and graph warm state differ. **How to avoid:** Re-run the existing deterministic ladder and compare against live Workers invocation CPU metrics. Report both; do not substitute one for the other. The existing script's p95 budget is 8 ms under the 10 ms free ceiling. [VERIFIED: routing/scripts/measure-route-cpu.mjs; CITED: Cloudflare limits] ### Pitfall 9: Double-Counting Static Assets as Worker Requests **What goes wrong:** The budget report inflates Worker usage or expects a hit rate on requests that never invoke the Worker. **Why it happens:** Asset traffic and dynamic traffic are conflated. **How to avoid:** State that static asset requests are free and unlimited; measure Worker invocations separately. The repo's `run_worker_first` array limits Worker execution to `/api/*` and `/telegram/*`. [CITED: Cloudflare pricing; VERIFIED: bot/wrangler.jsonc] ### Pitfall 10: Assuming a Cache Hit Rate Without Cache Headers **What goes wrong:** The budget record claims good cache behavior, but hashed assets retain the default `max-age=0, must-revalidate` and no `_headers` policy exists. [CITED: Cloudflare static-assets headers docs] **Why it happens:** Cache headers are inferred rather than inspected. **How to avoid:** Add/verify `web/public/_headers`: long immutable caching for Vite hashed `/assets/*`, no-cache/revalidation for HTML, then inspect live response headers and `CF-Cache-Status`. Treat `CF-Cache-Status` as probabilistic per Cloudflare's own footnote. ### Pitfall 11: Turning a Measured Battery Percentage into a Synthetic Claim **What goes wrong:** A single short or interrupted run is extrapolated into “<10%/hr.” **Why it happens:** The target feels like a unit test rather than an observation. **How to avoid:** Record device/OS, brightness, network, screen-on interval, start/end percentages, route length, and caveats. Calculate elapsed-hour rate from actual minutes. If the run is under 30 minutes or interrupted, mark it NOT RUN and keep the gate open. [CITED: D-04] ### Pitfall 12: Closing the Phase With Unverified Hardware Legs **What goes wrong:** A browser replay is treated as proof of an iPhone or Desktop WebView behavior. **Why it happens:** The launch is time-boxed and the checklist needs a signature. **How to avoid:** Preserve the Phase 4/6 gate honesty protocol. Record PASS/FAIL/NOT RUN with client version and observations; the milestone completes only when the human signs. [VERIFIED: existing gate files; D-09] ## Code Examples Verified patterns from official sources: ### Telegram Native Fullscreen and Orientation Guard ```typescript // Source: https://core.telegram.org/bots/webapps const supported = wa !== null && typeof wa.isVersionAtLeast === "function" && wa.isVersionAtLeast("8.0") && typeof wa.requestFullscreen === "function"; if (supported) { wa.requestFullscreen?.(); // lockOrientation locks the CURRENT mode; it is not a force-landscape call. wa.lockOrientation?.(); } ``` ### Safe-Area-Aware Edge Padding ```css /* Source: https://webkit.org/blog/7929/designing-websites-for-iphone-x/ */ .guide-banner { padding-left: 12px; padding-left: max(12px, env(safe-area-inset-left, 0px)); padding-right: 12px; padding-right: max(12px, env(safe-area-inset-right, 0px)); } ``` ### Wrangler KV Listing Probe ```bash # Source: https://developers.cloudflare.com/kv/reference/kv-commands/ npx wrangler kv key list \ --namespace-id=59f58a54bc44483e89872b20a005a5eb \ --prefix="guidance:" ``` The list response includes each key's `name`, optional absolute `expiration`, optional metadata, `list_complete`, and cursor. [CITED: https://developers.cloudflare.com/kv/api/list-keys] ### Static Asset Cache Rules ```text # Source: https://developers.cloudflare.com/workers/static-assets/headers/ /assets/* Cache-Control: public, max-age=31556952, immutable / Cache-Control: public, max-age=0, must-revalidate ``` ### Existing Offline Golden-Route Gate ```bash # Source: repository routing/scripts/route-corpus.mjs npm run routing:corpus npm run routing:test npm run search:corpus npm run search:test node routing/scripts/measure-route-cpu.mjs ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | Telegram Mini App expanded height only | Native Mini App fullscreen, safe-area fields/events, orientation lock | Bot API 8.0, 2024-11-17 | PX-04 can use a supported product API; older clients need fallback. [CITED: Telegram docs] | | Assume `Element.requestFullscreen()` is universally available | Capability detection plus Telegram-native path; DOM API is limited availability | Ongoing platform divergence | Avoids iOS/WebView activation and policy failures. [CITED: MDN] | | One boot-time safe-area read | Dynamic safe/content safe-area events and re-resolution | Bot API 8.0 | Fullscreen and orientation changes can update insets. [CITED: Telegram docs] | | Manual asset cache guesses | Workers static assets default `max-age=0, must-revalidate`, overridable with `_headers` | Current Workers static assets | Budget audit should inspect real headers, not assume caching. [CITED: Cloudflare docs] | | Privacy proof via unit tests only | Source + bundle + live KV/expiry + signed written record | Phase 7 decision D-01 | End-to-end PRIV-02/GEO-03 evidence for forks. | **Deprecated/outdated:** - `wrangler kv:key list` colon syntax is deprecated; current docs use `wrangler kv key list`. [CITED: Cloudflare KV commands] - Battery Historian is no longer actively maintained; it can still corroborate an Android run, but simple phone battery readings are the required D-04 evidence. [CITED: Android Developers] - `screen.orientation.lock()` should not be treated as the Android WebView orientation solution; Chromium reports WebView does not support it. [CITED: Chromium issue 40939281] ## Assumptions Log | # | Claim | Section | Risk if Wrong | |---|-------|---------|---------------| | A1 | Telegram's native `requestFullscreen()` is preferable to the DOM Fullscreen API for every matrix client. | Architecture Patterns / Standard Stack | MEDIUM: iOS/Telegram client behavior is not fully described by official docs. Validate in the real-device walk; DOM fallback remains harmless. | | A2 | Telegram `lockOrientation()` is useful but locks the current orientation rather than forcing landscape. | Pitfalls / Architecture | MEDIUM: The official text says “current mode”; keep CSS landscape support as the source of truth regardless. | | A3 | The current shell can adapt to landscape with CSS-only changes without changing the fixed-position model. | Landscape Pattern | MEDIUM: Telegram viewport/fullscreen behavior may require `dvh`; capture first, change only on a proven failure. | | A4 | The production bundle's only runtime fetch hosts are OpenFreeMap tiles, Telegram API, and the app's own Worker. | Privacy Audit | MEDIUM: MapLibre/telemetry endpoints and Telegram redirect URLs can vary. Produce the actual host inventory before signing. | | A5 | The live KV namespace contains only `session:`, `chat:`, `loc:`, and `guidance:` key families, all with expiry. | Privacy Audit / KV Probe | MEDIUM: stale/manual keys may exist. The probe must report unexpected families rather than assume absence. | | A6 | The current A* measurement can pass the 8 ms p95 gate against the checked-in graph. | Budget Audit | MEDIUM: the Phase 6 file records historical failures and mitigation; the current live graph may differ. Re-run and report, never assume. | | A7 | A single Android run can produce a defensible <10%/hr observation. | Battery Method | MEDIUM: battery percentage granularity, background activity, and route conditions can dominate. Record caveats; do not synthesize. | | A8 | Adding `_headers` for hashed assets and HTML will preserve Worker-free static delivery. | Budget / Cache Headers | LOW: documented Workers static-assets behavior; verify live headers. | | A9 | The existing custom test runners can host the new audit and landscape vectors without a new framework. | Validation Architecture | LOW: established repo convention. | | A10 | Phase 3 A1/A3 stale-fix assumption notes are locatable and closable/re-pointable in this phase. | Deferred Debt | LOW: locate during implementation; D-07 is explicit. | ## Open Questions (RESOLVED) *All five questions below were CLOSED by the Phase 7 plans. Each item carries its adopted disposition. The recommendations were adopted verbatim — the plans implement them; none was silently dropped.* 1. **Does Telegram iOS 8.0+ reliably emit fullscreen success/failure events to the Mini App?** - What we know: The official API exposes `requestFullscreen`, `isFullscreen`, `fullscreenChanged`, and `fullscreenFailed`; the exact per-client reliability is not specified. [CITED: Telegram docs] - What's unclear: iOS/Telegram Desktop event delivery and whether WebView-native fullscreen is used. - Recommendation: Treat events as observability, not correctness. The UI must be usable without receiving either event; record each client's observed behavior in the gate. - RESOLVED: **Adopted — events are observability, never correctness.** `web/src/telegram/fullscreen.ts` exposes `watchFullscreenEvents(wa, onChange)` with a no-op-safe, try/catch-wrapped handler that logs NOTHING; it is REGISTERED once at the guidance-start edge in `web/src/route/route.ts` (07-01 task 4) with a deliberately no-op callback. The UI never depends on either event. Per-client observed behaviour is recorded in the 07-06 device matrix (`07-LAUNCH-GATE.md` Part A). 2. **What battery rate did the chosen Android phone actually record?** - What we know: D-04 requires one >=30-minute screen-on run and no synthesis. - What's unclear: Phone model, brightness, cellular/Wi-Fi, route conditions, and background apps. - Recommendation: Use the exact checklist fields in `07-LAUNCH-GATE.md`; if unavailable, mark NOT RUN and hold the launch gate. - RESOLVED: **Answered by measurement at the launch gate per D-04/D-06.** `07-LAUNCH-GATE.md` Part B carries the exact checklist field list (device/model, OS + client version, brightness, network, ≥30-minute screen-on interval, start %, end %, route description, elapsed minutes, computed %/hr, caveats); a shorter or interrupted run is recorded NOT RUN and the gate stays open (07-06 t1 + t2). No number is synthesized. 3. **What is the live route CPU p95 now?** - What we know: The script measures deterministic p95 with an 8 ms gate; Phase 6 recorded historical failures/mitigations. [VERIFIED: repo] - What's unclear: Current live graph and Worker CPU metrics. - Recommendation: Re-run the script and collect Workers Logs CPU data for the same route class; report both and do not label the budget green if either exceeds its budget. - RESOLVED: **Answered by measurement at the launch gate per D-04/D-06.** The local ladder is RE-RUN as a child process by the `route-cpu` budget gate (07-04 t2) and its p95 is compared to the 8 ms budget against the 10 ms free-plan ceiling; the live Workers Logs CPU number is a SEPARATE measurement recorded in `docs/BUDGETS.md` §Statement — neither substitutes for the other, and the budget is not labelled green if either exceeds its budget (07-04 t2/t3, 07-06 Part C). 4. **Which static assets are actually long-cacheable?** - What we know: Vite emits hashed `/assets/*`; Workers defaults to revalidation and supports `_headers`. - What's unclear: Whether the deployed build has a stable `_headers` file and whether live `CF-Cache-Status` is meaningful. - Recommendation: Enumerate `web/dist`, add explicit rules, and verify live response headers. Treat `CF-Cache-Status` as probabilistic. - RESOLVED: **Adopted — explicit rules + a live header probe that quotes, never interprets.** `web/public/_headers` carries exactly two rules (`/assets/*` immutable for 31556952 s, `/` `max-age=0, must-revalidate`) and is verified to reach `web/dist/` (07-04 t1); the `cache-headers` gate prints the OBSERVED live headers and quotes `CF-Cache-Status` as probabilistic (07-04 t2). 5. **Does `/help` need an absolute GitHub URL or a repository-relative limitations link?** - What we know: D-08 says `/help` gains one line pointing at the doc; D-09 requires the public GPL repository. - What's unclear: Final public repository URL/name. - Recommendation: Put the URL in deployment configuration or a single strings-module constant rather than scattering it; defer final copy to implementation. - RESOLVED: **Adopted — a SINGLE strings-module constant per locale.** `bot/src/i18n/strings.ts` carries exactly one limitations pointer line per locale (`Known limits:` / `Had panduan:`) — the same URL in both, resolved at implementation from the repo's actual HTTPS remote (`git remote -v`) and never scattered across files (07-05 t2). ## Environment Availability | Dependency | Required By | Available | Version | Fallback | |------------|------------|-----------|---------|----------| | Node.js + npm | All repo scripts, corpus gates, audit tools | ✗ in research shell | — | Use the maintainer's normal Node environment; version must be confirmed at execution start | | Wrangler | Live KV probe, deploy/canary, Worker metrics | Unverified (`node` absent) | Manifest declares 4.151.3 | Install/use workspace dependency once Node is available | | Cloudflare account/API credentials | Live KV and Worker audit | Unverified | — | Run source/bundle gates locally; mark live-probe gates NOT RUN and keep launch gate open | | Real Android device | Fullscreen/landscape/battery | Human-owned per prior phases | — | NOT RUN; no synthetic pass | | Real iOS device | Fullscreen/landscape | Phase 4 records iOS present | — | NOT RUN if unavailable | | Telegram Desktop | Device matrix | Human-owned per prior phases | — | NOT RUN if unavailable | | Plain browser | Device matrix + Playwright | Playwright declared in web workspace | 1.50.0 | Use browser leg and capture harness | | ADB / Battery Historian | Optional battery corroboration | Unverified | — | D-04 only requires phone start/end readings; no fallback needed for the gate | **Missing dependencies with no fallback:** - A Node/npm execution environment is required to run any repository gate. The planning environment currently reports `node: command not found`; execution must occur in the project’s configured Node environment. - Human access to the selected Android phone and the real clients is required to sign SC1/SC4; the phase cannot substitute simulated evidence. **Missing dependencies with fallback:** - Cloudflare live probes require credentials. Source/bundle gates and corpus gates can still run; live KV/expiry and cache-header checks remain mandatory before signing and cannot be replaced by local-only checks. - Battery Historian is optional; D-04’s start/end phone readings are sufficient. ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | Repo-local Node test harnesses (`node:test`-style): bot/web/routing/search `test/runner.mjs`; Playwright for browser captures in `web` | | Config file | None global — each workspace has `test/runner.mjs`; Web `vite.config.ts` | | Quick run command | `npm run web:test && npm run bot:test` | | Full suite command | `npm run bot:test && npm run web:test && npm run routing:test && npm run search:test && npm run search:corpus && npm run routing:corpus && node routing/scripts/measure-route-cpu.mjs` | | Phase commands | `npm run privacy-audit && npm run budget-audit` (new) | | Physical gate | `.planning/phases/07-hardening-private-launch/07-LAUNCH-GATE.md` — one Android+iOS+Desktop+browser walk plus one Android battery run | ### Phase Requirements → Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | PX-04.a | Bot API <8.0 or absent method: guidance starts without fullscreen and remains usable | unit | `npm test -w web` | ❌ Wave 0 | | PX-04.b | Bot API 8.0: `requestFullscreen()` called once at guidance start; failure is non-error | unit | `npm test -w web` | ❌ Wave 0 | | PX-04.c | `fullscreenChanged` / `fullscreenFailed` update state without throwing | unit | `npm test -w web` | ❌ Wave 0 | | PX-04.d | Safe-area inset changes after fullscreen/rotation are re-applied | unit | `npm test -w web` | ❌ Wave 0 | | PX-04.e | Landscape viewport keeps banner, list, guidance card, and map usable without overlap/clipping | browser capture | `npm run capture -w web` | Existing harness; ❌ new landscape legs | | PX-04.f | Fullscreen and landscape on Android/iOS/Desktop/browser | manual device | `07-LAUNCH-GATE.md` device matrix | ❌ Wave 0 gate file | | PX-04.g | One Android guidance run >=30 min records battery and drains <10%/hr | manual measurement | `07-LAUNCH-GATE.md` battery record | ❌ Wave 0 gate file | | SC2.a | Empty route options render designed pending card | unit/integration | `npm test -w web` | Existing route surface gates; confirm coverage | | SC2.b | Route source error/timeout renders designed pending/no-route state | unit/integration | `npm test -w web` | ❌ explicit timeout/error vectors | | SC2.c | Off-road grace/reroute/no-reroute states render calmly | unit/replay | `npm test -w web` | Existing guidance replay; confirm DOM coverage | | SC2.d | No-geometry route is rejected/degraded, never half-drawn | unit | `npm test -w routing && npm test -w web` | Existing codec/route gates; confirm end-to-end vector | | SC2.e | Older Telegram client without 8.0 methods behaves sanely | unit + manual | `npm test -w web`; gate file | ❌ new old-client vector | | SC3.a | No banned persistence API in any production `src` tree | source gate | `npm run privacy-audit` | ❌ new tool; extends `web/test/no-storage.test.mjs` and bot privacy gates | | SC3.b | Every Worker KV write carries TTL; cron has zero writes | source + runtime | `npm run privacy-audit` | ❌ new tool; extends bot privacy/cron gates | | SC3.c | Live KV keys are TTL-expiring and only expected families exist | live probe | `npm run privacy-audit -- --live` | ❌ new probe | | SC3.d | Logs contain category-only vocabulary; no coordinates/chat ids/payloads | source + bundle | `npm run privacy-audit` | ❌ new tool; extends category-only law | | SC3.e | Production bundle contains no analytics/beacon and only approved fetch hosts | bundle gate | `npm run privacy-audit` | ❌ new tool | | SC4.a | Golden route corpus green | CLI + suite | `npm run routing:corpus && npm test -w routing` | ✅ existing | | SC4.b | Search golden corpus green | CLI + suite | `npm run search:corpus && npm test -w search` | ✅ existing | | SC4.c | A* CPU p95 <=8 ms local; live Worker CPU <=10 ms | measurement | `node routing/scripts/measure-route-cpu.mjs`; live logs probe | ✅ local script; ❌ live probe | | SC4.d | Production bundle and Worker bundle within recorded budgets | measurement | `npm run budget-audit` | ❌ new tool; BA9 precedent exists | | SC4.e | KV writes by request class recorded; cron=0 | source + runtime | `npm run budget-audit` | ❌ new tool | | SC4.f | Static assets have intended cache headers | live header probe | `npm run budget-audit -- --live` | ❌ new probe; no `_headers` found in repo | | SC4.g | Known-limitations doc shipped and linked from `/help` en/ms | unit/doc gate | `npm test -w bot`; link check in audit | ❌ doc + one-liner | | SC4.h | Deferred INFO debts fixed and exact-900s boundary pinned | unit | `npm test -w bot` | ❌ new boundary vector; IN-01/02 mechanical | ### Sampling Rate - **Per task commit:** `npm test -w web` or `npm test -w bot` for the touched workspace; run `npm run privacy-audit` when touching storage/log/network code. - **Per wave merge:** full automated suite plus `npm run privacy-audit && npm run budget-audit`. - **Phase gate:** Full suite green, both audits green, live probes green, physical device matrix complete, battery run recorded, limitations doc shipped, checklist signed. ### Wave 0 Gaps - [ ] `web/test/fullscreen.test.mjs` — covers PX-04.a–d and old-client fallback. - [ ] `web/test/failure-state-matrix.test.mjs` — covers SC2 named vectors and DOM state tokens. - [ ] `scripts/privacy-audit.mjs` + `scripts/lib/*` — consolidated source/bundle/live audit. - [ ] `scripts/budget-audit.mjs` + `scripts/lib/*` — consolidated budget record and live probes. - [ ] `web/public/_headers` — hashed-asset immutable caching + HTML revalidation policy, if verified by audit. - [ ] `bot/test/initdata.test.mjs` exact-900s vector — D-07 IN-03. - [ ] `docs/KNOWN-LIMITATIONS.md` — D-08. - [ ] `.planning/phases/07-hardening-private-launch/07-LAUNCH-GATE.md` — device matrix, battery method/result, checklist signature. - [ ] Framework install: none — use the existing custom runners. ## Security Domain ### Applicable ASVS Categories | ASVS Category | Applies | Standard Control | |---------------|---------|-----------------| | V2 Authentication | yes | Existing HMAC initData validation and one-time opaque session tokens; re-pin exact boundary. [VERIFIED: bot/src/security/initdata.ts] | | V3 Session Management | yes | 900-second session TTL and rotation; live expiry probe. [VERIFIED: bot/src/kv/sessions.ts] | | V4 Access Control | yes | Bearer claim-derived identity; chat/user ids never accepted from client bodies. [VERIFIED: bot/src/webhook.ts] | | V5 Input Validation | yes | UUID/slug/body caps and typed snapshot validation; preserve during hardening. [VERIFIED: bot/src/webhook.ts] | | V6 Cryptography | yes | Web Crypto HMAC; no new cryptography; exact-900 comparator boundary pinned. [VERIFIED: bot/src/security/initdata.ts] | ### Known Threat Patterns for This Stack | Pattern | STRIDE | Standard Mitigation | |---------|--------|---------------------| | Coordinate leakage through logs | Information Disclosure | Category-only log vocabulary; source scan + runtime log review. | | Persistent location/profile storage | Privacy / Information Disclosure | Memory-only client; TTL-only Worker KV; live key/expiry probe. | | Telemetry beacon smuggled into bundle | Information Disclosure | Production bundle host/beacon allowlist scan. | | Unapproved external runtime fetch | Information Disclosure / Tampering | Positive host allowlist: OpenFreeMap tiles, Telegram API, own Worker. | | Cross-user envelope access | Elevation of Privilege | Claim-derived ownership and indistinguishable 404 responses. | | Fullscreen request used as a crash path | Denial of Service | Capability guard, try/catch, `fullscreenFailed` fallback. | | Orientation lock abuse/DoS | Denial of Service | Telegram-only lock; no forced orientation; reject is non-error. | | Stale initData at exact boundary | Authentication bypass | Exact-900s inclusive-boundary test (D-07 IN-03). | | Asset cache policy leak/staleness | Information Disclosure | Explicit `_headers`; HTML revalidation; hashed assets immutable. | ## Sources ### Primary (HIGH confidence) - `https://core.telegram.org/bots/webapps` — Bot API 8.0 fullscreen/orientation/safe-area methods and events; version guard; current Bot API history. - `https://telegram.org/blog/fullscreen-miniapps-and-more` — Mini Apps can use full screen in portrait or landscape; orientation locking announcement. - `https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen` — promise/rejection behavior, transient activation, permissions policy, limited availability. - `https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock` — lock rejection types and limited availability. - `https://webkit.org/blog/7929/designing-websites-for-iphone-x/` — `viewport-fit=cover`, `env(safe-area-inset-*)`, `max()` guidance. - `https://developers.cloudflare.com/workers/platform/limits/` — current Free limits, CPU, subrequests, assets, logs. - `https://developers.cloudflare.com/workers/platform/pricing/` — static assets free/unlimited; KV and Workers Free quotas. - `https://developers.cloudflare.com/kv/platform/limits/` — KV Free reads/writes/list/storage and per-invocation operations. - `https://developers.cloudflare.com/kv/reference/kv-commands/` — current Wrangler KV syntax and flags. - `https://developers.cloudflare.com/kv/api/list-keys` — key expiration and pagination semantics. - `https://developers.cloudflare.com/workers/static-assets/headers/` — default cache headers, `_headers`, `CF-Cache-Status` caveat. - `https://developers.cloudflare.com/workers/observability/logs/workers-logs/` — invocation CPU/wall data and observability configuration. - `https://developer.android.com/topic/performance/power/battery-historian` — battery measurement context; tool no longer actively maintained. - Repository files listed in the phase context, especially `web/src/telegram/webapp.ts`, `web/src/telegram/safe-area.ts`, `web/src/shell.css`, `web/src/route/route.ts`, `web/src/guidance/guidance.ts`, `web/src/guidance/watch.ts`, `bot/src/webhook.ts`, `bot/test/route-privacy.test.mjs`, `web/test/no-storage.test.mjs`, `routing/scripts/measure-route-cpu.mjs`, routing/search corpus runners, and Phase 4/6 device gates. ### Secondary (MEDIUM confidence) - Chromium issue 40939281 — Android WebView does not support `screen.orientation.lock()` and now permits feature detection to report absence. - MDN browser-compatibility data / Can I Use search results — iOS Safari fullscreen partial support and WKWebView divergence; still requires real-device verification in Telegram. - Telegram bug report 66082 — Android 10 fullscreen Mini App safe-area/inset issue; use as a QA warning, not as a universal current limitation. ### Tertiary (LOW confidence) - General open-source navigation “known limitations” conventions — only section structure was synthesized from OSM/OsmAnd/GraphHopper documentation. The exact document wording remains a project choice. ## Metadata **Confidence breakdown:** - Standard stack: HIGH — repository manifests and official platform docs; registry freshness could not be queried because Node was unavailable in the research shell. - Architecture: MEDIUM-HIGH — current code boundaries are verified; per-client fullscreen behavior still needs the physical walk. - Pitfalls: HIGH for official platform constraints and repository conventions; MEDIUM for Telegram iOS/Desktop event reliability and battery interpretation. **Research date:** 2026-10-25 **Valid until:** 2026-11-24 (platform APIs and Worker limits are current as of research; re-check Telegram client notes and Cloudflare limits before signing the launch gate)