Files

26 KiB
Raw Permalink Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_have_notes, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_have_notes must_haves
07-hardening-private-launch 05 execute 1
bot/src/security/initdata.ts
bot/test/initdata.test.mjs
bot/wrangler.jsonc
bot/src/i18n/strings.ts
docs/KNOWN-LIMITATIONS.md
.planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md
true
PX-04
This plan pays the deferred debts (D-07) and ships the known-limitations document (D-08) — SC4's second half, delivered under the PX-04 phase.
truths artifacts key_links
The exact-900-second initData freshness boundary is pinned by a test — a `>=` regression cannot ship.
The dead `ParsedInitData` export and the unreachable branch are gone; the read helpers carry their intent comment.
The stale wrangler.jsonc comment is replaced with a present-tense record of the live namespace id.
A driver can reach the known-limitations document from /help in BOTH locales with one pointer line.
The limitations document tells the truth about OSM data gaps, the cross-metro CPU degradation, the /cari menu defect, the beta quality, and the roadmap pointers.
path provides contains
bot/test/initdata.test.mjs The T4b exact-900s boundary vector (IN-03) age 900 s
path provides contains
bot/src/security/initdata.ts The IN-01 cleanup — the dead export deleted, the intent comments added tampered-vs-malformed
path provides contains
bot/wrangler.jsonc The IN-02 present-tense namespace comment chat_sessions namespace created via
path provides contains
bot/src/i18n/strings.ts The /help limitations pointer line (en + ms) Known limits:
path provides contains
docs/KNOWN-LIMITATIONS.md The known-limitations document for drivers AND forks (D-08) Known limitations
from to via pattern
bot/src/i18n/strings.ts docs/KNOWN-LIMITATIONS.md the helpMenu pointer line names the doc's public URL — a SINGLE constant, never a scattered URL Known limits:
from to via pattern
bot/src/i18n/strings.ts bot/scripts/register-commands.mjs the pointer line does NOT start with '/', so the menu parse (CS5) and the setMyCommands derivation are unaffected Had panduan:
from to via pattern
bot/src/security/initdata.ts bot/test/initdata.test.mjs the exact-900s vector pins the strictly-greater comparator's INCLUSIVE edge 900
Pay every debt D-07 parked, and ship the known-limitations document the drivers (and forks) need — with a single pointer line from /help.

Purpose: D-07 — the three Phase-2 review INFOs (IN-01 dead code, IN-02 stale comment, IN-03 the unpinned 900 s security boundary), the /cari command-menu re-arm, and the Phase-3 A1/A3 assumption notes. D-08 — the roadmap's SC4 requires a known-limitations document BEFORE the first real drivers; an honest one is also the product's integrity claim. The privacy audit in plan 07-03 links to it, and the launch gate in plan 07-06 checks it. Output: the IN-01/IN-02/IN-03 fixes, the /cari re-arm record, the A1/A3 closure, docs/KNOWN-LIMITATIONS.md (new), and the /help pointer line in bot/src/i18n/strings.ts (en + ms).

<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/07-hardening-private-launch/07-CONTEXT.md @.planning/phases/07-hardening-private-launch/07-RESEARCH.md @.planning/phases/07-hardening-private-launch/07-PATTERNS.md @.planning/phases/02-bot-skeleton-initdata-security/02-REVIEW.md The IN-03 boundary vector — copy the T4 harness shape verbatim from `bot/test/initdata.test.mjs` lines 172–189:
test("T4 freshness boundary: age 899 s → ok:true", async () => {
  const validateInitData = await loadValidator();
  const now = Math.floor(Date.now() / 1000);
  const raw = signedUser(now - 899);
  const res = await validateInitData(raw, TEST_TOKEN);
  assert.equal(res.ok, true, `expected ok:true at 899 s, got ${JSON.stringify(res)}`);
});

The comparator under test — bot/src/security/initdata.ts lines 193–196:

  // — stale: age strictly beyond the D-13 window (899 ok / 900 ok / 901 stale) —
  if (nowSeconds - authDate > maxAgeSeconds) {
    return { ok: false, reason: "stale" as const };
  }

The IN-01 targets — bot/src/security/initdata.ts lines 59–66 (the dead export) and lines 87–104 / 154–165 / 198–214 (the parse blocks whose catch branches keep raw/undefined):

/** Shape of a fully parsed initData payload (hash/signature excluded from
 *  rawFields — they are auth metadata, not payload fields). */
export interface ParsedInitData {
  user: WebAppInitUser;
  chat?: WebAppInitChat;
  authDate: number;
  rawFields: Map<string, string>;
}

The IN-02 target — bot/wrangler.jsonc line 2 (the stale comment) versus the live id on the kv_namespaces line:

  // KV namespace id is filled in plan 03 task 1 after `npx wrangler kv namespace create chat_sessions`
  ...
  "kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]

The CS5 gate — bot/test/commands.test.mjs lines 134–155 (the menu parse and the exact-command assertion). The limitations pointer line MUST NOT start with / or it is parsed as a command and breaks CS5:

function menuEntries(helpMenu) {
  return helpMenu
    .split("\n")
    .filter((line) => line.startsWith("/"))
    .map((line) => {
      const em = line.indexOf(" — ");
      assert.ok(em !== -1, `helpMenu line lacks its " — " description marker: ${line}`);
      return { command: line.slice(1, em), description: line.slice(em + 3).trim() };
    });
}

The helpMenu strings as they stand — bot/src/i18n/strings.ts lines 94–95 (en) and 132–133 (ms):

    helpMenu:
      "Commands:\n/help — How araharah works and what it keeps\n/privacy — What araharah stores and never stores\n/cari — Search for a destination in the KL metro",
    helpMenu:
      "Arahan:\n/help — Cara araharah berfungsi dan apa yang ia simpan\n/privacy — Apa yang araharah simpan dan tidak pernah simpan\n/cari — Cari destinasi di kawasan KL",

The /help handler — bot/src/bot/commands/help.ts lines 18–20 (the reply is the helpMenu string verbatim, so the pointer line rides it automatically):

export function helpReplyText(locale: Locale): string {
  return strings[locale].helpMenu;
}

The re-arm tool — bot/scripts/register-commands.mjs (it arms every scope it may shadow: default, default+ms, all_private_chats, all_private_chats+ms, and clears the group scopes; it derives its descriptions from helpMenu).

task 1: pay IN-01 and IN-02 (dead code + the stale wrangler comment) and pin IN-03 (the exact-900s boundary) bot/src/security/initdata.ts, bot/test/initdata.test.mjs, bot/wrangler.jsonc - bot/src/security/initdata.ts (the dead export, the parse blocks, the comparator) - bot/test/initdata.test.mjs (the T4 harness shape + `signedUser`/`loadValidator` helpers) - bot/wrangler.jsonc (line 2's stale comment versus the live namespace id) - .planning/phases/02-bot-skeleton-initdata-security/02-REVIEW.md §Info (IN-01, IN-02, IN-03 verbatim) **IN-01** — in `bot/src/security/initdata.ts`: 1. DELETE the dead `ParsedInitData` interface (lines 59–66) entirely, INCLUDING its doc comment. It is never constructed and never returned (`validateInitData` returns `InitDataResult`). Before deleting, run `grep -rn "ParsedInitData" bot/ web/ routing/ search/` and confirm the ONLY hits are the declaration itself — if another file imports it, do NOT delete it; instead re-point it with an intent comment naming why it stays, and record that in the SUMMARY. 2. Add ONE intent comment over the `parseFields` catch block (lines 96–100) reading, in the file's voice: `// Intent: an undecodable percent-escape keeps the RAW value so the HMAC compare fails ("tampered") rather than silently dropping the field — malformed-vs-tampered precedence (D-14).` 3. Add ONE intent comment over the `user` JSON parse catch (lines 161–164) reading: `// Intent: a malformed user payload is the "malformed" class, not a tampered one — the field never reaches the HMAC path.` 4. Add ONE intent comment over the `chat` JSON parse catch (lines 210–213) reading: `// Intent: a malformed chat payload degrades to "no chat context" (a private chat) — it never fails an otherwise-valid private-chat payload.`

IN-02 — in bot/wrangler.jsonc, REPLACE line 2 (the stale "filled in plan 03 task 1" comment) with this present-tense record, in the file's own comment voice:

  // chat_sessions namespace created via `npx wrangler kv namespace create chat_sessions` — the id below is live.

IN-03 — in bot/test/initdata.test.mjs, add EXACTLY ONE new case immediately after the existing T4 freshness boundary: age 901 s → reason stale case, copying that case's harness shape verbatim (loadValidator(), signedUser(now - 900), TEST_TOKEN):

test("T4 freshness boundary: age 900 s → ok:true (the boundary is INCLUSIVE)", async () => {
  const validateInitData = await loadValidator();
  const now = Math.floor(Date.now() / 1000);
  const raw = signedUser(now - 900);
  const res = await validateInitData(raw, TEST_TOKEN);
  assert.equal(res.ok, true, `expected ok:true at exactly 900 s, got ${JSON.stringify(res)}`);
});

The case name MUST contain the literal age 900 s — it pins the comparator direction (> vs >=) that the implementation comment already claims. Do NOT change the comparator in initdata.ts — the implementation is correct today; the test is what makes the claim a pin.

Finally, run npm test -w bot and npm run bot:typecheck — both must pass. <acceptance_criteria> - grep -c "ParsedInitData" bot/src/security/initdata.ts returns 0 (the dead export is gone) and grep -rn "ParsedInitData" bot/src web/src returns nothing - grep -c "Intent:" bot/src/security/initdata.ts returns ≥ 3 (the three read-helper intent comments) - grep -c "age 900 s" bot/test/initdata.test.mjs returns ≥ 1 (the IN-03 vector) - grep -c "plan 03 task 1" bot/wrangler.jsonc returns 0 (the stale comment is gone) and grep -c "the id below is live" bot/wrangler.jsonc returns 1 - npm test -w bot exits 0 with 0 failed </acceptance_criteria> export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm test -w bot 2>&1 | tail -12 && npm run bot:typecheck 2>&1 | tail -5 IN-01's dead export is gone and the three read helpers carry their intent comments; IN-02's comment states the post-fact truth; IN-03's exact-900s vector is green.

task 2: the known-limitations document (D-08) + the /help pointer line (en + ms) docs/KNOWN-LIMITATIONS.md, bot/src/i18n/strings.ts - docs/CONFIGURATION.md (the clinical-prose style: a leading "surfaces, by kind" table, `File | Applies to` scoping, backticked identifiers, bold for requirements, every statement names its evidence) - bot/src/i18n/strings.ts (the D-05 law in the header + the two helpMenu strings) - bot/test/commands.test.mjs (the CS5 menu parse — the pointer line must not start with "/") - .planning/phases/07-hardening-private-launch/07-CONTEXT.md (D-08 verbatim) - routing/scripts/measure-route-cpu.mjs (the LADDER HISTORY, lines 30–40 — the c3 evidence) Create `docs/KNOWN-LIMITATIONS.md` (NEW — the whole file is new), in the `docs/CONFIGURATION.md` clinical-prose style (no marketing, no emoji, one honest sentence per claim, every claim naming its evidence). It is written for TWO readers — a DRIVER and a FORK — and must contain these sections in this order:
  1. # Known limitations + a one-paragraph lead: what araharah is today (a KL-metro turn-by-turn navigator inside Telegram, on OpenStreetMap data), and the plain statement that this page lists what it does NOT do well.
  2. ## Data coverage — the OSM gaps, honestly:
    • No traffic awareness. There is no live traffic feed; the ETA is a distance-and-speed estimate from the route model, not a prediction.
    • No lane guidance. There is no lane-level arrow guidance; the maneuver glyph names the turn, not the lane.
    • Speed limits appear only where OSM maps them — many KL roads have no maxspeed tag, so no limit is shown.
    • Turn restrictions and one-ways are only as good as the OSM snapshot the road graph was built from; a recent change may not be reflected.
    • The road graph is a build-time artifact (routing/data/kl-roadgraph.v1.bin), rebuilt by a maintainer — forks rebuild their own (docs/ROAD-GRAPH.md).
  3. ## Cross-metro routes may not resolve — the c3 class, in one paragraph: short product routes (2–8 km) measure 2–8 ms of Worker CPU (inside the free plan's 10 ms), while cross-metro routes (20–30 km) measured p95 38–93 ms and therefore degrade HONESTLY to the router-pending card ("the router is not ready for this trip") rather than freezing or drawing a wrong line. Evidence: routing/scripts/measure-route-cpu.mjs (LADDER HISTORY) and the Phase 6 plan summary. The committed upgrade lever is the ALT landmark preprocessing — a NEXT-MILESTIME item, NOT available in this release. State the free-tier reason: Workers Paid was rejected (the $0 mandate).
  4. ## The /cari command may be missing from the Telegram menu — the Telegram setMyCommands non-default-scope defect. State the symptom: the menu shows /help and /privacy but not /cari (Telegram resolves a chat's menu from the narrowest scope that has ANY commands set, and a stale all_private_chats set shadows the default scope). The WORKAROUND: type /cari in the chat — the command works. The bot re-arms every scope (bot/scripts/register-commands.mjs) but the Telegram client may cache the old menu until the app restarts.
  5. ## Beta quality — one honest paragraph: this is a private beta with a handful of drivers in Kuala Lumpur; the device matrix was walked on real Android, iOS, Telegram Desktop and a plain browser; battery drain was MEASURED on one Android phone (record the method and the caveats pointer: the launch gate file holds the run); fullscreen and orientation behaviour varies by Telegram client version, and the app degrades to a usable portrait/landscape layout when the platform refuses a fullscreen request. Name the KNOWN limits honestly: no voice guidance, no live trip sharing, no toll toggle, no traffic ETA — those are post-validation milestones, not defects.
  6. ## What is being worked on next — the roadmap pointers D-08 names, each with its honest status: the WASM-router track (a client-side router as the static-hosted endgame), the ALT landmark preprocessing (the cross-metro lever), and the "engine B" (a fully client-side webview router). State that none of them are in this release.
  7. ## Privacy in one paragraph — coordinates live only in memory and in TTL-expiring KV (15-minute sessions, 5-minute location pins, 2-hour guidance envelopes); there is NO telemetry of any kind in this release — no analytics, no crash reporting, no usage tracking; the shipped bundle contacts only the map tile host, the Telegram API, and the app's own Worker. Point at docs/PRIVACY-AUDIT.md for the full record.
  8. ## How to run the audit yourself — for the FORK reader: the exact command npm run privacy-audit and the exact command npm run budget-audit, with one line on what each proves and what exit code 2 means (environment drift, never a pass).

THEN — in bot/src/i18n/strings.ts, add ONE pointer line to the END of BOTH helpMenu strings. It MUST NOT start with / (CS5). Use EXACTLY these strings:

  • en — append \nKnown limits: https://github.com/araharah/araharah/blob/main/docs/KNOWN-LIMITATIONS.md
  • ms — append \nHad panduan: https://github.com/araharah/araharah/blob/main/docs/KNOWN-LIMITATIONS.md

The URL MUST be identical in both locales and MUST appear in EXACTLY ONE place per locale (the strings module is the single copy source — the D-05 law). If the repository's public URL is not github.com/araharah/araharah, use the repo's actual remote URL (run git remote -v and use the HTTPS remote) — but the SAME URL in both locales, and record the choice in the SUMMARY.

After the edit, run npm test -w bot — CS5 must still pass (the pointer line is filtered out of the menu parse because it does not start with /). <acceptance_criteria> - docs/KNOWN-LIMITATIONS.md exists and contains the section headings ## Data coverage, ## Cross-metro routes may not resolve, ## The /cari command may be missing from the Telegram menu, ## Beta quality, ## What is being worked on next, ## Privacy in one paragraph, ## How to run the audit yourself - grep -c "KNOWN-LIMITATIONS.md" bot/src/i18n/strings.ts returns exactly 2 (one per locale — the single-copy law) - grep -c "Known limits:" bot/src/i18n/strings.ts returns 1 and grep -c "Had panduan:" bot/src/i18n/strings.ts returns 1 - grep -c "helpMenu" bot/src/i18n/strings.ts returns ≥ 2 (unchanged shape) - npm test -w bot exits 0 with 0 failed (CS5 still green — the pointer line is not parsed as a command) </acceptance_criteria> export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm test -w bot 2>&1 | tail -12 && grep -c "KNOWN-LIMITATIONS.md" bot/src/i18n/strings.ts The limitations document exists with all seven sections and the honest c3 and /cari content, and /help carries one pointer line per locale that the CS5 menu gate still ignores.

task 3: the /cari re-arm attempt + the A1/A3 assumption closure, recorded honestly .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md - .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md (the A1 stale-fix leg at line 44 and the A3 local-dev note at line 99, plus the FINAL VERDICT table at line 258) - bot/scripts/register-commands.mjs (the re-arm tool — its scope table) - .planning/phases/07-hardening-private-launch/07-CONTEXT.md (D-07 items 2 and 3) Two records, both in `.planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md` (append a new dated section at the END of that file — do not rewrite its history):

A — the /cari re-arm attempt. Attempt to re-arm the command menu by running the existing tool:

npm run bot:commands

This requires BOT_TOKEN in bot/.dev.vars (gitignored) or the environment. Record the outcome HONESTLY in a new ## 2026-10-25 — Phase 7 debt closure (/cari re-arm + A1/A3) section:

  • If the script ran and every scope reported ok:true, record the six scope labels it printed and the literal line all command scopes armed.
  • If it could not run (no token, no network, a Telegram API error), record the EXACT error line and mark the re-arm NOT RUN — a fabricated "re-armed" claim is forbidden (D-05's honesty law).
  • In BOTH cases, state the residual risk: Telegram clients cache the command menu; the defect is a Telegram setMyCommands scope-resolution behaviour, not a bot bug — and docs/KNOWN-LIMITATIONS.md now carries the user-facing statement plus the workaround (typing /cari).

B — the A1/A3 assumption closure. In the same section, close or re-point the two Phase-3 assumptions:

  • A1 (the Android stale-fix hypothesis — that navigator.geolocation.getCurrentPosition() refreshes the stale native cache so the next LocationManager.getLocation() returns fresh coordinates). The Phase-3 device matrix recorded the A1 leg as passed with the note "A1 notes stand in 03-RESEARCH.md" — the walk completed WITHOUT needing the toggle. Record the honest closure: A1 is NOT RETIRED as a mechanism hypothesis — it was simply not needed on the tested device and route; the app's chain degrades gracefully to polling when the stale-fix path does not fire. The risk it covers is therefore MITIGATED-BY-DESIGN rather than closed. State that in one sentence and point at the degradation path (web/src/location/chain.ts).
  • A3 (that localhost:8787 is a secure context so navigator.geolocation works under wrangler dev while a LAN-IP dev URL would not). Record the closure: A3 IS RETIRED as a decision constraint — the project deploys to a remote origin (workers.dev), not to a LAN-IP dev server, so the assumption no longer constrains any open decision. State it in one sentence.

Do NOT edit the Phase-3 FINAL VERDICT table (it is the historical record); the new dated section is the closure. <acceptance_criteria> - .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md contains the literal ## 2026-10-25 — Phase 7 debt closure (/cari re-arm + A1/A3) - the new section contains the literal A1 and the literal A3 and the literal /cari - the new section contains either all command scopes armed (the run happened) OR an explicit NOT RUN with the exact error line (it did not) — never a fabricated success - grep -c "FINAL VERDICT" .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md returns 1 (the historical table is untouched) </acceptance_criteria> export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && grep -c "Phase 7 debt closure" .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md && grep -c "A3 IS RETIRED" .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md The /cari re-arm outcome and the A1/A3 closure are recorded in the Phase-3 device matrix, honestly — with the re-arm either armed or explicitly NOT RUN.

<threat_model>

Trust Boundaries

Boundary Description
The Telegram client → the bot's command menu The client resolves the menu from the narrowest scope with any commands; a stale scope silently hides a command.
The initData comparator → the session gate The 900 s freshness edge is the authentication boundary; a >= regression widens the acceptance window by one second.
The limitations document → the driver A doc that overstates capability is a false promise; the doc must state only what the measurements support.

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-07-08 Spoofing (Authentication Bypass) bot/src/security/initdata.ts — the 900 s freshness comparator mitigate The exact-900s inclusive boundary is pinned by a named test vector (T4 freshness boundary: age 900 s → ok:true); a >= regression turns that case red. The comparator itself is unchanged — the test is what makes the existing claim a pin.
T-07-23 Spoofing bot/wrangler.jsonc — the stale comment telling a future maintainer to re-create the namespace mitigate IN-02 replaces the plan-scoped comment with a present-tense record naming the live id's provenance; a maintainer following the comment can no longer create a replacement namespace.
T-07-24 Elevation of Privilege the /cari menu gap — a driver cannot find the search command mitigate The re-arm tool arms every shadowing scope (default, default+ms, all_private_chats, all_private_chats+ms) and clears the group scopes; the residual Telegram-side caching behaviour is disclosed to drivers with the type-the-command workaround in docs/KNOWN-LIMITATIONS.md. The command itself was always functional — the gap is discoverability, not authorization.
T-07-25 Information Disclosure docs/KNOWN-LIMITATIONS.md — a doc that names internal identifiers accept The doc names the road-graph binary path and the CPU measurement script — both are PUBLIC repository artifacts under GPL-3.0 and are documented for forks by design. No secret, no token, no coordinate appears.
T-07-26 Repudiation the debt closure record mitigate The re-arm outcome is recorded either as the printed scope results or as an explicit NOT RUN with the verbatim error — the record never asserts an outcome that did not happen (the phase's no-fake protocol).
</threat_model>
Run after all tasks:
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"
cd /home/C-Fu/araharah
npm test -w bot
npm run bot:typecheck

Both must exit 0. bot/test/commands.test.mjs's CS5 case must pass — the limitations pointer line must stay invisible to the command-menu parse.

<success_criteria>

  • The dead ParsedInitData export is gone, the three read helpers carry their intent comments, and the stale wrangler comment states the post-fact truth.
  • The exact-900s inclusive boundary is pinned by a green test vector.
  • docs/KNOWN-LIMITATIONS.md exists with all seven sections, covering OSM data gaps, the c3 cross-metro degradation with its measurement evidence, the /cari menu defect with its workaround, the beta quality statement, the roadmap pointers, the privacy posture, and the fork-facing audit commands.
  • /help carries exactly one limitations pointer line per locale, in the strings module only, and CS5 still passes.
  • The /cari re-arm attempt and the A1/A3 closure are recorded in the Phase-3 device matrix honestly. </success_criteria>
After completion, create `.planning/phases/07-hardening-private-launch/07-05-SUMMARY.md`