forked from c-fu/arah2
- 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
366 lines
16 KiB
Markdown
366 lines
16 KiB
Markdown
<!-- generated-by: gsd-doc-writer -->
|
|
|
|
# Getting Started
|
|
|
|
This guide takes you from an empty machine to a locally running arah² — the Telegram
|
|
bot (**arah2bot**), its Cloudflare Worker, and the Mini App webview — in five steps.
|
|
|
|
For what each piece does, see [ARCHITECTURE.md](ARCHITECTURE.md). For the full list of
|
|
environment variables, config files, and per-package scripts, see
|
|
[CONFIGURATION.md](CONFIGURATION.md). For a package overview, see the root
|
|
[README.md](../README.md).
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
| Requirement | Why | Notes |
|
|
| --- | --- | --- |
|
|
| **Node.js + npm** | Every build, dev, test, and deploy script in the repo is an npm script; all three workspaces are ESM (`"type": "module"`) | No `engines` field, `.nvmrc`, or `.node-version` exists anywhere in the repo. Use a current Node LTS. |
|
|
| A **Telegram account** | You chat with arah2bot and receive the Mini App webview | — |
|
|
| A bot token from **[@BotFather](https://t.me/BotFather)** | `BOT_TOKEN` is required for the Worker and every registration script | Also used as the HMAC key that validates Mini App `initData` |
|
|
| **Internet access** | `wrangler dev`, the Telegram Bot API, and OpenFreeMap tile fetches all need the network | — |
|
|
| **A Cloudflare account** *(deploy and `npm run bot:commands` only)* | `wrangler deploy` and the Workers KV namespace live there | Wrangler prompts for browser-based auth on first use |
|
|
|
|
<!-- VERIFY: minimum Node.js version — no `engines` field, `.nvmrc`, or `.node-version` file exists in the repository, so no supported floor is recorded. Confirm the intended version with the maintainers. -->
|
|
|
|
Only required for specific commands, not for a normal first run:
|
|
|
|
- **Python 3** — `npm run gallery` serves the design gallery via `python3 -m http.server 8123`
|
|
(`theme/package.json`, script `basemap:preview`).
|
|
- **A Playwright-managed Chromium** — `npm run capture` and `npm run web:capture` both call
|
|
bare `chromium.launch()` (`theme/scripts/screenshot.mjs:103`, `web/scripts/capture.mjs:163`), so
|
|
they need `npx playwright install chromium` first. No system Chrome is used.
|
|
|
|
There is no database to install, no Docker step, and no `.env` file to author — local secrets
|
|
live only in `bot/.dev.vars` (created in step 3). See
|
|
[CONFIGURATION.md](CONFIGURATION.md#environment-variables) for what each variable does.
|
|
|
|
---
|
|
|
|
## Installation
|
|
|
|
1. **Clone the repository.**
|
|
|
|
```bash
|
|
git clone http://gitmirah:3000/c-fu/arah2.git
|
|
```
|
|
|
|
2. **Enter the project directory.** The clone is named `arah2` after the repo slug; the
|
|
workspace root is what `package.json` lives in.
|
|
|
|
```bash
|
|
cd arah2
|
|
```
|
|
|
|
3. **Install all three workspaces from the root.** npm workspaces hoist dependencies into a
|
|
single root `node_modules`, so one install covers `theme`, `bot`, and `web` — including
|
|
`@araharah/theme`, which `web` consumes by relative path and never needs to be published.
|
|
|
|
```bash
|
|
npm install
|
|
```
|
|
|
|
---
|
|
|
|
## First run
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
CLONE["git clone"] --> INSTALL["npm install"]
|
|
INSTALL --> VARS["cp bot/.dev.vars.example bot/.dev.vars"]
|
|
VARS --> FILL["fill BOT_TOKEN, WEBHOOK_SECRET, WEBHOOK_URL"]
|
|
FILL --> BUILD["npm run build"]
|
|
BUILD --> DEV["npm run dev — wrangler dev on :8787"]
|
|
DEV --> HOOK["point the webhook at the tunnel"]
|
|
HOOK --> TALK["message arah2bot in Telegram"]
|
|
|
|
style CLONE fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
|
style INSTALL fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
|
style VARS fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
|
style FILL fill:#EB8008,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
|
style BUILD fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
|
|
style DEV fill:#1A1A1A,stroke:#EB8008,stroke-width:2px,color:#F7F0E3
|
|
style HOOK fill:#1A1A1A,stroke:#EB8008,stroke-width:2px,color:#F7F0E3
|
|
style TALK fill:#736B1E,stroke:#1A1A1A,stroke-width:2px,color:#F7F0E3
|
|
```
|
|
|
|
### 1. Create your local secrets file
|
|
|
|
`bot/.dev.vars` is gitignored (`bot/.gitignore`) and must never be committed. Copy the template:
|
|
|
|
```bash
|
|
cp bot/.dev.vars.example bot/.dev.vars
|
|
```
|
|
|
|
Then fill in all three variables:
|
|
|
|
```bash
|
|
# Copy to .dev.vars and fill for local wrangler dev (D-15). 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=
|
|
```
|
|
|
|
- **`BOT_TOKEN`** — from [@BotFather](https://t.me/BotFather). Required.
|
|
- **`WEBHOOK_SECRET`** — any high-entropy random string you choose. Telegram echoes it in the
|
|
`X-Telegram-Bot-Api-Secret-Token` header, and the Worker constant-time-compares it before
|
|
touching any update.
|
|
- **`WEBHOOK_URL`** — the public origin Telegram will POST updates to. Locally this is a
|
|
tunnel (see step 5); `set-webhook.mjs` appends `/telegram/webhook` automatically if you give it
|
|
a bare origin.
|
|
|
|
Every script in `bot/scripts/` resolves each variable as `process.env.<NAME> ?? devVars.<NAME>`,
|
|
so your shell environment wins over the file and the file is the fallback.
|
|
|
|
### 2. Build
|
|
|
|
```bash
|
|
npm run build
|
|
```
|
|
|
|
This runs `build -w theme` then `build -w web`. **The order matters:** `theme/dist/rams.css` is an
|
|
input to the web build, so the theme must compile first. Neither `theme/dist` nor `web/dist` is
|
|
committed (root `.gitignore` excludes `dist/`), so this step is required on every fresh clone and
|
|
before every `dev`/`deploy`.
|
|
|
|
### 3. Run the Worker locally
|
|
|
|
```bash
|
|
npm run dev
|
|
```
|
|
|
|
Root `dev` is `npm run build && npm run dev -w bot`, which is `wrangler dev --port 8787 --local`.
|
|
Wrangler reads `bot/.dev.vars`, emulates the `chat_sessions` KV namespace under `bot/.wrangler/`,
|
|
and serves three things on **one origin, port `8787`**:
|
|
|
|
| Path | Served by |
|
|
| --- | --- |
|
|
| `/telegram/webhook` | Worker `fetch` handler (`run_worker_first`) |
|
|
| `/api/*` | Worker `fetch` handler (`run_worker_first`) |
|
|
| everything else | `web/dist` static assets via the `ASSETS` binding |
|
|
|
|
Because it is the single origin, you can smoke-test the API without Telegram at all:
|
|
|
|
```bash
|
|
# Unknown paths are 404 — the Worker's fetch handler routes exactly four paths.
|
|
curl -i http://localhost:8787/api/location
|
|
# → 401 (no bearer token)
|
|
|
|
# An unauthenticated webhook POST fails closed when WEBHOOK_SECRET is unset,
|
|
# otherwise it 401s on the secret-token mismatch.
|
|
curl -i -X POST http://localhost:8787/telegram/webhook
|
|
```
|
|
|
|
### 4. Expose the webhook
|
|
|
|
`wrangler dev` binds to localhost only, so Telegram cannot reach it. Expose port `8787` with a
|
|
tunnel, then run the one-time registration script, which reads the same `bot/.dev.vars`:
|
|
|
|
```bash
|
|
# Terminal 1 — the dev server (from step 3)
|
|
npm run dev
|
|
|
|
# Terminal 2 — a tunnel that publishes port 8787 at a public HTTPS origin
|
|
cloudflared tunnel --url http://localhost:8787
|
|
```
|
|
|
|
Take the `https://…trycloudflare.com` origin it prints, put it in `WEBHOOK_URL` in
|
|
`bot/.dev.vars`, and register it:
|
|
|
|
```bash
|
|
npm run deploy --workspace bot # or, without the deploy half: see below
|
|
```
|
|
|
|
`bot`'s own `deploy` script is `wrangler deploy && node scripts/set-webhook.mjs`, which is the
|
|
wrong tool for local work. For a **local** webhook registration, run the script directly — it only
|
|
calls Telegram's `setWebhook`; it does not deploy anything:
|
|
|
|
```bash
|
|
node bot/scripts/set-webhook.mjs
|
|
```
|
|
|
|
Expected output on success:
|
|
|
|
```text
|
|
setWebhook ok:true — updates will be delivered to the configured URL with the secret-token header.
|
|
```
|
|
|
|
If `WEBHOOK_URL` has no path, the script prints a normalization line first:
|
|
|
|
```text
|
|
WEBHOOK_URL had no path — normalized to .../telegram/webhook
|
|
```
|
|
|
|
<!-- VERIFY: the recommended tunnel tooling is not pinned by the repository — no `cloudflared`, `ngrok`, or `localtunnel` dependency or config exists. Replace with whatever tunnel your team standardizes on, and record the choice here. -->
|
|
|
|
Register the persistent command menu (English + Bahasa Melayu) once, so `/help` and `/privacy`
|
|
appear in the Telegram UI:
|
|
|
|
```bash
|
|
npm run bot:commands
|
|
```
|
|
|
|
`register-commands.mjs` registers exactly `["help", "privacy"]` in two scopes — the default scope
|
|
plus `language_code: "ms"` — and deliberately reserves no stub entries for future commands, so
|
|
`/start` is a working handler that does **not** appear in the menu (type it manually). The
|
|
descriptions are derived from the `helpMenu` lines in `bot/src/i18n/strings.ts`, the same text
|
|
`/help` replies with, so the menu and the reply cannot drift.
|
|
|
|
### 5. Talk to arah2bot
|
|
|
|
Open Telegram, find **arah2bot**, and send `/start`. The Worker receives the update at
|
|
`/telegram/webhook`, verifies the secret-token header, and replies. Supported commands:
|
|
|
|
| Command | Alias | Behaviour |
|
|
| --- | --- | --- |
|
|
| `/start` | `/mula` | One-sentence introduction to arah² and its privacy posture |
|
|
| `/help` | — | Lists the commands available in this phase |
|
|
| `/privacy` | `/privasi` | States exactly what is stored (~15 min) and what is never stored |
|
|
|
|
The Mini App opens from the bot's menu button. The script that repoints it at the deployed origin
|
|
(`type: web_app`) has **no npm alias** — run it directly:
|
|
|
|
```bash
|
|
node bot/scripts/set-menu-button.mjs
|
|
```
|
|
|
|
### Working on the Mini App alone
|
|
|
|
For map and UI work that doesn't need the bot, run Vite instead of the Worker. It serves on port
|
|
**`5173`** with `strictPort: true`, so a port clash fails loudly rather than drifting:
|
|
|
|
```bash
|
|
npm run web:dev
|
|
```
|
|
|
|
There is no Worker behind it, so `web/src/main.ts` falls back to the mock Telegram WebApp via
|
|
`import.meta.env.DEV` and the `/api/*` calls stay stubbed. This is the fastest map/UI loop; run
|
|
`npm run dev` when you need the real session handshake.
|
|
|
|
---
|
|
|
|
## Verify the setup
|
|
|
|
Run both suites and both typechecks — the runners are hand-rolled and zero-dependency
|
|
(`node bot/test/runner.mjs` and `node web/test/runner.mjs`), so no test framework needs installing:
|
|
|
|
```bash
|
|
npm run bot:test # 6 suites in bot/test/*.test.mjs
|
|
npm run web:test # 6 suites in web/test/*.test.mjs
|
|
npm run bot:typecheck # tsc --noEmit
|
|
npm run web:typecheck # tsc --noEmit
|
|
```
|
|
|
|
Each runner prints `PASS <name>` / `FAIL <name>` per case and exits `0` only when every case
|
|
passed. If a suite fails to load, it is recorded as one failing case rather than crashing the run.
|
|
|
|
The bot suites are `commands`, `initdata`, `location-pin`, `privacy`, `sessions`, and `webhook`;
|
|
the web suites are `build-artifact`, `copy-gate`, `location-chain`, `no-storage`, `session`, and
|
|
`theme`. Two are worth knowing about as you get started:
|
|
|
|
- **`no-storage`** pins that nothing in `web/src` uses `localStorage`, `sessionStorage`, cookies,
|
|
IndexedDB, or the Cache API.
|
|
- **`privacy`** greps that every `chat_sessions` write carries an explicit `expirationTtl`.
|
|
|
|
Optionally, prove the design tokens carry no off-token values:
|
|
|
|
```bash
|
|
npm run audit
|
|
```
|
|
|
|
`theme/scripts/audit.mjs` scans the rendered CSS and exits `1` on any colour, spacing,
|
|
typography, radius, shadow, or blur value not derived from `theme/src/tokens.css`; it prints
|
|
`AUDIT CLEAN — …` and exits `0` when the sheet is pure.
|
|
|
|
---
|
|
|
|
## Common setup issues
|
|
|
|
**`npm run dev` starts but every request returns the Mini App, or nothing.**
|
|
`web/dist` may be stale or absent. `run_worker_first` covers only `/api/*` and `/telegram/*`
|
|
(a glob **array**, not a boolean), so everything else falls through to the assets binding. Run
|
|
`npm run build` first, or just use `npm run dev` — it already builds.
|
|
|
|
**`[webhook] rejected: misconfigured` → `500 Server misconfigured`.**
|
|
`WEBHOOK_SECRET` is missing or empty in `bot/.dev.vars`. The guard is at `bot/src/webhook.ts:65`
|
|
and it fails closed deliberately: grammY's `compareSecretToken` silently accepts *every* request
|
|
when the secret is `undefined`, so an unarmed secret must never reach it. Generate a new random
|
|
string and put it in `WEBHOOK_SECRET`.
|
|
|
|
**`setWebhook ok:true` but arah2bot never responds.**
|
|
Telegram accepted the URL, so it answered `ok` while delivering updates to a path that `404`s.
|
|
This happens when `WEBHOOK_URL` is a bare workers.dev origin. `set-webhook.mjs` now appends
|
|
`/telegram/webhook` for you — if you registered the webhook by hand or via a different tool,
|
|
re-run `node bot/scripts/set-webhook.mjs` and confirm the registered URL includes the path.
|
|
|
|
**`Missing BOT_TOKEN, WEBHOOK_URL, WEBHOOK_SECRET` and exit code `1`.**
|
|
`set-webhook.mjs` checks all three before any network call and never echoes values — only names.
|
|
Confirm you ran it from a path where `bot/.dev.vars` resolves (`bot/.dev.vars` is read relative to
|
|
the script, so it works from the repo root too), and that you did not commit a truncated file.
|
|
|
|
**Nothing happens when I send `/start`.**
|
|
Almost always the tunnel. `wrangler dev --local` binds to localhost, so Telegram has no route in.
|
|
Re-run the tunnel, update `WEBHOOK_URL`, re-run `node bot/scripts/set-webhook.mjs`, and confirm the
|
|
`tunnel origin` in `.dev.vars` still matches the one currently printing in Terminal 2 — tunnels
|
|
rotate their hostname on every restart.
|
|
|
|
**`chromium.launch()` fails — `npm run capture` or `npm run web:capture` errors on a missing browser.**
|
|
Both scripts use bare `chromium.launch()` with no `executablePath` and no `channel`, so they need
|
|
Playwright's own browser build:
|
|
|
|
```bash
|
|
npx playwright install chromium
|
|
```
|
|
|
|
`npm run web:capture` additionally requires the harness build to exist — it serves
|
|
`web/dist-harness/`, not `web/dist`, and waits for `RAMS-BASEMAP-READY` then `ARAHARAH-LIVE`
|
|
signals. Run `npm run web:build:harness` first. Its 90-second denial timeout is deliberate: the
|
|
chat-pin poll must elapse before the denied card appears.
|
|
|
|
**Mini App boots but shows no map.**
|
|
Check the network tab for tile requests to `https://tiles.openfreemap.org/…` — the generated
|
|
`theme/basemap/liberty-rams.json` points there, so a blocked or offline request leaves a blank
|
|
canvas. `npm run gallery` serves the basemap locally on `:8123` if you want to inspect the style
|
|
in isolation.
|
|
|
|
**`theme/dist/rams.css` is missing when `web` builds.**
|
|
Run `npm run build` from the root rather than `npm run build -w web`. The root `build` script
|
|
chains theme then web; the web build imports the compiled stylesheet, not the token source.
|
|
|
|
---
|
|
|
|
## Next steps
|
|
|
|
- **[ARCHITECTURE.md](ARCHITECTURE.md)** — how the Worker, KV namespace, Mini App, and design
|
|
system fit together; the Worker routing table; the three-link location acquisition chain.
|
|
- **[CONFIGURATION.md](CONFIGURATION.md)** — every environment variable, `bot/wrangler.jsonc`
|
|
and `web/vite.config.ts`, and the full command reference per workspace.
|
|
- **[../README.md](../README.md)** — package overview, HTTP surface, design-system usage.
|
|
- **[../DESIGN.md](../DESIGN.md)** — the Dieter Rams philosophy behind the token vocabulary.
|
|
|
|
### Everyday commands
|
|
|
|
| Command | What it does |
|
|
| --- | --- |
|
|
| `npm run dev` | Build, then `wrangler dev --port 8787 --local` |
|
|
| `npm run build` | Build theme → Mini App (`theme/dist/rams.css` must exist first) |
|
|
| `npm run web:dev` | Vite dev server on `5173` with a mock WebApp and no Worker |
|
|
| `npm run bot:test` / `npm run web:test` | Zero-dependency test runners |
|
|
| `npm run bot:typecheck` / `npm run web:typecheck` | `tsc --noEmit` |
|
|
| `npm run audit` | Off-token audit of the rendered CSS; non-zero on violations |
|
|
| `npm run bot:commands` | Register the persistent Telegram command menu (EN + MS) |
|
|
| `npm run deploy` | Build, then `wrangler deploy` + `set-webhook.mjs` |
|
|
|
|
The two build outputs must **never** share an `outDir`: `web:build:harness` emits to
|
|
`web/dist-harness/` with the mock installer and `/api/*` fetch stubs that production must not
|
|
carry. `vite.config.ts` calls this out as the reason the split exists.
|
|
|
|
### Deploying
|
|
|
|
`npm run deploy` publishes the Worker to Cloudflare and registers the webhook. It reads no local
|
|
file for secrets — production values live as Cloudflare Workers secrets. See
|
|
[CONFIGURATION.md](CONFIGURATION.md#per-environment-overrides).
|
|
|
|
<!-- VERIFY: which environment variables are set as Cloudflare Workers secrets via `wrangler secret put`, and under which Cloudflare account, cannot be determined from the repository. --> |