Files
arah2/web
araharah-executor aec8b07530 test(07-01): FS1-FS9 fullscreen gates + mock satisfies the grown contract
- telegram-mock gains requestFullscreen/exitFullscreen/lockOrientation/unlockOrientation + __fullscreen* state fields in the SAME change as the contract (contract + fake + test together)
- FS1-FS7: both-halves guard vectors, the throwing-platform path (FS4), the exit mirror (FS5), the event categories (FS6), the old-client fallback (FS7, SC2.e)
- FS8: safeAreaChanged re-resolution through the union authority, with a minimal document stub restored in finally
- FS9: comment-stripped source-scan ban on the DOM-fullscreen/orientation-lock escape hatches (T-07-10) + the guard-is-not-optional assertion
- suite 157 passed, 0 failed (148 baseline + 9); mutation check: neutering the 8.0 guard fails FS2/FS5
2026-10-25 03:45:49 +08:00
..

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


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