Files
arah2/docs/CONFIGURATION.md
T
2026-10-23 19:18:43 +08:00

14 KiB

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:

# 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.


Configuration flow

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)

{
  // 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.

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.