26 KiB
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 |
|
true |
|
|
|
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).
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.
# 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.## 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
maxspeedtag, 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).
## 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).## The /cari command may be missing from the Telegram menu— the TelegramsetMyCommandsnon-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/cariin 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.## 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.## 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.## 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 atdocs/PRIVACY-AUDIT.mdfor the full record.## How to run the audit yourself— for the FORK reader: the exact commandnpm run privacy-auditand the exact commandnpm 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.
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 lineall 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
setMyCommandsscope-resolution behaviour, not a bot bug — anddocs/KNOWN-LIMITATIONS.mdnow 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 nextLocationManager.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:8787is a secure context sonavigator.geolocationworks underwrangler devwhile 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> |
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
ParsedInitDataexport 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.mdexists 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./helpcarries 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>