1
Web Package
araharah-executor edited this page 2026-10-23 21:05:27 +08:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@araharah/web

The arah² Telegram Mini App — a MapLibre GL navigation shell built with Vite and TypeScript. It owns the three-source location acquisition chain, an in-memory-only session store, the Telegram WebApp integrations (theme slots, BackButton, safe-area insets), and the Rams chrome (status strip, boot/denied/signal-lost cards, recenter plate).

Part of the arah² monorepo (workspaces: theme, bot, web). Private package — not published to a registry.

⚠ VERIFY: the deploy step mounts web/dist/ via the bot worker's ASSETS binding (see ../bot wrangler config)


Install

Installed through the monorepo root (npm workspaces), not standalone:

# from the repository root
npm install

@araharah/theme is a workspace dependency ("@araharah/theme": "*"), so the theme package must be built before this one:

npm run build -w theme    # emits theme/dist/rams.css (only)
npm run basemap -w theme  # separately emits theme/basemap/liberty-rams.json

Quick start

npm run web:dev            # vite dev server on :5173 (strictPort)

With no Telegram client present, src/mock/telegram-mock.ts installs a fake window.Telegram.WebApp automatically in dev (import.meta.env.DEV), so the shell boots to LIVE in a plain browser with the KL default fix. Without it, the app renders its denied card — still no blank screen.

Production build (what the Worker serves):

npm run web:build          # theme build + vite build -> web/dist

Scripts

Run from the package directory, or from the root with npm run <script> -w web.

Command Description
npm run dev Vite dev server on port 5173 (strictPort: true, vite.config.ts).
npm run build Production build to dist/ — the harness mock and fetch stubs are dead-code-eliminated.
npm run build:harness vite build --mode harness to dist-harness/ — __ARAHARAH_HARNESS__ is true, so the mock installer and runner-page stubs are compiled in. Never deployed.
npm test Zero-dependency runner (node test/runner.mjs) that auto-discovers test/*.test.mjs.
npm run typecheck tsc --noEmit over src (strict, noUncheckedIndexedAccess, verbatimModuleSyntax).
npm run capture Playwright harness (scripts/capture.mjs): serves dist-harness/, drives every boot state, writes screenshots/, and runs the runtime storage probe.

dist/ and dist-harness/ must never share an outDir — the two builds would clobber each other and void the production-artifact gate.


Module map

src/
├── main.ts                 boot choreography — order is load-bearing
├── shell.css               the Rams chrome; imports @araharah/theme/dist/rams.css
├── vite-env.d.ts           ambient __ARAHARAH_HARNESS__ declaration
├── telegram/
│   ├── webapp.ts           the one typed accessor for window.Telegram.WebApp (TelegramWebAppLike)
│   ├── theme.ts            Telegram theme signal -> [data-tg-theme] slot switch
│   ├── safe-area.ts        env(safe-area-inset-*) ∪ WebApp.safeAreaInset -> CSS custom properties
│   └── backbutton.ts       BackButton total wire (root press -> WebApp.close())
├── session/
│   ├── client.ts           POST /api/session with the verbatim initData string
│   └── store.ts            memory-only token holder, 900s TTL — no storage API anywhere
├── location/
│   ├── chain.ts            acquireFix(): the locked telegram -> webview -> chat-pin chain
│   ├── telegram.ts         LocationManager wrapper + Android stale-fix nudge
│   ├── webview.ts          navigator.geolocation (maximumAge: 0)
│   ├── chat-pin.ts         bounded poll of /api/location + ASK VIA CHAT trigger
│   └── types.ts            Fix, FixSource, LocationState, KL_CENTER
├── map/
│   ├── map.ts              MapLibre v6 bootstrap; returns null when WebGL2 is absent
│   ├── follow.ts           following/free camera controller
│   └── marker.ts           position marker + accuracy circle + heading cone
├── ui/
│   ├── strip.ts            status strip (validating/acquiring/live/lost/denied/awaiting)
│   ├── cards.ts            boot / denied / signal-lost cards + gantry blink
│   └── recenter.ts         follow/recenter plate
├── i18n/strings.ts         every user-facing string, en + ms (the copy gate)
└── mock/telegram-mock.ts   Mock-Telegram harness — excluded from dist by BA5
graph TD
    MAIN["src/main.ts — boot choreography"]
    WA["src/telegram/webapp.ts"]
    TH["src/telegram/theme.ts"]
    SA["src/telegram/safe-area.ts"]
    BB["src/telegram/backbutton.ts"]
    SESS["src/session/client.ts"]
    STORE["src/session/store.ts"]
    CHAIN["src/location/chain.ts"]
    TG["src/location/telegram.ts"]
    WV["src/location/webview.ts"]
    PIN["src/location/chat-pin.ts"]
    TYPES["src/location/types.ts"]
    MAP["src/map/map.ts"]
    FOLLOW["src/map/follow.ts"]
    MARK["src/map/marker.ts"]
    STRIP["src/ui/strip.ts"]
    CARDS["src/ui/cards.ts"]
    REC["src/ui/recenter.ts"]
    STR["src/i18n/strings.ts"]
    MOCK["src/mock/telegram-mock.ts"]

    MAIN --> WA
    MAIN --> TH
    MAIN --> SA
    MAIN --> BB
    MAIN --> SESS
    MAIN --> CHAIN
    MAIN --> MAP
    MAIN --> STRIP
    MAIN --> CARDS
    MAIN --> REC
    MAIN --> MOCK
    SESS --> STORE
    CHAIN --> TG
    CHAIN --> WV
    CHAIN --> PIN
    CHAIN --> TYPES
    TG --> TYPES
    WV --> TYPES
    PIN --> TYPES
    MAP --> FOLLOW
    MAP --> MARK
    FOLLOW --> TYPES
    MARK --> TYPES
    STRIP --> TYPES
    STRIP --> STR
    CARDS --> STR
    REC --> STR

Usage examples

Acquire a fix through the chain (this is exactly what main.ts does):

import { acquireFix, isFixStale, fixAgeSeconds } from "./location/chain.ts";
import { createSession, } from "./session/client.ts";
import { getWebApp } from "./telegram/webapp.ts";
import { getSession } from "./session/store.ts";

const wa = getWebApp();                       // null off-Telegram — the chain handles null
const session = await createSession(wa?.initData ?? "");
if (!session.ok) throw new Error(session.reason); // "malformed" | "tampered" | "stale" | "network"

const { fix, state } = await acquireFix({ wa, getToken: getSession }, null);
if (fix !== null && !isFixStale(fix, Date.now())) {
  console.log(fix.source, fixAgeSeconds(fix, Date.now()), "s");
} else {
  console.log(state.kind);                    // "denied" | "awaiting-pin" | ...
}

Boot the map on its own (returns null rather than throwing on a WebGL2-less device):

import { bootMap } from "./map/map.ts";
import { createPositionMarker } from "./map/marker.ts";

const map = bootMap(document.getElementById("map")!);
if (map !== null) {
  const marker = createPositionMarker();
  marker.marker.addTo(map);
  marker.update(map, fix);
}

Testing

npm run web:test       # or: npm test   (inside web/)
npm run web:typecheck

test/runner.mjs auto-discovers every test/*.test.mjs suite and drains the shared registry from test/harness.mjs. Suites cover the location chain (location-chain.test.mjs), the memory-only session store (session.test.mjs), theme slots (theme.test.mjs), the en/ms copy gates (copy-gate.test.mjs), the no-storage invariant (no-storage.test.mjs), and the production build artifact (build-artifact.test.mjs — CDN references, self-hosted fonts, hashed MapLibre worker, inlined Rams style, and BA5/BA6 asserting no mock installer or fetch stub ships in dist/).

The Playwright capture harness is a separate command (npm run web:capture): it serves dist-harness/, waits on the RAMS-BASEMAP-READY and ARAHARAH-LIVE console signals, and writes the state gallery into screenshots/.