406 lines
26 KiB
Markdown
406 lines
26 KiB
Markdown
<!-- generated-by: gsd-doc-writer -->
|
||
|
||
# Configuration
|
||
|
||
arah² is an npm-workspaces monorepo with five workspaces — `theme`, `bot`, `web`, `search`, `routing`
|
||
— 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 + the repo gate CLIs |
|
||
| Worker env typing | `bot/src/config.ts` | bot |
|
||
| Local secrets template | `bot/.dev.vars.example` | bot |
|
||
| Static-asset cache policy | `web/public/_headers` | web (served by the Worker's asset binding) |
|
||
| Bundler config | `web/vite.config.ts` | web |
|
||
| TypeScript config | `bot/tsconfig.json`, `web/tsconfig.json`, `search/tsconfig.json`, `routing/tsconfig.json` | bot, web, search, routing |
|
||
| Design tokens (source of truth) | `theme/src/tokens.css` → `theme/src/theme.css` | theme + everything downstream |
|
||
| Graph binary + gazetteer data | `routing/data/kl-roadgraph.v1.bin`, `search/src/data/kl-gazetteer.json` | routing, search |
|
||
| Ignore rules | `.gitignore`, `bot/.gitignore` | repo |
|
||
|
||
---
|
||
|
||
## Environment variables
|
||
|
||
Three variables reach the Worker (BOT_TOKEN, WEBHOOK_SECRET, MINI_APP_URL) plus two bindings; `WEBHOOK_URL` is script-only (`bot/src/config.ts` declares them as a typed `Env` interface,
|
||
plus two *bindings* — a KV namespace and an R2 bucket); `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. |
|
||
| `MINI_APP_URL` | **Required for the deep link** | none (concrete value committed) | Public origin of the deployed Mini App — NOT a secret, so it lives in `bot/wrangler.jsonc`'s `vars`, not in a secret. The `/cari` venue card's OPEN button is built from it: `<MINI_APP_URL>/?dest=<gazetteer id>`. It must equal the Mini App URL registered in @BotFather (Bot API 10.2 enforces the origin). The Worker never derives it from user input — an unvalidated origin would be an open redirect. |
|
||
| `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()` across `bot/src/kv/` (`sessions.ts`, `location-pin.ts`, `guidance-sessions.ts`) writes through it with an explicit `expirationTtl`. |
|
||
| `ROAD_GRAPH` | **Required (binding)** | n/a | Not an env var but a Cloudflare **R2 bucket** binding (`r2_buckets` in `bot/wrangler.jsonc`, bucket `araharah-graphs`). `/api/route` decodes the road-graph binary through it; `loadGraph` memoizes one decoded graph per isolate. An absent or corrupt object degrades honestly (null → router-pending), never a crash. The bucket name is public configuration, never a secret — `docs/ROAD-GRAPH.md` owns the fork contract. |
|
||
|
||
### Maintainer-only credentials (never reach the Worker)
|
||
|
||
Two more variables are read **only** by the `--live` probes of the repo gate CLIs
|
||
(`scripts/lib/cloudflare-probes.mjs`), never by any `src/` tree and never by the Worker:
|
||
|
||
| Variable | Required | Default | Description |
|
||
| --- | --- | --- | --- |
|
||
| `CLOUDFLARE_API_TOKEN` | Required for `--live` probes only | none | Wrangler credential used to run `wrangler kv key list` (KV-expiry probe) and the asset-header probe. Resolved as `process.env` first, then parsed from `bot/.dev.vars`. Never printed, never logged, never written anywhere. Not present in `.dev.vars.example` — add it by hand to `.dev.vars` or export it in the shell. |
|
||
| `CLOUDFLARE_ACCOUNT_ID` | Required for `--live` probes only | none | Cloudflare account id paired with the API token above. Same resolution order and same never-echo rule. |
|
||
|
||
The probes are invoked with `npm run privacy-audit -- --live` and `npm run budget-audit -- --live`.
|
||
Without the credentials the probes report `NOT RUN` and the CLI exits `2` — a skipped probe is
|
||
never mistaken for a pass. Without `--live`, the probes are listed as `NOT RUN (pass --live)` by
|
||
the operator's explicit choice, which is excluded from the exit-2 class.
|
||
|
||
The gate CLIs also honor one non-secret override:
|
||
|
||
| Variable | Required | Default | Description |
|
||
| --- | --- | --- | --- |
|
||
| `PRIVACY_AUDIT_ROOT` | Optional | repo root | Repo-root override for the gate CLIs' scanners (used by their test suites and by fork checkouts). The gates are otherwise cwd-independent — a stray subdirectory run must not scan the wrong tree and false-pass. |
|
||
|
||
<!-- VERIFY: the actual credential VALUES (BOT_TOKEN, WEBHOOK_SECRET, CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID) and the Cloudflare account they live under cannot be read from the repository — only their names and resolution order are documented here. -->
|
||
|
||
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=
|
||
# Public Mini App origin (not a secret) — lives in bot/wrangler.jsonc "vars" in production.
|
||
# The /cari venue card's OPEN button deep link is built from it: <MINI_APP_URL>/?dest=<id>.
|
||
MINI_APP_URL=
|
||
```
|
||
|
||
The template does NOT list `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` — those are read
|
||
directly from `.dev.vars` (or the shell) by the audit probes when present; they are optional and
|
||
only required for the `--live` mode. Values are never echoed anywhere in this documentation.
|
||
|
||
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
|
||
|
||
```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 binding: chat_sessions (TTL-only, 4 families)"]
|
||
R2["R2 binding: ROAD_GRAPH (araharah-graphs)"]
|
||
ASSETS["Static assets: ../web/dist + _headers"]
|
||
CRON["Cron trigger: */2 * * * * (stateless scan)"]
|
||
WORKER["src/webhook.ts fetch + scheduled handlers"]
|
||
end
|
||
|
||
DEVVARS --> WRANGLERDEV
|
||
DEVVARS --> SC1
|
||
DEVVARS --> SC2
|
||
DEVVARS --> SC3
|
||
|
||
SECRETS --> WORKER
|
||
KV --> WORKER
|
||
R2 --> WORKER
|
||
ASSETS --> WORKER
|
||
CRON --> 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. The
|
||
same is true of `search/` and `routing/`: neither reads any environment variable at runtime
|
||
(`loader-env.test.mjs` in both workspaces pins that the data loads with zero infrastructure —
|
||
static imports under Node, esbuild inlining under Vite, and an inlined seed under the wrangler
|
||
/esbuild Worker bundle).
|
||
|
||
The Worker has exactly one scheduled configuration surface: the `*/2 * * * *` cron trigger in
|
||
`wrangler.jsonc`. The tick is stateless — zero KV writes (the budget law) — and gates its card
|
||
sends on `CARD_CADENCE_S = 240` in code (`bot/src/guidance/cron.ts`): `*/2` is the coarsest
|
||
cadence that keeps 2-min ticks × 1 list-op = 720/day inside the free plan's 1,000/day KV-list
|
||
ceiling while honoring the 240 s card window.
|
||
|
||
---
|
||
|
||
## Required vs optional settings
|
||
|
||
**Fail loudly (Worker):**
|
||
|
||
- `WEBHOOK_SECRET` — the only setting with an explicit runtime guard, at `bot/src/webhook.ts`:
|
||
`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.
|
||
|
||
**Fail loudly (gate CLIs):** `privacy-audit` and `budget-audit` never pass silently — a required
|
||
measurement that cannot run (missing graph binary, missing build, missing `--live` credentials)
|
||
yields exit `2` NOT RUN, never a green check.
|
||
|
||
**Degrade honestly (binding):** a missing or corrupt `ROAD_GRAPH` object does not crash the
|
||
Worker — `/api/route` returns the honest router-pending state. This is deliberate: the graph
|
||
bucket is fork provisioning, not a startup invariant.
|
||
|
||
**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`.
|
||
- `MINI_APP_URL` — a **public** configuration value declared in the Worker `Env` and shipped via
|
||
`bot/wrangler.jsonc`'s `vars` block with its concrete value committed
|
||
(`https://araharah-bot.araharah-bot.workers.dev`). When it is blank the `/cari` OPEN button is
|
||
omitted rather than guessed; the venue card and its alternates still send.
|
||
- `WEBHOOK_URL` path segment — auto-derived by `set-webhook.mjs` when the configured origin has no path.
|
||
- KV TTLs — constants in source, not configuration. One per family, all pinned by grep gates in
|
||
the test suite so no write path can persist without a TTL:
|
||
|
||
| Constant | Value | File | Key family |
|
||
| --- | --- | --- | --- |
|
||
| `SESSION_TTL_SECONDS` | `900` | `bot/src/kv/sessions.ts` | `session:<token>`, `chat:<chat_id>` |
|
||
| `PIN_TTL_SECONDS` | `300` | `bot/src/kv/location-pin.ts` | `loc:<chat_id>` |
|
||
| `GUIDANCE_SESSION_TTL_SECONDS` | `7200` | `bot/src/kv/guidance-sessions.ts` | `guidance:<id>` |
|
||
|
||
The live KV probe asserts every listed key's family prefix is one of the four TTL-only families
|
||
(`session:`, `chat:`, `loc:`, `guidance:`) and carries an `expiration`.
|
||
- `CARD_CADENCE_S = 240` (`bot/src/guidance/cron.ts`) and the cron expression `*/2 * * * *`
|
||
(`wrangler.jsonc`) — a code/config pair: the cron ticks every 120 s, a card is due on the first
|
||
tick of each 240 s window.
|
||
|
||
---
|
||
|
||
## Worker configuration file (`bot/wrangler.jsonc`)
|
||
|
||
```jsonc
|
||
{
|
||
// chat_sessions namespace created via `npx wrangler kv namespace create chat_sessions` — the id below is live.
|
||
"name": "araharah-bot",
|
||
"main": "src/webhook.ts",
|
||
"compatibility_date": "2026-10-22",
|
||
// workers_dev: serve on the <name>.<account>.workers.dev URL — the WEBHOOK_URL
|
||
// target for this phase (a custom route/domain may supersede)
|
||
"workers_dev": true,
|
||
// Workers static assets binding: serves ../web/dist. The Worker fetch still
|
||
// owns /api/* and /telegram/* first via run_worker_first (a GLOB ARRAY, not a boolean).
|
||
"assets": {
|
||
"directory": "../web/dist",
|
||
"binding": "ASSETS",
|
||
"not_found_handling": "none"
|
||
},
|
||
"run_worker_first": ["/api/*", "/telegram/*"],
|
||
// Public vars (NOT secrets): the deployed Mini App ORIGIN used to build
|
||
// the /cari venue card's OPEN deep link (<MINI_APP_URL>/?dest=<id>).
|
||
"vars": { "MINI_APP_URL": "https://araharah-bot.araharah-bot.workers.dev" },
|
||
// R2 binding: hosts the road-graph binary /api/route loads (loadGraph memoizes
|
||
// ONE GET per cold isolate). Forks create + upload their OWN bucket:
|
||
// `npx wrangler r2 bucket create <name>` + routing/scripts/upload-road-graph.mjs
|
||
// (docs/ROAD-GRAPH.md is the fork contract).
|
||
"r2_buckets": [{ "binding": "ROAD_GRAPH", "bucket_name": "araharah-graphs" }],
|
||
// Cron trigger: */2 scan — 2-min ticks × 1 list-op = 720/day, inside the free
|
||
// plan's 1,000/day KV-list ceiling; card cadence is 240 s in code (CARD_CADENCE_S).
|
||
// The tick is STATELESS (zero KV writes — the budget law).
|
||
"triggers": { "crons": ["*/2 * * * *"] },
|
||
"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.
|
||
Vite copies `web/public/_headers` into the bundle, so the cache policy ships with it (see below).
|
||
- `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 and the committed `MINI_APP_URL` value.
|
||
- `vars: { "MINI_APP_URL": ... }` — a **public** origin shipped as a plain var (never a secret).
|
||
It must equal the Mini App URL registered in @BotFather; see the env-var table above.
|
||
- `r2_buckets` / `triggers` — the R2 graph bucket and the */2 cron are both free-plan-compatible;
|
||
the cron's budget arithmetic (720 KV list-ops/day against a 1,000/day ceiling) is measured and
|
||
printed by `npm run budget-audit` (see `docs/BUDGETS.md`).
|
||
|
||
### Static-asset cache policy (`web/public/_headers`)
|
||
|
||
Committed under `web/public/`, so Vite copies it into every build output (`web/dist`,
|
||
`web/dist-harness`) and the Workers asset binding serves it verbatim:
|
||
|
||
```text
|
||
/assets/*
|
||
Cache-Control: public, max-age=31556952, immutable
|
||
|
||
/
|
||
Cache-Control: public, max-age=0, must-revalidate
|
||
```
|
||
|
||
Rationale (from the file's header comment): `/assets/*` filenames are content-HASHED — a hashed
|
||
URL's bytes can never change, so they are immutable for a year. The HTML entry is NOT hashed (it
|
||
points at the current hashed assets), so it must revalidate on every visit or a stale entry would
|
||
point at deleted asset files. The `--live` asset-headers probe (`privacy-audit --live`, re-used by
|
||
`budget-audit --live`) **quotes the observed `cache-control` and `cf-cache-status` headers verbatim**
|
||
against the deployed origin — it reports, it does not assert the policy.
|
||
|
||
---
|
||
|
||
## Build and run scripts
|
||
|
||
Root `package.json` (workspaces: `theme`, `bot`, `web`, `search`, `routing`; the root and `bot`,
|
||
`web`, `search`, `routing` packages set `"private": true` while `@araharah/theme` omits the field,
|
||
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 search:test` / `search:typecheck` / `search:corpus` | `search` workpace passthroughs; `search:corpus` runs `node search/scripts/gazetteer-corpus.mjs` (golden corpus measurement). |
|
||
| `npm run routing:test` / `routing:typecheck` / `routing:corpus` / `routing:fixtures` | `routing` workspace passthroughs; `routing:corpus` runs `node routing/scripts/route-corpus.mjs`; `routing:fixtures` re-fetches route fixtures from the OSRM demo server (maintainer-time, fixed public coordinates only). |
|
||
| `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. |
|
||
| `npm run privacy-audit` | `node scripts/privacy-audit.mjs` — the five static privacy gates (+ `--live` probes). Exits `0` clean, `1` violations, `2` NOT RUN. |
|
||
| `npm run budget-audit` | `node scripts/budget-audit.mjs` — the five measured budget gates + the free-tier comparison table (+ `--live` cache-header probe). Same exit-code contract. |
|
||
|
||
Workspace-level commands worth knowing:
|
||
|
||
| Command | Where | Notes |
|
||
| --- | --- | --- |
|
||
| `npm test` | `bot`, `web`, `search`, `routing` | All run `node test/runner.mjs` (a hand-rolled runner, no external test framework). |
|
||
| `npm run typecheck` | `bot`, `web`, `search`, `routing` | `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 22 boot/route/guidance legs. |
|
||
| `npm run build` | `theme` | `tailwindcss -i ./src/theme.css -o ./dist/rams.css --minify`. |
|
||
| `npm run send` | `theme` | Copies `fonts/geist` + `fonts/space-grotesk` into `dist/fonts/`. |
|
||
| `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` builds, captures, then serves `http://localhost:8123/gallery.html + /basemap/` via `npm run basemap:preview` (`python3 -m http.server 8123`). |
|
||
|
||
Routing maintainer scripts with no npm alias (run directly with `node` from the workspace):
|
||
`build-road-graph.mjs`, `upload-road-graph.mjs` (fills the R2 bucket), `measure-route-cpu.mjs`
|
||
(the A*-CPU ladder the budget gate re-runs), `build-glyphs.mjs` (256×256 PNG render tiles via
|
||
`sharp`). Search maintainer scripts: `json-to-sqlite.mjs`, `overpass-topup.mjs` (gazetteer top-up
|
||
from Overpass — maintainer-time, never a runtime dependency). `docs/ROAD-GRAPH.md` is the fork
|
||
contract for the graph binary and the R2 bucket.
|
||
|
||
---
|
||
|
||
## 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. In plain `vite dev` the define keeps `main.ts`'s ambient
|
||
reference from throwing while the fallback mock still fires via `import.meta.env.DEV`.
|
||
- `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 (sharing one would make them clobber each other and make
|
||
the BA5 gate meaningless).
|
||
- `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`, `?dest=<gazetteer id>`, `?fixpair=<pairId>`, `?drive=<model>`,
|
||
`?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";` (inlined
|
||
at build time, never re-set at runtime)
|
||
- `theme/basemap/liberty-rams.json` — the generated basemap style; its source URL, sprite, and glyph
|
||
endpoints point at `https://tiles.openfreemap.org/...`.
|
||
|
||
---
|
||
|
||
## Data configuration (search + routing)
|
||
|
||
`search/` and `routing/` carry their data as committed files rather than configuration:
|
||
|
||
| File | Purpose |
|
||
| --- | --- |
|
||
| `search/src/data/kl-gazetteer.json` | The KL gazetteer seed — static-imported, esbuild-inlined for the Worker bundle. Carries the ODbL provenance header. |
|
||
| `routing/data/kl-roadgraph.v1.bin` | The built road-graph binary; `/api/route` decodes it via the `ROAD_GRAPH` R2 binding in production. |
|
||
| `routing/src/data/route-fixtures.json` | Committed demo-pair routes (polyline6 geometry), precomputed from the OSRM demo server by `routing/scripts/fetch-route-fixtures.mjs` over FIXED PUBLIC coordinates only — never user data. |
|
||
|
||
Neither workspace reads an environment variable at runtime; the loader-env suites in both pin the
|
||
"zero infrastructure" property (bare Node, Vite, and the wrangler/esbuild bundle all serve the same
|
||
seed). The only environment knob in this layer is `PRIVACY_AUDIT_ROOT`, honored by the maintainer
|
||
gate CLIs, not by the workspaces themselves.
|
||
|
||
---
|
||
|
||
## Ignore rules and generated output
|
||
|
||
`.gitignore` at the repo root TRACKS `.planning/` (the ignore line is commented out), `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` (with `_headers`); KV resolves to the real namespace; `ROAD_GRAPH` resolves to the real R2 bucket; the cron trigger schedules the stateless scan. |
|
||
| Gate-CLI `--live` | `process.env` or `bot/.dev.vars` | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` (shell wins over the file); absent credentials → exit `2` NOT RUN, never a pass. |
|
||
|
||
`@araharah/theme`, `@araharah/web`, `@araharah/search`, and `@araharah/routing` are consumed by
|
||
relative path within the workspace, so there is no registry token or publish step to configure.
|