479 lines
26 KiB
Markdown
479 lines
26 KiB
Markdown
---
|
||
phase: 07-hardening-private-launch
|
||
plan: 05
|
||
type: execute
|
||
wave: 1
|
||
depends_on: []
|
||
files_modified:
|
||
- 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
|
||
autonomous: true
|
||
requirements:
|
||
- PX-04
|
||
must_have_notes:
|
||
- "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."
|
||
must_haves:
|
||
truths:
|
||
- "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."
|
||
artifacts:
|
||
- path: "bot/test/initdata.test.mjs"
|
||
provides: "The T4b exact-900s boundary vector (IN-03)"
|
||
contains: "age 900 s"
|
||
- path: "bot/src/security/initdata.ts"
|
||
provides: "The IN-01 cleanup — the dead export deleted, the intent comments added"
|
||
contains: "tampered-vs-malformed"
|
||
- path: "bot/wrangler.jsonc"
|
||
provides: "The IN-02 present-tense namespace comment"
|
||
contains: "chat_sessions namespace created via"
|
||
- path: "bot/src/i18n/strings.ts"
|
||
provides: "The /help limitations pointer line (en + ms)"
|
||
contains: "Known limits:"
|
||
- path: "docs/KNOWN-LIMITATIONS.md"
|
||
provides: "The known-limitations document for drivers AND forks (D-08)"
|
||
contains: "Known limitations"
|
||
key_links:
|
||
- from: "bot/src/i18n/strings.ts"
|
||
to: "docs/KNOWN-LIMITATIONS.md"
|
||
via: "the helpMenu pointer line names the doc's public URL — a SINGLE constant, never a scattered URL"
|
||
pattern: "Known limits:"
|
||
- from: "bot/src/i18n/strings.ts"
|
||
to: "bot/scripts/register-commands.mjs"
|
||
via: "the pointer line does NOT start with '/', so the menu parse (CS5) and the setMyCommands derivation are unaffected"
|
||
pattern: "Had panduan:"
|
||
- from: "bot/src/security/initdata.ts"
|
||
to: "bot/test/initdata.test.mjs"
|
||
via: "the exact-900s vector pins the strictly-greater comparator's INCLUSIVE edge"
|
||
pattern: "900"
|
||
---
|
||
|
||
<objective>
|
||
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).
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||
@$HOME/.config/opencode/get-shit-done/templates/summary.md
|
||
</execution_context>
|
||
|
||
<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
|
||
|
||
<interfaces>
|
||
The IN-03 boundary vector — copy the T4 harness shape verbatim from
|
||
`bot/test/initdata.test.mjs` lines 172–189:
|
||
|
||
```javascript
|
||
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:
|
||
|
||
```typescript
|
||
// — 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):
|
||
|
||
```typescript
|
||
/** 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:
|
||
|
||
```jsonc
|
||
// 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:
|
||
|
||
```javascript
|
||
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):
|
||
|
||
```typescript
|
||
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",
|
||
```
|
||
```typescript
|
||
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):
|
||
|
||
```typescript
|
||
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`).
|
||
</interfaces>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto">
|
||
<name>task 1: pay IN-01 and IN-02 (dead code + the stale wrangler comment) and pin IN-03 (the exact-900s boundary)</name>
|
||
<files>bot/src/security/initdata.ts, bot/test/initdata.test.mjs, bot/wrangler.jsonc</files>
|
||
<read_first>
|
||
- 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)
|
||
</read_first>
|
||
<action>
|
||
**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:
|
||
|
||
```jsonc
|
||
// 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`):
|
||
|
||
```javascript
|
||
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.
|
||
</action>
|
||
<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>
|
||
<verify>
|
||
<automated>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</automated>
|
||
</verify>
|
||
<done>
|
||
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.
|
||
</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>task 2: the known-limitations document (D-08) + the /help pointer line (en + ms)</name>
|
||
<files>docs/KNOWN-LIMITATIONS.md, bot/src/i18n/strings.ts</files>
|
||
<read_first>
|
||
- 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)
|
||
</read_first>
|
||
<action>
|
||
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 `/`).
|
||
</action>
|
||
<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>
|
||
<verify>
|
||
<automated>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</automated>
|
||
</verify>
|
||
<done>
|
||
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.
|
||
</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>task 3: the /cari re-arm attempt + the A1/A3 assumption closure, recorded honestly</name>
|
||
<files>.planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md</files>
|
||
<read_first>
|
||
- .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)
|
||
</read_first>
|
||
<action>
|
||
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:
|
||
|
||
```bash
|
||
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.
|
||
</action>
|
||
<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>
|
||
<verify>
|
||
<automated>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</automated>
|
||
</verify>
|
||
<done>
|
||
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.
|
||
</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<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>
|
||
|
||
<verification>
|
||
Run after all tasks:
|
||
|
||
```bash
|
||
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.
|
||
</verification>
|
||
|
||
<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>
|
||
|
||
<output>
|
||
After completion, create `.planning/phases/07-hardening-private-launch/07-05-SUMMARY.md`
|
||
</output>
|