Commit Graph
30 Commits
Author SHA1 Message Date
araharah-executor 212aa57c14 test(06-03): guidance endpoint suite GW1-GW11 — 401/413/400 classes, claim-owned chat (T-06-02 injection probe), own-vs-foreign GET/DELETE, 405 (RED)
- offline undici Request/Response pattern; claims minted through the real
  /api/session path (mintSession) against the Map-fake KV
- GW8 green-by-construction (the catch-all already 404s absent ids);
  ownership teeth live in GW7/GW10
2026-10-24 13:04:17 +08:00
araharah-executor c87dc7dc14 feat(06-03): guidance envelope service — TTL-only mint/get/delete beneath the frozen shape
- mintGuidanceSession: crypto.randomUUID id, claim-derived chat_id arg,
  ONE put at guidance:<id> with same-line expirationTtl (GE1/PG1)
- getGuidanceSession: UUID-shape guard BEFORE any KV touch (V5), exact
  4+6-field + type + finite/range validation (the location-pin defensive
  read law) — corrupt records read as null, never reach the cron
- deleteGuidanceSession: active trip-end sweep (D-14's server half)
- module header: the types-only note becomes the shape-frozen + service-
  owns-every-write law; envelope/snapshot interfaces byte-identical (GE2)
- no new namespace (GE5), no new Env field (GE6) — the chat_sessions
  binding is handed in structurally
2026-10-24 13:03:19 +08:00
araharah-executor 0cfa0bde38 test(06-03): re-pin GE4/GE7 to the live service + behavioral envelope suite + PG1 storage list (RED)
- GE1/GE2/GE3 UNCHANGED in meaning (TTL 7200, 4+6 field freeze, inline snapshot)
- GE4 re-pinned: types-only token ban -> the module must EXPORT mint/get/deleteGuidanceSession
- GE7 re-pinned: total consumer ban -> named-imports-only of the service surface
- GE5/GE6 stand (exactly one kv namespace; no Env guidance field)
- GS1-GS5 behavioral: TTL'd mint, roundtrip, 7-class defensive null read,
  UUID key-guard without KV touch (V5), active delete (D-14 server half)
- PG1 storageModules now covers kv/guidance-sessions.ts (same-line TTL law)
2026-10-24 13:02:28 +08:00
araharah-executor 7da7c2019c test(04-04): commit the CR13/CS5 shadow-law gate updates (missed in 54a2b54 staging) 2026-10-24 12:00:09 +08:00
araharah-executor 6302901f82 test(05-05): Worker no-route-proxy gates RP1+RP2 (D-05)
- RP1: zero /api/route + registerRoute/routeProxy tokens in comment-stripped
  bot/src — the Worker gains no route endpoint, even accidentally (T-05-16)
- RP2: zero @araharah/routing specifiers in bot/src — routing lives in the
  webview bundle only; the Worker imports only @araharah/search
- scans grow with the bot/src tree automatically (the PG-gate pattern)
- TDD note: gates target an already-clean tree, so the RED run is a teeth
  proof — a planted violating file fails both gates; removed, both pass
  (74/0 bot suite green)
2026-10-24 06:03:01 +08:00
araharah-executor d5c2bc0938 feat(05-03): freeze guidance_sessions envelope as types-only contract (GREEN)
- GuidanceSessionEnvelope: EXACTLY id, chat_id, route_snapshot, created_at
  (SessionClaim field-freeze law — a 5th property means drift)
- GuidanceRouteSnapshot INLINE (D-07: one key, one TTL, ~2h): destination
  id/name, distanceM/durationS, polyline6 geometry (05-01 storage format),
  startedAtSec resume anchor
- GUIDANCE_SESSION_TTL_SECONDS = 7200 — every Phase 6 write carries it
- guidance:<id> key-scheme doc header; zero runtime behavior (no binding,
  no function, no import — GE4 pins it)

Note: header says 'No KV binding' rather than naming the KV type because
the acceptance gate requires that token to appear NOWHERE in the file,
comments included — same law, gate-clean wording.
2026-10-24 05:09:27 +08:00
araharah-executor 0b8671e9ca test(05-03): add failing guidance-envelope types-only gates (GE1-GE7) (RED)
- GE1 TTL constant === 7200 (~2h, D-06)
- GE2 field freeze: envelope 4 fields / snapshot 6 fields (SessionClaim law)
- GE3 inline snapshot: typed inline, zero snapshotRef tokens in bot/src (D-07)
- GE4 types-only: zero behavior tokens comment-stripped (T-05-08)
- GE5-GE7 negative-space guards: one kv namespace, no Env field, no consumer
  (green by construction — redden only on drift; 05-01 C4/C5/C6 precedent)
2026-10-24 05:08:52 +08:00
araharah-executor 54a2b54b8b fix(04-04): register-commands arms every shadowing scope — stale all_private_chats set hid /cari (device-gate finding) 2026-10-24 03:08:20 +08:00
araharah-executor 135621f007 feat(04-04): /start carries the araharah logo (static-asset URL, resilient text fallback) + repo icon assets 2026-10-24 02:57:39 +08:00
araharah-executor 5dcef0e2bb test(04-03): the four CS landmines + the CR1-CR15 /cari gate suite
- commands.test.mjs: commandSources gains commands/cari.ts; CS4 documents
  the SECOND inline-keyboard carve (venue card allowed, reply keyboard
  still banned in commands/); CS5 menu becomes help/privacy/cari; CS5b's
  future-stub ban narrows to /pergi_ke (reserved for Phase 5, D-01); CS6
  registers cari+search in the alias set
- cari.test.mjs NEW: CR1-CR15 — payload shapes captured through the real
  createBot + fake baseFetch (getMe included), plus source-introspection
  gates with the stripComments helper: venue fields, OPEN URL = configured
  origin + plain slug id, NO-CRYPTO law (D-11), one-message alternates,
  callback_data discipline, honest refusal, 128-char cap, always-answer,
  category-only logging (D-15), bilingual copy, registration script, swap
  semantics, hostile-id drop
- register-commands.mjs: menu list gains cari (two fetch calls unchanged)
- [Rule 1] location-fallback.ts: the message handler now passes non-location
  messages through (next) — it matched EVERY message and silently starved
  all downstream message-stage handlers, so /cari never fired through the
  real factory; caught by the fake-transport suite
- suites: bot 65/65 (15 CR + 9 CS + all prior), web 33/33, search 57/57,
  bot:typecheck green
2026-10-24 01:21:10 +08:00
araharah-executor 13e5073261 feat(04-03): the alternates callback handler + the bot factory wiring (D-02)
- search-alternate.ts: scoped alt: callback matcher that answers FIRST
  (30-second rule), validates the id against the slug charset BEFORE the
  lookup (T-04-23), resolves via source.byId() — honest null, never a
  fabricated card; catch-all callback_query:data answers everything else
  so Telegram never spins for 60 s (T-04-27)
- sendVenueCard: the ONE card builder shared by /cari and the swap path —
  first card and swapped card structurally identical; a tap sends a NEW
  sendVenue (no in-place edits — they destroy the venue payload, T-04-28)
- alternatesFor(entry, source): async — awaits search() on the entry's own
  name, excludes the entry, caps at 4 (D-02)
- bot.ts: ONE createGazetteerSource() shared by both registrations (single
  memoized index); origin = env.MINI_APP_URL ?? ''; header names the second
  documented D-02 exception
- cari.ts now calls the shared card builder
- wrangler dry-run inlines the gazetteer (suria-klcc marker in the bundle) —
  completes the L3 bundle-marker half deferred from plan 04-01
2026-10-24 01:15:21 +08:00
araharah-executor a73a38830a feat(04-03): the /cari command + venue card + OPEN deep link (D-01/D-02/D-10/D-11)
- cari.ts: /cari + /search twin, 128-char pre-search cap (T-04-21), honest
  no-match refusal (D-13), single sendVenue card with inline OPEN + alternates
- deep-link carrier corrected per RESEARCH Pitfall 1: web_app button URL
  carries ?dest=<plain slug id> — WebAppInfo has only a url field; no crypto
- origin from configured MINI_APP_URL only (never user input); blank origin
  omits the OPEN button instead of guessing (T-04-30)
- strings.ts: searchUsage/searchNoMatch/searchQueryTooLong/searchOpenButton/
  osmAttribution in en+ms; helpMenu gains the /cari line in both locales
- category-only logging law (D-15): four literal rejection categories, query
  never logged; success path logs nothing
- config: MINI_APP_URL in Env + wrangler vars (public, not secret) +
  .dev.vars.example + docs/CONFIGURATION.md row and sections
- bot deps gain @araharah/search (workspace link)
2026-10-24 01:13:13 +08:00
araharah-executor 91ad0c9d93 docs: generate + verify project documentation — canonical docs, per-package READMEs, verified fix pass
- New: docs/ARCHITECTURE.md, docs/GETTING-STARTED.md, docs/DEVELOPMENT.md,
  docs/TESTING.md, docs/CONFIGURATION.md (colored mermaid diagrams, quoted labels)
- Regenerated README.md (arah² / arah2bot naming, canonical clone URL)
- New per-package READMEs: theme/, bot/, web/
- Verified 13 docs against the live codebase (2 fix iterations + 2 residual
  corrections); corrected 56 stale claims across AGENTS/DESIGN/IDEAS/GSD_FIX
2026-10-23 20:56:40 +08:00
araharah-executor af3edefbc6 add sume 2026-10-23 19:18:43 +08:00
araharah-executor 22fda6d12a fix(03-04): HMAC data-check-string excludes only hash — signature field participates (real clients rejected as tampered; erratum M6) 2026-10-23 01:34:19 +08:00
araharah-executor b1d8768866 fix(03-04): web_app menu button on deployed origin + wire CTA actions on both denied-shaped cards 2026-10-23 01:00:27 +08:00
araharah-executor 8b39ea70c3 feat(03-04): Worker [assets] binding + one-command deploy + D-14 build-artifact gate
- wrangler.jsonc assets block serves ../web/dist; run_worker_first keeps /api/* + /telegram/* on the Worker (D-13)
- root npm run deploy = build theme → web → bot wrangler deploy + set-webhook (D-15); dev = build + wrangler dev --port 8787 --local (D-12)
- vite.config.ts dual-build: __ARAHARAH_HARNESS__ define keyed on --mode harness, separate dist/dist-harness outDirs
- main.ts guard unchanged; ambient constant declared once in web/src/vite-env.d.ts
- build-artifact.test.mjs BA1..BA6: zero CDN refs, self-hosted fonts, MapLibre worker, inlined style, no mock installer in ANY emitted file (HTML included)
2026-10-23 00:15:26 +08:00
araharah-executor ecaf242d0e feat(03-01): GEO-02 server half both directions — chat-keyboard fallback + GET /api/location + POST /api/location-fallback (claim-scoped)
- handlers/location-fallback.ts: requestLocationFallback (ReplyKeyboardMarkup request_location:true copy from strings.ts), recordSharedPin, registerLocationFallback (filters message.location, T-03-03)
- strings.ts: awaitingPin/shareLocationButton/pinReceived in en+ms (CS1 parity) — all bot copy stays in strings.ts
- bot.ts: createBot threads env.chat_sessions into the fallback handler; optional client fetch seam for tests; one-caller rule documented
- webhook.ts: GET /api/location relay + POST /api/location-fallback trigger, both resolve chat_id & locale from getClaimByToken CLAIM, never client input (T-03-01/26/27); telegram-send failure is category-only 502 (T-03-28)
- location-pin.test.mjs: LP1–LP14 (TTL contract, range guard, cross-user isolation both routes, locale split); LP12/LP13 patch node-fetch's cached default (grammy's node shim transport), then restore
- CS4 scope untouched (handlers/ not scanned); CS1 parity automatic
2026-10-22 23:33:01 +08:00
araharah-executor 4fbbb0b516 feat(03-01): TTL-only KV location-pin layer under the existing privacy gates (GEO-03, PRIV-02)
- kv/location-pin.ts mirrors sessions.ts shape: writePin/getPin/clearPin, key loc:<chat_id> (chat-scoped, claim-derived — never user-id scoped across keys)
- PIN_TTL_SECONDS=300; writePin is the only write path, expirationTtl on the same line referencing the named constant
- getPin is a defensive read: parse guard + shape guard + |lat|<=90 / |lon|<=180 range guard (V5 Input Validation) — corrupt records return null
- privacy.test.mjs: storageModules now scans kv/location-pin.ts, so PG1 (same-line TTL) and PG2 (no persistence-shaped exports) apply automatically
2026-10-22 23:25:59 +08:00
araharah-executor af691ae40e fix(02-bot-skeleton-initdata-security): WR-03 honest rotate-on-new invariant + best-effort post-mint reconciliation (fold in IN-01b dead branch, IN-01c Phase-4 notes) 2026-10-22 20:21:11 +08:00
araharah-executor 3ea1fdd932 fix(02-bot-skeleton-initdata-security): WR-02 fail-closed /telegram/webhook — unarmed WEBHOOK_SECRET rejects (constant 500, category-only log) before grammY's accept-everything compare 2026-10-22 20:18:34 +08:00
araharah-executor 8995498bff fix(02-bot-skeleton-initdata-security): WR-01 cap /api/session body at 4 KB before JSON split (413, category-only logs) 2026-10-22 20:17:34 +08:00
araharah-executor d600d6e71a fix(02-03): set-webhook auto-appends /telegram/webhook to a pathless URL
- Deploy walkthrough revealed a bare workers.dev origin setWebhook answers ok but drops updates at / (404)
- Normalize WEBHOOK_URL: no path -> append /telegram/webhook; invalid URL -> fail fast, never echo values (T-02-04)
- .dev.vars.example documents the path expectation
2026-10-22 19:56:24 +08:00
araharah-executor 492a97bd89 fix(02-03): wire chat_sessions KV namespace id (created via wrangler kv namespace create)
- Replace KV_ID_PLACEHOLDER with real namespace id (non-secret, account-scoped)
- Closes the deliberate deploy-checkpoint input from task 2
2026-10-22 18:45:44 +08:00
araharah-executor df3b7d65c1 feat(02-03): Worker entry — webhook route + /api/session security gate + deploy plumbing (D-11, D-14, D-16, D-17)
- src/webhook.ts: routes exactly /telegram/webhook (grammY cloudflare-mod
  adapter, constant-time secret-token auth against env.WEBHOOK_SECRET) and
  POST /api/session (validateInitData → 401 category-only rejections →
  mintSession one-time token; single mint path, no extra branch — T-02-01/02/06)
- D-14: the only log statement is '[initdata] rejected: <category>' — no
  payload, ids, or timestamps
- scripts/set-webhook.mjs: setWebhook with secret_token + allowed_updates
  [message], secrets read from gitignored .dev.vars, token never echoed
- bot deploy script chains wrangler deploy + set-webhook; wrangler.jsonc
  gains workers_dev (webhook URL target); KV id stays placeholder for the
  deploy checkpoint
- verified: 27/27 tests, typecheck clean, wrangler deploy --dry-run exit 0
2026-10-22 11:49:43 +08:00
araharah-executor b4393fbcf3 feat(02-03): strings module + text-only command surface with Malay aliases (D-01..D-05)
- src/i18n/strings.ts: single copy source of truth, en + ms identical
  key-sets, clinical/honest start + privacy copy, pickLocale fallback rule
- /start /mula, /help, /privacy /privasi handlers: plain text replies with
  link previews disabled, zero menu/button surface (D-02, grep-gated)
- createBot(env) factory registers exactly the phase command stack
- scripts/register-commands.mjs: one-time setMyCommands, two scopes
  (default + language_code ms), descriptions derived from helpMenu —
  no inline copy, no future-command stubs (D-03/D-05)
- test/commands.test.mjs: CS1-CS6 gates — locale parity, pickLocale
  matrix, PRIV-01 copy gate vs never-facts + TTL wording, D-02 structural
  grep, D-03 menu gap-freedom, D-04 alias completeness (27 passing)
2026-10-22 11:48:50 +08:00
araharah-executor e9aec5833b feat(02-02): TTL-only KV sessions with rotate-on-new + PRIV-02 storage gates (D-06/D-07/D-08/D-14)
- sessions.ts: mintSession (crypto.randomUUID opaque token, T-02-05), rotate-on-new deletes prior session record via chat pointer (D-07), both puts carry expirationTtl: SESSION_TTL_SECONDS on the call line (PRIV-02)
- SESSION_TTL_SECONDS = 900 pairs the session envelope with the D-13 freshness window
- SessionClaim is exactly D-08: user_id, chat_id, issued_at, language_code — nothing else serializes to storage
- sessions.test.mjs: in-memory KV fake asserting finite expirationTtl on every put; D-08 key-set equality; rotate removes old token; expired-token → null (D-14 class)
- privacy.test.mjs: PG1 same-line expirationTtl source gate (auto-tightens to webhook.ts when plan 03 adds it) + PG2 no persistence-shaped exports
- 18/18 PASS across initdata + sessions + privacy suites; full plan GREEN
2026-10-22 11:43:20 +08:00
araharah-executor 54bffe728c feat(02-02): validateInitData — SubtleCrypto HMAC core with rejection classes (D-13, D-14)
- GREENs the plan-01 RED vector suite: T1-T7 all PASS (10/10 cases)
- Telegram-verified two-step key derivation: secretKey = HMAC(botToken, key=salt), checkHex = HMAC(dataCheckString, secretKey bytes)
- Sort by field name, hash/signature excluded, values decoded once with '+' literal (no URLSearchParams)
- Rejection precedence: malformed (hash/user.id/auth_date) -> tampered (timing-safe hex compare) -> stale (>900s, D-13)
- Hand-rolled WebAppInitUser/WebAppInitChat types (grammy ships none); zero deps of any kind — pure (raw, botToken, opts)
2026-10-22 11:41:46 +08:00
araharah-executor fc5001a4eb test(02-01): hand-rolled harness + RED initdata vector suite (D-12, TDD RED)
- bot/test/harness.mjs: zero-dep registry (test(name, fn))
- bot/test/runner.mjs: auto-discovers *.test.mjs, suite-load failure -> FAIL case, exit 0 iff all green
- bot/test/initdata.test.mjs: T1-T7 vector contract signed via node:crypto in-suite
  (T1 known-good, T2 tampered, T3 stale 3600s, T4 boundary 899/901, T5 order-independence,
  T6a-c malformed, T7 RFC-4231 node:crypto independence pin)
- demonstrated RED: 8 FAILs / exit 1 against missing src/security/initdata.ts; GREEN lands in plan 02
2026-10-22 11:38:06 +08:00
araharah-executor c9b6ba402d feat(02-01): bot workspace scaffold with Env contract (D-09/D-10/D-11/D-15)
- bot/ npm workspace sibling to theme/: grammy ^1.46.0, wrangler ^4.151.3, TS ^5.9.3
- src/config.ts exports Env (BOT_TOKEN, WEBHOOK_SECRET, chat_sessions KVNamespace)
- wrangler.jsonc: main src/webhook.ts + chat_sessions KV binding (id filled in plan 03)
- .gitignore ships .dev.vars/.wrangler/ in the same commit as .dev.vars.example (D-17)
- root delegate scripts bot:test/typecheck/dev/deploy/commands
2026-10-22 11:35:54 +08:00