This commit is contained in:
araharah-executor
2026-10-23 19:18:43 +08:00
parent 1890b5b1ca
commit af3edefbc6
9 changed files with 3027 additions and 2 deletions
+259
View File
@@ -0,0 +1,259 @@
<!-- generated-by: gsd-doc-writer -->
# Configuration
arah² is an npm-workspaces monorepo with three workspaces — `theme`, `bot`, `web` — plus a root
`package.json` that composes their scripts. There is **no committed `.env` file**: secrets live in
`bot/.dev.vars` (gitignored) for local work and in Cloudflare Workers secrets for deployment.
Configuration surfaces, by kind:
| Surface | File | Applies to |
| --- | --- | --- |
| Workspaces + scripts | `package.json` | all |
| Worker deployment + bindings | `bot/wrangler.jsonc` | bot |
| Secret env contract | `bot/.dev.vars` (from `.dev.vars.example`) | bot |
| Worker env typing | `bot/src/config.ts` | bot |
| Local secrets template | `bot/.dev.vars.example` | bot |
| Bundler config | `web/vite.config.ts` | web |
| TypeScript config | `bot/tsconfig.json`, `web/tsconfig.json` | bot, web |
| Design tokens (source of truth) | `theme/src/tokens.css` → `theme/src/theme.css` | theme + everything downstream |
| Ignore rules | `.gitignore`, `bot/.gitignore` | repo |
---
## Environment variables
Three variables reach the Worker (`bot/src/config.ts` declares them as a typed `Env` interface);
a fourth (`WEBHOOK_URL`) is consumed only by the one-time registration scripts, never by the Worker.
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `BOT_TOKEN` | **Required** | none | Telegram Bot API token from @BotFather. Used by `createBot()` in `bot/src/bot/bot.ts` to construct the grammY `Bot`, and by `validateInitData()` in `bot/src/security/initdata.ts` as the HMAC key for Mini App `initData` verification. |
| `WEBHOOK_SECRET` | **Required** | none | Value Telegram echoes in the `X-Telegram-Bot-Api-Secret-Token` header on every update. `/telegram/webhook` constant-time-compares it **before** any bot processing or KV access. If missing or empty at request time the route fails closed with `500 Server misconfigured` — grammY's secret check would otherwise accept every request. |
| `WEBHOOK_URL` | **Required for the one-time scripts** | none | Full origin + path of the deployed Worker, used by `bot/scripts/set-webhook.mjs` and `bot/scripts/set-menu-button.mjs`. If the value points at the workers.dev root (no path), `set-webhook.mjs` appends `/telegram/webhook` automatically. The Worker never reads this variable. |
| `chat_sessions` | **Required (binding)** | n/a | Not an env var but a Cloudflare KV namespace binding. Wired in `bot/wrangler.jsonc` under `kv_namespaces`; every `kv.put()` in `bot/src/kv/sessions.ts` and `bot/src/kv/location-pin.ts` writes through it with an explicit `expirationTtl`. |
Local secret template — `bot/.dev.vars.example`:
```bash
# Copy to .dev.vars and fill for local wrangler dev. NEVER commit .dev.vars.
BOT_TOKEN=
WEBHOOK_SECRET=
# Full origin + path; set-webhook.mjs auto-appends /telegram/webhook if only an origin is set.
WEBHOOK_URL=
```
Script-level precedence: every script in `bot/scripts/` resolves a variable as
`process.env.<NAME> ?? devVars.<NAME>` — shell environment wins over the file, and the file is the
fallback. `set-webhook.mjs` requires all three of `BOT_TOKEN`, `WEBHOOK_URL`, `WEBHOOK_SECRET` and
exits `1` listing whichever are missing. `register-commands.mjs` requires only `BOT_TOKEN`.
`set-menu-button.mjs` requires `BOT_TOKEN` and `WEBHOOK_URL`.
<!-- VERIFY: the production secret names registered via `wrangler secret put` and the Cloudflare account they live under cannot be read from the repository. -->
---
## Configuration flow
```mermaid
flowchart TD
subgraph Local["Local development"]
DEVVARS["bot/.dev.vars (gitignored)"]
WRANGLERDEV["wrangler dev --port 8787 --local"]
end
subgraph Scripts["One-time Telegram registration"]
SC1["scripts/set-webhook.mjs"]
SC2["scripts/register-commands.mjs"]
SC3["scripts/set-menu-button.mjs"]
end
subgraph Production["Deployed Worker"]
SECRETS["wrangler secrets: BOT_TOKEN, WEBHOOK_SECRET"]
KV["KV namespace binding: chat_sessions"]
ASSETS["Static assets: ../web/dist"]
WORKER["src/webhook.ts fetch handler"]
end
DEVVARS --> WRANGLERDEV
DEVVARS --> SC1
DEVVARS --> SC2
DEVVARS --> SC3
SECRETS --> WORKER
KV --> WORKER
ASSETS --> WORKER
WRANGLERDEV --> WORKER
style Local fill:#D9D2C6,stroke:#1A1A1A,color:#1A1A1A
style Scripts fill:#D9D2C6,stroke:#1A1A1A,color:#1A1A1A
style Production fill:#D9D2C6,stroke:#1A1A1A,color:#1A1A1A
style DEVVARS fill:#EB8008,stroke:#1A1A1A,color:#1A1A1A
style SECRETS fill:#ED3F1C,stroke:#1A1A1A,color:#FFFFFF
```
The Mini App in `web/` has **no build-time or runtime configuration surface**. It calls its own
origin with relative paths (`/api/session`, `/api/location`, `/api/location-fallback`), so the API
base URL is implied by wherever the bundle is served from — which is why the Worker serves
`../web/dist` as its own static assets and reserves `/api/*` and `/telegram/*` for itself.
---
## Required vs optional settings
**Fail loudly (Worker):**
- `WEBHOOK_SECRET` — the only setting with an explicit runtime guard, at `bot/src/webhook.ts:65`:
`if (!env.WEBHOOK_SECRET)` logs `[webhook] rejected: misconfigured` and returns
`500 Server misconfigured`. The rationale is documented in the file: grammY's `compareSecretToken`
silently accepts every request when the secret is `undefined`, so an unarmed binding must never
reach it.
- `BOT_TOKEN` — declared required in the `Env` type but not guarded at runtime; a missing token
surfaces as a grammY authentication failure on the first request that instantiates the bot.
**Fail loudly (scripts):** `set-webhook.mjs`, `register-commands.mjs`, and `set-menu-button.mjs` each
print a missing-variable list to stderr and `process.exit(1)` before any network call.
**With defaults / derived:**
- `KV namespace id` — `bot/wrangler.jsonc` already carries a concrete id for `chat_sessions`; the
file's leading comment notes it was filled in after `npx wrangler kv namespace create chat_sessions`.
- `WEBHOOK_URL` path segment — auto-derived by `set-webhook.mjs` when the configured origin has no path.
- KV TTLs — constants in source, not configuration: `SESSION_TTL_SECONDS = 900`
(`bot/src/kv/sessions.ts`) and `PIN_TTL_SECONDS = 300` (`bot/src/kv/location-pin.ts`). Both are
pinned by a grep gate in the test suite so no write path can persist without a TTL.
---
## Worker configuration file (`bot/wrangler.jsonc`)
```jsonc
{
// KV namespace id is filled in plan 03 task 1 after `npx wrangler kv namespace create chat_sessions`
"name": "araharah-bot",
"main": "src/webhook.ts",
"compatibility_date": "2026-10-22",
"workers_dev": true,
"assets": {
"directory": "../web/dist",
"binding": "ASSETS",
"not_found_handling": "none"
},
"run_worker_first": ["/api/*", "/telegram/*"],
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
}
```
Key notes:
- `assets.directory: "../web/dist"` — the Worker serves the Mini App bundle itself, so `npm run build`
(theme then web) must run before `wrangler deploy`; a stale or absent `web/dist` ships no Mini App.
- `run_worker_first` is a **glob array**, not a boolean: `/api/*` and `/telegram/*` are handled by
`src/webhook.ts` first; everything else falls through to the static asset binding.
- `not_found_handling: "none"` — unmatched paths are not served the SPA `index.html` fallback.
- `workers_dev: true` serves on the `<name>.<account>.workers.dev` URL, which is the `WEBHOOK_URL`
target.
<!-- VERIFY: the Cloudflare account subdomain backing `araharah-bot.<account>.workers.dev` is not recorded in the repository. -->
---
## Build and run scripts
Root `package.json` (workspaces: `theme`, `bot`, `web`; all packages are `"private": true`, ESM via
`"type": "module"`):
| Command | Description |
| --- | --- |
| `npm run build` | Builds the theme then the web bundle (`theme` → `web`). |
| `npm run deploy` | `build`, then `npm run deploy -w bot` (`wrangler deploy` + `set-webhook.mjs`). |
| `npm run dev` | `build`, then `npm run dev -w bot` (`wrangler dev --port 8787 --local`). |
| `npm run bot:test` / `bot:typecheck` / `bot:dev` / `bot:deploy` / `bot:commands` | Per-workspace passthroughs to the `bot` package. |
| `npm run web:test` / `web:typecheck` / `web:dev` | Passthroughs to the `web` package. |
| `npm run web:build` | Same as `build`; `web:build:harness` runs the harness-mode build; `web:capture` runs the screenshot harness. |
| `npm run sheet` / `gallery` / `audit` / `capture` | Passthroughs to the `theme` design-system tooling. |
Workspace-level commands worth knowing:
| Command | Where | Notes |
| --- | --- | --- |
| `npm test` | `bot`, `web` | Both run `node test/runner.mjs` (a hand-rolled runner, no external test framework). |
| `npm run typecheck` | `bot`, `web` | `tsc --noEmit`, `strict` plus `noUncheckedIndexedAccess`. |
| `npm run dev` | `bot` | `wrangler dev --port 8787 --local` — reads `bot/.dev.vars`. |
| `npm run deploy` | `bot` | `wrangler deploy && node scripts/set-webhook.mjs`. |
| `npm run commands` | `bot` | `node scripts/register-commands.mjs` — `setMyCommands` in default scope and in `language_code: "ms"`; descriptions are derived from `src/i18n/strings.ts` `helpMenu` lines so the menu and `/help` reply cannot drift. |
| `npm run build` | `web` | `vite build` → `web/dist`. |
| `npm run build:harness` | `web` | `vite build --mode harness` → `web/dist-harness`, with mock installer and fetch stubs. |
| `npm run dev` | `web` | `vite` dev server on port 5173 (`strictPort: true`). |
| `npm run capture` | `web` | `node scripts/capture.mjs` — serves `web/dist-harness` and screenshots the boot/live/denied states. |
| `npm run build` | `theme` | `tailwindcss -i ./src/theme.css -o ./dist/rams.css --minify`. |
| `npm run audit` | `theme` | `node ./scripts/audit.mjs ./dist/rams.css` — flags any rendered color/spacing/typography/radius/shadow/blur value not derived from `src/tokens.css`. Exits `0` clean, `1` violations, `2` drift. |
| `npm run basemap` | `theme` | `node basemap/build.mjs` — forks OpenFreeMap `liberty` into `theme/basemap/liberty-rams.json`, deriving every cartographic value from the token file. |
| `npm run gallery` / `sheet` / `capture` | `theme` | Design tooling; `gallery` serves on `http://localhost:8123` via `python3 -m http.server 8123`. |
---
## Bundler configuration (`web/vite.config.ts`)
Vite is configured from a function of `mode`, with one switch driving a **dual-build contract**:
- `__ARAHARAH_HARNESS__` is injected via `define` as `true` only in `--mode harness` and `false`
everywhere else. Vite replaces the identifier textually, so the production bundle tree-shakes the
mock installer and `/api/*` fetch stubs out entirely; `web/index.html` carries its mock profile
block inside the same guarded branch.
- `build.outDir` is `dist-harness` in harness mode, `dist` otherwise, with `emptyOutDir: true` and
`target: "es2022"`. The two outputs must never share a directory — the file's own comment calls
that out as the reason the split exists.
- `base: "/"`, dev server on port 5173 with `strictPort: true`.
- `__ARAHARAH_HARNESS__` is declared ambient in `web/src/vite-env.d.ts` so `tsc --noEmit` passes in
both modes.
- Harness-only URL parameters (documented in `web/index.html`): `?mock=android|ios|tdesktop|macos|web`,
`?scheme=light|dark`, `?pin=1`, `?locale=en|ms`, `?sessiondelay=<ms>`, `?fixdelay=<ms>`.
---
## Design token configuration
`theme/src/tokens.css` is the single source of truth for every color, spacing, typography, and radius
value. `theme/src/theme.css` imports Tailwind v4 plus `tokens.css` and `components.css`, declares
self-hosted Geist and Space Grotesk `@font-face` rules from `theme/fonts/` (no Google Fonts CDN), and
scopes class generation with explicit `@source` lines — the four mockup markup files under
`stitch_minimalist_route_navigator/` are the only allowed utility sources, with
`@source not "../scripts"` and `@source not "../basemap"` excluding prose.
Downstream consumers:
- `web/src/shell.css` — `@import "@araharah/theme/dist/rams.css";`
- `web/src/map/map.ts` — `import ramsStyle from "@araharah/theme/basemap/liberty-rams.json";`
- `theme/basemap/liberty-rams.json` — the generated basemap style; its source URL, sprite, and glyph
endpoints point at `https://tiles.openfreemap.org/...`.
---
## Ignore rules and generated output
`.gitignore` at the repo root excludes `.planning/`, `node_modules/`, `dist/` (which covers both
`theme/dist/` and `web/dist/`), `web/dist-harness/`, `theme/screenshots/`, `web/screenshots/`,
`.env`, `.env.*` (with `!*.example`), and `*:Zone.Identifier`. `bot/.gitignore` adds `.dev.vars` and
`.wrangler/`.
Practical consequence: **`web/dist` and `theme/dist` are never committed.** Both must be rebuilt from
source before a deploy, which is why `npm run deploy` and `npm run dev` both run `build` first.
---
## Per-environment overrides
There are no committed `.env.development`, `.env.production`, or `.env.test` files. The separation
is structural rather than file-based:
| Environment | Configuration source | How it differs |
| --- | --- | --- |
| Local `wrangler dev` | `bot/.dev.vars` | Secrets read from the gitignored file; KV emulated locally under `bot/.wrangler/`; Mini App served from `web/dist`. |
| Local `vite dev` | none | Vite dev server on 5173 with no Worker behind it — `main.ts` falls back to the mock WebApp via `import.meta.env.DEV`. |
| Harness capture | `vite build --mode harness` | `web/dist-harness` plus `web/index.html`'s URL-parameter profile. Never deployed. |
| Production | `wrangler.jsonc` + Cloudflare secrets | Secrets set with `wrangler secret put` rather than a file; assets come from the uploaded `../web/dist`; KV resolves to the real namespace. |
`@araharah/theme` and `@araharah/web` are consumed by relative path within the workspace, so there is
no registry token or publish step to configure.