ambek .planning
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
---
|
||||
phase: 02-bot-skeleton-initdata-security
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["02-01", "02-02"]
|
||||
files_modified:
|
||||
- bot/src/i18n/strings.ts
|
||||
- bot/src/bot/bot.ts
|
||||
- bot/src/bot/commands/start.ts
|
||||
- bot/src/bot/commands/help.ts
|
||||
- bot/src/bot/commands/privacy.ts
|
||||
- bot/src/webhook.ts
|
||||
- bot/scripts/register-commands.mjs
|
||||
- bot/scripts/set-webhook.mjs
|
||||
- bot/test/commands.test.mjs
|
||||
- bot/wrangler.jsonc
|
||||
autonomous: false
|
||||
requirements: [ONBD-01, PRIV-01]
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-01/D-02/D-04: a real Telegram user sends /start (or /mula) and receives one honest clinical sentence with no buttons/inline keyboards"
|
||||
- "D-03/D-04: the bot's command menu lists exactly /help and /privacy via setMyCommands in default + ms scopes — nothing else, no stub future entries"
|
||||
- "D-01/D-05/D-17: /privacy states stored-facts and never-facts in en + ms that match the code's actual KV writes (PRIV-01)"
|
||||
- "D-05: unknown Telegram locales fall back to English; ms-prefixed locales get the Malay surface"
|
||||
- "D-14/D-16/D-17: webhook 401s without the secret-token header; only validated initData identities obtain a one-time token; rejection logs are category-only; deploy is local wrangler + setWebhook"
|
||||
artifacts:
|
||||
- path: "bot/src/i18n/strings.ts"
|
||||
provides: "Single copy source of truth (en + ms with identical key-sets)"
|
||||
contains: "pickLocale"
|
||||
- path: "bot/src/webhook.ts"
|
||||
provides: "Worker entry: /telegram/webhook + /api/session security gate + D-14 category-only logs"
|
||||
contains: "cloudflare-mod"
|
||||
- path: "bot/scripts/register-commands.mjs"
|
||||
provides: "One-time setMyCommands (default + ms scope — D-03/D-04)"
|
||||
contains: "setMyCommands"
|
||||
- path: "bot/scripts/set-webhook.mjs"
|
||||
provides: "setWebhook with secret_token (D-16 deploy scripting)"
|
||||
contains: "secret_token"
|
||||
key_links:
|
||||
- from: "bot/src/webhook.ts"
|
||||
to: "bot/src/security/initdata.ts"
|
||||
via: "handleSession → validateInitData → 401 on rejection / mint on success"
|
||||
pattern: "validateInitData"
|
||||
- from: "bot/src/webhook.ts"
|
||||
to: "bot/src/kv/sessions.ts"
|
||||
via: "mintSession called ONLY after ok:true (one mint path — PRIV-02)"
|
||||
pattern: "mintSession"
|
||||
- from: "bot/src/webhook.ts"
|
||||
to: "bot/src/bot/bot.ts"
|
||||
via: "webhookCallback(bot, 'cloudflare-mod', secretToken=env.WEBHOOK_SECRET)"
|
||||
pattern: "webhookCallback"
|
||||
- from: "bot/src/i18n/strings.ts"
|
||||
to: "bot/src/bot/commands/*.ts"
|
||||
via: "handlers import copy from strings (no inline copy — D-05)"
|
||||
pattern: "strings"
|
||||
user_setup:
|
||||
- service: telegram-bot
|
||||
why: "Real BOT_TOKEN — webhook validation, session minting, and command-menu registration all authenticate with the bot token from @BotFather"
|
||||
env_vars:
|
||||
- name: BOT_TOKEN
|
||||
source: "Telegram @BotFather -> /newbot -> telegram -> copy token"
|
||||
dashboard_config:
|
||||
- task: "Keep token secret; paste into bot/.dev.vars (never commit)"
|
||||
location: "local file bot/.dev.vars (D-15)"
|
||||
- service: cloudflare
|
||||
why: "Workers deploy target + KV namespace chat_sessions (D-16 deploy gate)"
|
||||
env_vars:
|
||||
- name: CLOUDFLARE_API_TOKEN (or automated wrangler login)
|
||||
source: "Cloudflare Dashboard -> My Profile -> API Tokens (or `npx wrangler login` browser flow)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the user-facing Telegram surface (commands + honest privacy copy + Malay aliases) and wire the Worker: webhook route with secret-token auth, `/api/session` security gate minting tokens, command-menu registration, deploy plumbing — then pause for the human deploy gate (D-16) that proves the whole phase in a real client.
|
||||
|
||||
Purpose: ROADMAP SC1/SC2 (visible command surface, honest /privacy) become real bot behavior; the tested security core becomes reachable (`/api/session` is the phase's security-gate entry).
|
||||
Output: runtime-complete bot + wired worker + one-time registration scripts; deploy checkpoint for ONBD-01.
|
||||
</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/phases/02-bot-skeleton-initdata-security/02-CONTEXT.md
|
||||
@.planning/phases/02-bot-skeleton-initdata-security/02-RESEARCH.md
|
||||
@.planning/phases/02-bot-skeleton-initdata-security/02-VALIDATION.md
|
||||
@.planning/phases/02-bot-skeleton-initdata-security/02-01-SUMMARY.md
|
||||
@.planning/phases/02-bot-skeleton-initdata-security/02-02-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Contracts consumed from plan 01/02 files. -->
|
||||
|
||||
From bot/src/config.ts: `interface Env { BOT_TOKEN: string; WEBHOOK_SECRET: string; chat_sessions: KVNamespace }`
|
||||
|
||||
From bot/src/security/initdata.ts (plan 02):
|
||||
```typescript
|
||||
export type InitDataRejection = "malformed" | "tampered" | "stale";
|
||||
export async function validateInitData(raw: string, botToken: string,
|
||||
opts?: { nowSeconds?: number; maxAgeSeconds?: number }): Promise<{ ok: true; user: {id: number; language_code?: string}; chat?: {id: number; type: string}; languageCode: string | null; authDate: number } | { ok: false; reason: InitDataRejection }>;
|
||||
```
|
||||
|
||||
From bot/src/kv/sessions.ts (plan 02):
|
||||
```typescript
|
||||
export const SESSION_TTL_SECONDS = 900;
|
||||
export interface SessionClaim { user_id: number; chat_id: number; issued_at: number; language_code: string | null; }
|
||||
export async function mintSession(kv: KVNamespace, claim: SessionClaim): Promise<string>;
|
||||
```
|
||||
|
||||
grammY webhook adapter (plan 02 RESEARCH verified): `webhookCallback(bot, "cloudflare-mod", onTimeout, timeoutMilliseconds, secretToken)` from `"grammy"` — constant-time X-Telegram-Bot-Api-Secret-Token check whose config is this plan's `env.WEBHOOK_SECRET`.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>task 1: strings module + command surface with aliases (D-01, D-02, D-03, D-04, D-05)</name>
|
||||
<files>bot/src/i18n/strings.ts, bot/src/bot/commands/start.ts, bot/src/bot/commands/help.ts, bot/src/bot/commands/privacy.ts, bot/src/bot/bot.ts, bot/test/commands.test.mjs</files>
|
||||
<read_first>
|
||||
- .planning/phases/02-bot-skeleton-initdata-security/02-CONTEXT.md (D-01..D-05 verbatim + <specifics> "strings module is the copy's tokens.css")
|
||||
- .planning/phases/02-bot-skeleton-initdata-security/02-RESEARCH.md § Code Examples (setMyCommands localized scopes — grammar verified)
|
||||
- bot/src/kv/sessions.ts (SESSION_TTL_SECONDS / claim fields — the copy's "what is stored" facts must match this code, D-17)
|
||||
- bot/test/harness.mjs (test API)
|
||||
</read_first>
|
||||
<action>
|
||||
1. **bot/src/i18n/strings.ts** — single copy source of truth (D-05), en default + `ms` (Bahasa Melayu), clinical + honest tone (D-01, no emoji, no marketing). Keys (identical key-set in both locales — test asserts):
|
||||
```typescript
|
||||
export interface BotStrings {
|
||||
startIntro: string; // D-01: what araharah is, one sentence
|
||||
privacyStored: string; // exactly what is stored — must name: Telegram user id, chat id, language, kept minutes (claim fields from sessions.ts)
|
||||
privacyNotStored: string; // exactly what is NOT stored — must name: location, routes, trip history, no profile (D-17: copy verifiable against code)
|
||||
helpMenu: string; // lists only /help and /privacy (D-03 — no stub entries for future commands)
|
||||
}
|
||||
export type Locale = "en" | "ms";
|
||||
export const strings: Record<Locale, BotStrings> = { en: {…}, ms: {…} };
|
||||
export function pickLocale(telegramLanguageCode?: string | null): Locale;
|
||||
// "ms" prefix match → "ms"; every other value (incl. null/undefined) → "en" (D-05 fallback rule)
|
||||
```
|
||||
Concrete copy (use as written; MS kept parallel in meaning):
|
||||
- en.startIntro: `"araharah — navigation through Telegram for the KL metro. Everything it knows about you expires in about fifteen minutes; /privacy shows exactly what that means."`
|
||||
- en.privacyStored: `"What araharah keeps: your Telegram user id, this chat id, and your interface language — kept only for about 15 minutes so the bot can answer you, then gone."`
|
||||
- en.privacyNotStored: `"What araharah never keeps: your location, your routes, your trip history, any profile about you."`
|
||||
- en.helpMenu: Commands `/help …` and `/privacy …` descriptions (matches the setMyCommands registry — D-03; /help itself is a live command this phase so listing it is honest).
|
||||
- ms.startIntro: `"araharah — navigasi melalui Telegram untuk kawasan KL. Semua yang diketahui tentang anda luput dalam lebih kurang lima belas minit; /privacy menunjukkan dengan tepat maksudnya."`
|
||||
- ms.privacyStored: `"Yang araharah simpan: id pengguna Telegram anda, id chat ini, dan bahasa antara muka anda — disimpan hanya sekitar 15 minit supaya bot boleh menjawab anda, kemudian hilang."`
|
||||
- ms.privacyNotStored: `"Yang araharah tidak pernah simpan: lokasi anda, laluan anda, sejarah perjalanan anda, apa-apa profil tentang anda."`
|
||||
- ms.helpMenu: parallel to en.
|
||||
2. **Commands as pure builders + thin grammar wiring (testable without a Bot instance):** each command module exports `replyText(kind, locale)` pure functions and the grammy handler that calls them. Handlers use **plain text-only replies** — `ctx.reply(text, { link_preview_options: { is_disabled: true } })` and NOTHING else (D-02: no reply keyboard, no inline buttons; the structural assertion lives in the test).
|
||||
- `start.ts`: exports handler for both `/start` and `/mula` (aliases on the same command stack, D-04)
|
||||
- `help.ts`: `/help` handler (no alias — D-04 names later-phase aliases only)
|
||||
- `privacy.ts`: handlers for `/privacy` and `/privasi` (D-04); reply = privacyStored + "\n\n" + privacyNotStored
|
||||
3. **bot/src/bot/bot.ts:** `createBot(env: Env): Bot` — instantiates `new Bot(env.BOT_TOKEN)`, registers handlers above (alias forms via `bot.command("mula", …)` etc.), and **sets NO keyboards, NO inline buttons anywhere** (D-02). Menu registration lives in the one-time script (next item), not per-request.
|
||||
4. **bot/scripts/register-commands.mjs** (one-time; D-03/D-04 — node, zero deps beyond `node:fs`):
|
||||
- Reads `BOT_TOKEN` from `bot/.dev.vars` (parse `KEY=VALUE` lines)
|
||||
- `fetch` to `https://api.telegram.org/bot<TOKEN>/setMyCommands` twice:
|
||||
- default scope: `{ "commands": [ {command:"help", desc-en}, {command:"privacy", desc-en} ], "bot_command_scope": { "type": "default" } }`
|
||||
- `ms` scope: same two commands with Malay descriptions + `"language_code": "ms"`
|
||||
- Exits 0 on `ok:true`; prints the JSON response; no other commands registered (D-03 — no stub entries for future commands like /cari)
|
||||
5. **bot/test/commands.test.mjs:**
|
||||
- strings integrity: en/ms key-sets identical (assert key arrays sort-equal); `pickLocale("ms-MY") → "ms"`, `pickLocale("en") → "en"`, `pickLocale(null/undefined/\"id\") → "en"`
|
||||
- PRIV-01 copy gate: `strings.en.privacyNotStored` + `ms.privacyNotStored` mention location, routes, history; `privacyStored` mentions the TTL minutes wording
|
||||
- D-02 structural: the bot module source contains no `"keyboard"` or `"inline_keyboard"`/`InlineKeyboard` token (grep gate on bot/src/bot/bot.ts + commands files)
|
||||
- helpMenu text matches the two descriptions passed to register-commands (gap-free D-03)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm test -w bot && npm run bot:typecheck && node --check bot/scripts/register-commands.mjs</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `bot/src/i18n/strings.ts` exports `strings` (Record<"en"|"ms", BotStrings>) and `pickLocale`, with identical key-sets across locales (suite asserts)
|
||||
- commands.test.mjs asserts privacyNotStored text mentions location + routes + history (PRIV-01 gate) and privacyStored mentions 15 minutes
|
||||
- bot/src/bot/bot.ts + command files grep: zero occurrences of `keyboard` / `inline_keyboard` (D-02)
|
||||
- register-commands.mjs contains exactly two fetch calls to `setMyCommands`, one with `"language_code": "ms"`, one without; command lists contain only `help` and `privacy` (D-04)
|
||||
- `npm test -w bot` exit 0, `npm run bot:typecheck` exit 0
|
||||
</acceptance_criteria>
|
||||
<done>Copy lives only in strings.ts (en+ms); /start /help /privacy + /mula /privasi handlers wired text-only; menu registration script ready with honest D-03 scope.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>task 2: Worker entry — webhook route, /api/session security gate, startWebhook plumbing (D-14, D-16, D-17)</name>
|
||||
<files>bot/src/webhook.ts, bot/scripts/set-webhook.mjs, bot/wrangler.jsonc</files>
|
||||
<read_first>
|
||||
- bot/src/webhook.ts is new; read bot/src/bot/bot.ts, src/security/initdata.ts, src/kv/sessions.ts (contracts in <interfaces>)
|
||||
- .planning/phases/02-bot-skeleton-initdata-security/02-RESEARCH.md § Pattern 1 (real webhookCallback signature — verify before wiring) + § RESEARCH.md Architecture diagram
|
||||
- bot/wrangler.jsonc (created in plan 01 — KV_ID_PLACEHOLDER remains until task 3 checklist fills it)
|
||||
</read_first>
|
||||
<action>
|
||||
1. **bot/src/webhook.ts** (the D-11 "worker entry"):
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/telegram/webhook") {
|
||||
const bot = createBot(env); // env-bound token → per-request instantiation (research-verified pattern)
|
||||
const cb = webhookCallback(bot, "cloudflare-mod", undefined, undefined, env.WEBHOOK_SECRET);
|
||||
return cb(req);
|
||||
}
|
||||
if (url.pathname === "/api/session") { // Phase 2 security gate — Mini App caller arrives Phase 4
|
||||
return handleSession(req, env);
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
|
||||
async function handleSession(req: Request, env: Env): Promise<Response> {
|
||||
if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
|
||||
const raw = await req.text(); // the verbatim Telegram.WebApp.initData string
|
||||
const result = await validateInitData(raw, env.BOT_TOKEN);
|
||||
if (!result.ok) {
|
||||
console.log(`[initdata] rejected: ${result.reason}`); // D-14 category-only — NO raw payload, NO user ids, NO timestamps
|
||||
return new Response(JSON.stringify({ error: result.reason }), { status: 401, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
const token = await mintSession(env.chat_sessions, {
|
||||
user_id: result.user.id,
|
||||
chat_id: result.chat?.id ?? result.user.id, // private chat: Telegram conflates; claim surface unchanged (D-08)
|
||||
issued_at: Math.floor(Date.now() / 1000),
|
||||
language_code: result.languageCode,
|
||||
});
|
||||
return new Response(JSON.stringify({ token }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
```
|
||||
(Replay: a repeated valid initData rotates through mintSession — plan 02's D-07 invariant closes it; the endpoint adds no extra branch.)
|
||||
2. **bot/scripts/set-webhook.mjs:** one-time; reads BOT_TOKEN + WEBHOOK_URL + WEBHOOK_SECRET from `bot/.dev.vars`; `fetch` `https://api.telegram.org/bot<TOKEN>/setWebhook` with `{ "url": WEBHOOK_URL, "secret_token": WEBHOOK_SECRET, "allowed_updates": ["message"] }`; prints result; exit 0 only on `ok:true` (D-16).
|
||||
3. **bot/package.json scripts (edit — no new file):** `"deploy": "wrangler deploy && node scripts/set-webhook.mjs"` (D-16: deploy + webhook in one command), keeping `commands`, `dev`, `test`, `typecheck` from plan 01.
|
||||
4. **bot/wrangler.jsonc:** add `"workers_dev": true` comment marker only; leave `KV_ID_PLACEHOLDER` for task 3 (deployment checklist fills it).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm test -w bot && npm run bot:typecheck && (cd bot && npx wrangler deploy --dry-run --outdir /tmp/opencode/worker-dryrun)</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `bot/src/webhook.ts` routes exactly `/telegram/webhook` and `/api/session` (dry-run bundle accepts `satisfies ExportedHandler<Env>`)
|
||||
- The only initdata-related log statement is category-only: grep `bot/src/webhook.ts` for `console.log` statements each matching `\`[initdata] rejected: ${…`} — no payload/ids logging (D-14)
|
||||
- `set-webhook.mjs` passes `secret_token` = WEBHOOK_SECRET into setWebhook payload (verified secret-token flow)
|
||||
- `wrangler deploy --dry-run` exits 0 (no account credentials needed — local esbuild only)
|
||||
- `npm run bot:typecheck` + `npm test -w bot` exit 0 (existing suites unbroken)
|
||||
</acceptance_criteria>
|
||||
<done>Worker builds for deploy; security gate reachable at /api/session; webhook auth + D-14 category logging in place; nothing secret leaves `env` reading.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>task 3: deploy gate — real-client walkthrough of the full command surface (D-16, ONBD-01)</name>
|
||||
<action>Automate everything possible first (credentials aside): wrangler kv namespace create + fill id, `npm run bot:deploy` (wrangler deploy + setWebhook), `npm run bot:commands`, KV key spot-check — then walk the user through the numbered real-client verification in <how-to-verify>. Human-only steps: @BotFather token, wrangler login/browser auth, Telegram client walkthrough (a–e).</action>
|
||||
<what-built>
|
||||
The complete Phase 2 bot: grammY bot with /start /help /privacy (+ /mula /privasi aliases), honest RAMS-tone privacy copy (en + ms), TTL-only KV sessions behind the /api/session initData security gate, secret-token-protected webhook, all under CI test vectors (green suites). All backend code paths are automated-verified; what remains needs a real Telegram client, a real bot token, and a real Worker deploy (D-16: no CI yet — human deploy gate).
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
OpenCode automates everything below EXCEPT the credential steps and the real-client walkthrough; run these in order:
|
||||
1. Credentials (human-only): create bot via @BotFather → copy token into `bot/.dev.vars` as `BOT_TOKEN=...`; generate `WEBHOOK_SECRET=$(openssl rand -hex 32)` and add to `bot/.dev.vars`; authenticate Cloudflare (`npx wrangler login` or CLOUDFLARE_API_TOKEN).
|
||||
2. OpenCode runs: `npx wrangler kv namespace create chat_sessions` → copy returned id into `bot/wrangler.jsonc` replacing `KV_ID_PLACEHOLDER`; set `WEBHOOK_URL` in `.dev.vars` to the printed workers.dev URL after `npm run bot:deploy` (deploy + setWebhook via scripts).
|
||||
3. OpenCode runs `npm run bot:commands` (registers /help /privacy default + ms scopes).
|
||||
4. Real-client walkthrough (human, this checkpoint): in Telegram against your own bot —
|
||||
a. send `/start` → one sentence intro, NO buttons (D-01/D-02)
|
||||
b. open the command menu (menu button) → ONLY /help and /privacy listed (D-03)
|
||||
c. `/privacy` → stored-facts block (user id, chat id, language, ~15 min) + never-block (location, routes, history, profile)
|
||||
d. `/mula` behaves exactly like /start; `/privasi` like /privacy
|
||||
e. send gibberish `/xyz` → no crash, no menu drift
|
||||
5. Storage spot-check (PRIV-02 work-around): OpenCode runs `npx wrangler kv key list` after a manual /start — only `session:*`/`chat:*` keys, TTL-bounded, no coordinates; keys vanish after ~15 min.
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" or describe issues — deploy gate per ROADMAP SC1–SC4 (ONBD-01, PRIV-01, PRIV-02) must all be observed before phase exit.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Telegram servers → webhook endpoint | updates arrive over the public internet; secret-token header is the gatekeeper |
|
||||
| Mini App client → /api/session | initData arrives untrusted from the user's webview |
|
||||
| Worker logs → Cloudflare log stream | anything logged becomes visible outside the app |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-02-03 | DoS / webhook flooding | webhook route | mitigate | grammY's verified constant-time secret-token compare 401s garbage BEFORE bot processing; no KV touched pre-check |
|
||||
| T-02-01 | Spoofing (forged identity → token) | /api/session | mitigate | Only validateInitData→mintSession path mints; rejection classes log category-only, return 401s |
|
||||
| T-02-02 | Replay of stale-but-valid initData | /api/session | mitigate | 900s freshness window + D-07 rotate via plan-02 mintSession |
|
||||
| T-02-04 | Information Disclosure (logs, SSRF/repo) | webhook.ts, scripts/* | mitigate | D-14 category logs; secrets read from .dev.vars/ secrets at runtime only; scripts never echo the bot token (grep-gate: no `console.log` of BOT_TOKEN/secret) |
|
||||
| T-02-06 | Information Disclosure (KV persistence drift) | webhook.ts | mitigate | only mintSession writes KV (TTL-enforced by plan-02 suite); endpoint adds no storage |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npm test -w bot` + `npm run bot:typecheck` exit 0 (all 4 suites)
|
||||
- `wrangler deploy --dry-run` exit 0
|
||||
- Manual checkpoint observes SC1–SC3 in a real client before phase exit (D-16 deploy gate; autonomous: false precisely for this)
|
||||
- Storage spot-check shows TTL-only KV keys (SC4)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Real Telegram client: /start concise intro text-only; command menu lists exactly /help and /privacy; /privacy shows stored/never-stored truth; Malay aliases co-route; en fallback for unknown locales
|
||||
- Deployed Worker rejects unauthorized webhook calls (secret-token) and unvalidated session requests (rejection classes), mints one-token-per-chat for validated identities
|
||||
- KV contains only TTL-bounded session/pointer keys — no persistent coordinates or profiles
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-bot-skeleton-initdata-security/02-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user