# Browser frontend plan Status: v1 implemented in 5.0.0. This is the working plan and the deployment contract; see `CONTEXT.md` and ADRs 0003/0004 for the decisions. ## Shape ``` browser ──wss──> nginx ──ws──> webd ──TLS──> game server /play/ /ws 127.0.0.1:8890 127.0.0.1:11525 static files gateway one thread per authoritative rules, browser session balances, crash clock ``` - `web/play/` is a static TypeScript app. It sends `Command` JSON and renders `View` JSON. It has no rules and no wire parser. - `webd/` is a workspace member. It owns one `ClientCore`, one upstream TLS connection and a 60-second grace window per browser session. - `src/protocol.rs` is v5: bounded frames, `REQ`/`RES`/`EVT`, request ids, server time and crash commit–reveal. ## Phases 1. **Protocol v5** — frames, envelope, correlation, replay cache, clock sync, ticks, commit–reveal, TUI/GUI updates. Done in the 5.0.0 change. 2. **Gateway** — WS accept, cookie session registry, takeover, grace, pinned upstream, `Command`/`View` JSON. Done in the 5.0.0 change. 3. **Web shell** — login/register, lobby, history, notices, route at `/play/`. Done in the 5.0.0 change. 4. **Games** — slots, roulette (felt + chips), blackjack, crash (curve + verification). Done in the 5.0.0 change. 5. **Deployment** — nginx routes, static build, systemd unit, verification in `scripts/deploy.sh`. Done in the 5.0.0 change. ## Browser contract Browser → gateway: ```json {"type":"command","command":{"Login":{"user":"alice","credential":""}}} {"type":"command","command":{"Play":{"game":"crash","bet":10}}} {"type":"command","command":"Start"} {"type":"ping"} ``` `Command` uses serde's external tagging because several variants carry a primitive or a string. Unit variants are bare strings (`"Spin"`, `"Start"`). Gateway → browser: ```json {"type":"hello","version":"5.0.0","grace_seconds":60,"user":"alice","time":1700000000000} {"type":"view","view":{"screen":"lobby", "...": "..."}} {"type":"notice","text":"Logged in as alice"} {"type":"error","text":"Upstream connection failed: ..."} {"type":"replaced"} {"type":"pong"} ``` `View` is internally tagged by `screen` (`lobby`, `history`, `slots`, `roulette`, `blackjack`, `crash`). The browser renders each screen; it never computes a result. The cookie is `zagan_session`, HttpOnly, SameSite=Strict, Secure on HTTPS. It is set on the WebSocket handshake. It covers refresh and the grace window only; there is no remember-me. ## Build and run ```sh # static app cd web/play npm ci npm run build # writes app.js; commit it # gateway cargo build -p zagan-bet-webd --release ZAGAN_UPSTREAM=127.0.0.1:11525 \ ZAGAN_UPSTREAM_FINGERPRINT=<64 hex> \ ZAGAN_WEB_ORIGIN=https://zagan.2137697.xyz \ ZAGAN_WEB_BIND=127.0.0.1:8890 \ target/release/zagan-bet-webd ``` The fingerprint is the same SPKI SHA-256 the desktop client shows: ```sh openssl x509 -in cert.pem -pubkey -noout \ | openssl pkey -pubin -outform DER \ | sha256sum ``` Test against a staging site with `ZAGAN_WEB_ORIGIN=http://127.0.0.1:8899 ZAGAN_WEB_COOKIE_SECURE=0`. ## Security boundaries - The gateway binds loopback. nginx terminates TLS and is the only public entry. Do not expose `ZAGAN_WEB_BIND` to the internet. - The upstream key is pinned; there is no TOFU and no unsafe mode. A mismatch logs both fingerprints and refuses credentials. - The browser has no certificate UI and no credential storage. XSS cannot read the HttpOnly cookie; the backend still must escape every notice. - The browser speaks JSON, not the game wire. A malicious page cannot use the gateway as a protocol proxy: `Origin` is checked and the session cookie is SameSite=Strict. - The web app is play-money only. No deposits, withdrawals, KYC or real-money claims belong in v1 copy. ## Failure behaviour | Event | Result | | --- | --- | | Refresh / new tab, within grace | Same `ClientCore`, current `View` restored | | New tab, same cookie | Old tab gets `replaced` and closes; new tab continues | | Socket closed, grace expires | Upstream closes; server forfeits live hand | | Gateway restart | Sessions lost; players log in again | | Upstream disconnect | Browser gets an error, core resets, reconnect attempts | | Browser retry of a request | Gateway does not retry; request ids are per connection | ## Verification - `cargo test --workspace` covers frames, envelope parsing, idempotent replay, commit verification, ticks, core routing and session JSON. - `web/play`: `npm run typecheck` and `npm run build`. - Manual WS smoke test: connect, login, play slots/crash, refresh in a live crash round, verify the revealed seed. - `scripts/deploy.sh` checks `/play/`, `app.js`, `style.css` and the existing download feed.