docs: full docs-update pass — 9 GSD docs regenerated + verified (434→454 claims; verifier corrections applied)

This commit is contained in:
araharah-executor
2026-10-25 12:51:01 +08:00
parent 809864f5b6
commit 44851c70fe
9 changed files with 2014 additions and 324 deletions
+284
View File
@@ -0,0 +1,284 @@
<!-- generated-by: gsd-doc-writer -->
# Deployment
arah² deploys as **one Cloudflare Worker** (`araharah-bot`, defined in `bot/wrangler.jsonc`) that
serves three things from a single origin: the Telegram webhook, the `/api/*` surface, and the Mini
App's static assets (Vite output from `web/dist`). Deployment is **manual and one-command** — there
is no CI pipeline in this repo. Everything is $0: Workers free plan, KV free tier, R2 free tier.
Companion docs: `docs/CONFIGURATION.md` (the full variable list and binding contract),
`docs/ROAD-GRAPH.md` (the fork's road-graph contract), `docs/BUDGETS.md` (the measured free-plan
budget record).
---
## Deployment targets
| Target | Config file | Notes |
| --- | --- | --- |
| Cloudflare Workers (`araharah-bot`) | `bot/wrangler.jsonc` | The only deployment target. `main: src/webhook.ts`; `workers_dev: true` (serves on `<name>.<account>.workers.dev`); static assets binding `ASSETS` → `../web/dist`; `run_worker_first: ["/api/*", "/telegram/*"]` (a **glob array**, not a boolean — the Worker fetch owns those paths before static assets answer). |
| Cloudflare KV (`chat_sessions`) | `bot/wrangler.jsonc` `kv_namespaces` | Exactly one namespace (GE5). Every write in the codebase carries an explicit `expirationTtl` — TTL-only storage. |
| Cloudflare R2 (`araharah-graphs`) | `bot/wrangler.jsonc` `r2_buckets` | Hosts the road-graph binary `kl-roadgraph.v1.bin` behind the `ROAD_GRAPH` binding. A **bucket**, not a KV namespace. Forks create and upload their own (see Fork provisioning). |
| Telegram Bot API | registered via `bot/scripts/*.mjs` + @BotFather | Webhook, command menu, menu button, Mini App origin — see Telegram registration below. |
| Cron trigger | `bot/wrangler.jsonc` `triggers.crons: ["*/2 * * * *"]` | The guidance turn-card tick, every 2 minutes. Stateless: zero KV writes per tick. |
No `vercel.json`, `netlify.toml`, `fly.toml`, `serverless.yml`, Dockerfile, or GitHub Actions
workflow exists — Cloudflare Workers is the platform, by design (the $0 law).
## Prerequisites
- **Node.js + npm** (npm workspaces monorepo; wrangler runs via `npx`/devDependencies —
`wrangler ^4.151.3`).
- **A Cloudflare account** with Workers + KV enabled. Authenticate once:
```bash
npx wrangler login
```
- **A Telegram bot token** from [@BotFather](https://t.me/BotFather).
- **R2 enabled** on the account (one-time activation in the Cloudflare dashboard: **R2 → Get
started**) — the first `r2 bucket create` fails until it is.
## The one-command deploy
From the repo root:
```bash
npm run deploy
```
which composes (`package.json` root + `bot/package.json`):
1. `npm run build` → `theme` then `web`:
- `theme`: `tailwindcss -i ./src/theme.css -o ./dist/rams.css --minify` (+ fonts copied by
`theme`'s build pipeline into `dist/fonts/`)
- `web`: `vite build` → `web/dist` (hashed assets + the `_headers` cache-policy file from
`web/public/`)
2. `npm run deploy -w bot` → `wrangler deploy && node scripts/set-webhook.mjs`
- `wrangler deploy` uploads the Worker and the `web/dist` static assets in one deployment.
- `scripts/set-webhook.mjs` re-registers the Telegram webhook (see below) — this step reads
`bot/.dev.vars` (or env), so **the deploy command requires `bot/.dev.vars` to exist** with
`BOT_TOKEN`, `WEBHOOK_URL`, `WEBHOOK_SECRET` filled in.
**The `_headers` lesson (client caching):** `web/public/_headers` ships the cache policy —
`/assets/*` are content-hashed and served `Cache-Control: public, max-age=31556952, immutable`;
the HTML entry is *not* hashed and must revalidate (`max-age=0, must-revalidate`) or a stale entry
would point at deleted hashed assets. Deploy drift (a deployed Worker serving an older bundle than
`web/dist`) surfaces as a 404 on a freshly-built hashed asset — the live audits
(`npm run privacy-audit -- --live`, `npm run budget-audit -- --live`) report that honestly as
*deployment drift, exit 2*.
### MINI_APP_URL must match the deployed origin
`bot/wrangler.jsonc` carries `vars: { "MINI_APP_URL": "https://araharah-bot.araharah-bot.workers.dev" }`.
On a fork or a custom domain, change this var to your real origin **before** deploying — it builds
the `/cari` venue card's OPEN deep link and the cron card's OPEN button. It must equal the Mini App
URL registered in @BotFather: Bot API 10.2 (auto-enabled July 20, 2026) *disallows Mini App methods
from origins different from the registered Mini App domain*, and it fails **silently** —
`Telegram.WebApp.*` calls from a mismatched origin throw with no clear error. BotFather's
registered URL is the security authority.
<!-- VERIFY: the workers.dev origin in bot/wrangler.jsonc is account-specific; a fork's deployed URL differs. -->
## Secrets
Production secrets are **Cloudflare Workers secrets**, never committed (D-15/D-17):
```bash
cd bot
npx wrangler secret put BOT_TOKEN # from @BotFather
npx wrangler secret put WEBHOOK_SECRET # any strong random value — Telegram echoes it back on every update
```
`MINI_APP_URL` is a **public var**, not a secret — it lives in `wrangler.jsonc` `vars`.
For local work and for the registration scripts, the mirror is `bot/.dev.vars` (gitignored; copy
from `bot/.dev.vars.example`):
```bash
BOT_TOKEN= # @BotFather token
WEBHOOK_SECRET= # the same value wrangler secret put received
WEBHOOK_URL= # full origin + path; set-webhook.mjs auto-appends /telegram/webhook if only an origin is set
MINI_APP_URL= # public Mini App origin (wrangler.jsonc "vars" in production)
```
**Never commit `.dev.vars`; never echo secret values** — every script prints only API `ok`/error
fields, and the Worker logs categories only (see `docs/API.md`, logging law).
## Telegram registration (one-time, per bot)
After the first successful deploy, register Telegram's three surfaces. All three scripts read
secrets from `bot/.dev.vars` or the environment (`process.env` wins) and run idempotently.
### 1. Webhook — `bot/scripts/set-webhook.mjs`
Already run by `npm run deploy`; re-run standalone with:
```bash
cd bot && node scripts/set-webhook.mjs
```
- Sends `setWebhook` with `url`, `secret_token`, `allowed_updates: ["message"]`.
- **Auto-appends the path:** if `WEBHOOK_URL` is a bare origin (no path), the script normalizes it
to `<origin>/telegram/webhook`. The hard-won lesson (2026-10-22 deploy walkthrough): a bare
origin makes Telegram answer `ok:true` while delivering updates to `/` → Worker 404 →
**silent total drop of all updates**.
- Exits 0 only when Telegram answers `ok:true`.
### 2. Command menu — `bot/scripts/register-commands.mjs`
```bash
npm run bot:commands # or: cd bot && node scripts/register-commands.mjs
```
- Arms **six calls**: `default` (en) · `default` + `language_code: "ms"` ·
`all_private_chats` (en) · `all_private_chats` (ms) · **clear** `all_group_chats` ·
**clear** `all_chat_administrators`.
- Why every scope: Telegram resolves a chat's menu from the **narrowest scope that has ANY
commands set** — a stale `all_private_chats` set shadows the default scope entirely (the
`/cari`-missing-from-menu incident). Group scopes are cleared so leftovers can't shadow
anything elsewhere.
- Copy is derived from `bot/src/i18n/strings.ts` `helpMenu` lines — the registered menu and the
`/help` reply cannot drift (D-05).
- **The silent-field-name lesson:** this script previously sent the scope under the parameter name
`bot_command_scope` — an unknown field Telegram **silently ignores**. Every call then hit the
default scope, and the two "clear" calls wiped the default set entirely. `ok:true` proved
nothing. The Bot API field is `scope` (`core.telegram.org/bots/api/methods/setmycommands`).
General law: read the response back with the matching `get*` method before trusting any
registration.
### 3. Menu button — `bot/scripts/set-menu-button.mjs`
```bash
cd bot && node scripts/set-menu-button.mjs
```
- `setChatMenuButton` with `menu_button: { type: "web_app", text: "arah²", web_app: { url: origin } }`
(default scope — all private chats). The app origin is derived by stripping
`/telegram/webhook` from `WEBHOOK_URL`.
### 4. BotFather manual steps
In the @BotFather chat (not scriptable from this repo):
1. **Register the Mini App origin** — BotFather's Mini App URL must equal the deployed origin
exactly (Bot API 10.2 origin restriction — see the MINI_APP_URL section above). Check via
`/setmenubutton` / the WebApp URL settings.
2. **`/setuserpic`** — set the bot's profile picture from the project's logo asset
(`assets/MASTER_FILE-arah2_logo.svg`).
<!-- VERIFY: BotFather's exact menu paths and the registered Mini App URL are per-account Telegram configuration, not repo facts. -->
## Fork provisioning: the road graph
The R2 bucket is **not** shared — every fork provisions its own ("your graph, YOUR bucket";
`docs/ROAD-GRAPH.md` is the contract):
```bash
# 1. Create the bucket (once). Name must agree with bot/wrangler.jsonc's ROAD_GRAPH bucket_name.
npx wrangler r2 bucket create araharah-graphs
# 2. Build the KL road-graph binary (or tune bbox/classes for your region in the script).
node routing/scripts/build-road-graph.mjs
# 3. Check the artifact (decode + stats, no network):
node routing/scripts/build-road-graph.mjs --check routing/data/kl-roadgraph.v1.bin
# 4. Upload (pre-flight gates: binary exists, 1–80 MB envelope; shells wrangler r2 object put --remote).
node routing/scripts/upload-road-graph.mjs
# 5. Round-trip verify (what the script prints as its own last step):
npx wrangler r2 object get araharah-graphs/kl-roadgraph.v1.bin --file /tmp/opencode/graph-roundtrip.bin --remote
node routing/scripts/build-road-graph.mjs --check /tmp/opencode/graph-roundtrip.bin
```
If the bucket name differs from `araharah-graphs`, change `bot/wrangler.jsonc`'s
`ROAD_GRAPH.bucket_name` to match — the Worker reads the *binding*, never the name; the two must
simply agree. Until this upload succeeds, `/api/route` answers the honest router-pending `200 null`
(absent graph is a designed miss class, not an error) and the cron's cards degrade to street-less
bare-verb compositions.
## Environment setup (production checklist)
Full variable table in `docs/CONFIGURATION.md`; the production minimum:
- **Secrets:** `BOT_TOKEN`, `WEBHOOK_SECRET` (`wrangler secret put`, values in `bot/.dev.vars` too
for the scripts).
- **Public var:** `MINI_APP_URL` = the deployed origin (`wrangler.jsonc` `vars`).
- **Bindings:** `chat_sessions` (KV namespace — the id in `wrangler.jsonc` is a live created
namespace; a fork creates its own via `npx wrangler kv namespace create chat_sessions` and
updates the id), `ROAD_GRAPH` (R2 bucket), `ASSETS` (static assets, wired by `wrangler deploy`
from `../web/dist`).
- **Cron:** `*/2 * * * *` is committed in `wrangler.jsonc` and arms with the deploy.
## Post-deploy canary verification protocol
Every deploy is verified by **reading state back** — registration calls returning `ok:true` prove
nothing by themselves (the `bot_command_scope` lesson). The launch-gate canary set:
```bash
# 1. Webhook armed: url ends /telegram/webhook, pending_update_count → 0, secret-token channel armed.
curl -s "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo"
# 2. Menu button: reads back type web_app pointing at the deployed origin.
curl -s "https://api.telegram.org/bot$BOT_TOKEN/getChatMenuButton"
# 3. Command menu, PER SCOPE: each private-chat scope reads back help, privacy, cari.
curl -s "https://api.telegram.org/bot$BOT_TOKEN/getMyCommands" # default (en)
curl -s "https://api.telegram.org/bot$BOT_TOKEN/getMyCommands?language_code=ms" # default (ms)
# 4. Static-asset headers read-back: the deployed origin must serve the shipped policy.
curl -sSI "https://<MINI_APP_URL>/"
curl -sSI "https://<MINI_APP_URL>/assets/<a freshly-built hashed asset>.js"
```
Canary rules learned the hard way:
- **`getWebhookInfo.last_error` staleness:** `last_error` cannot clear until the next real update
arrives — record `last_error_date` and compare it against the deploy timeline before judging a
fresh deploy broken.
- **Read back per scope:** `setMyCommands` `ok:true` says nothing about which scope received the
commands — only `getMyCommands` per scope does.
- **Header probe honesty:** read the HTTP status before interpreting headers — a stale-deploy 404
is deployment drift (fix: redeploy), not a cache-policy violation.
- **Client caching:** Telegram clients cache the command menu — a device that opened the bot
before a fix may show a stale menu until the app restarts. Typing the command directly has
always worked.
## Rollback procedure
No CI/CD pipeline exists, so rollback is manual:
1. Redeploy the previous known-good commit: `git checkout <last-good-sha> && npm run deploy`.
2. Cloudflare retains recent deployment versions per Worker — a point-in-time rollback is also
available from the Cloudflare dashboard (Workers & Pages → araharah-bot → Deployments).
3. If only the Telegram surface is wrong (webhook/menu/commands), re-run the registration scripts
and the BotFather steps — they are idempotent and independent of the Worker build.
4. The road graph degrades honestly: an absent/corrupt R2 object yields router-pending `200 null`
everywhere, so a bad graph upload is recoverable by re-uploading — no Worker redeploy needed.
5. Cron cards are stateless and the envelopes TTL out at 7200 s — a bad guidance deploy ages out
of existence within ~2 hours.
<!-- VERIFY: the Cloudflare dashboard's per-Worker deployment-version retention window is platform state, not a repo fact. -->
## Monitoring
No third-party monitoring is installed — no Sentry, Datadog, New Relic, or OpenTelemetry in any
`package.json` (verified). Observability is:
- **`npx wrangler tail`** — live Worker logs. The logging law makes tail output safe: every line is
a category (`[webhook] rejected: misconfigured`, `[route] rejected: unauthorized`,
`[guidance-cron] send failed`, …) — never payloads, ids, coordinates, or secrets.
- **Workers Logs (free tier)** — 200,000 events/day, 3-day retention (documented in
`docs/BUDGETS.md`).
- **The audits as health checks:** `npm run privacy-audit` and `npm run budget-audit` (add
`-- --live` for the deployed-origin probes) re-derive the privacy and budget gates from source
and live responses.
- **Cron smoke test (local):** under `npm run dev -w bot` (`wrangler dev`), fire the scheduled arm
directly: `curl "http://localhost:8787/cdn-cgi/local/scheduled"`.
- **Budget ceiling watch:** the cron is stateless and paginated specifically so the KV list op
count (720/day at `*/2`) stays under the 1,000/day free ceiling — `docs/BUDGETS.md` holds the
measured record.
<!-- VERIFY: Workers Logs limits and the Cloudflare dashboard URLs are platform-documentation values; see developers.cloudflare.com and docs/BUDGETS.md's sourced ceiling table. -->