From 058432130628d00764663ecf819a9adc6793768d Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 8 Nov 2026 03:29:35 +0100 Subject: v5.0.0: protocol v5 and the browser frontend Protocol v5 replaces newline lines with bounded [u32 len][text] frames and REQ/RES/EVT envelopes. Requests carry correlation ids and the server replays the last 256 replies per connection, so a gateway retry cannot double-debit and a late reply cannot clobber a newer request. HELLO/PONG carry server time; crash rounds announce started_at and push coalesced 10 Hz ticks. Crash now commits sha256(seed) before a round and reveals the seed when it ends, so clients recompute the crash point. Adds the webd/ gateway: one blocking thread, one ClientCore and one pinned upstream TLS connection per session, an HttpOnly session cookie, takeover, and a 60-second grace window. The browser sends Command JSON and renders View JSON; it never speaks the game protocol or stores credentials. Adds the vanilla TypeScript browser app at /play/ (login, lobby, all four games, history, notices, crash verification), the nginx /play and /ws routes, and the headless DOM smoke test. webd and the app stay out of the TUI/GUI/server build graph. The client/server version moves to 5.0.0 in lockstep; v4 peers are refused at HELLO. --- docs/WEB-PLAN.md | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 docs/WEB-PLAN.md (limited to 'docs/WEB-PLAN.md') diff --git a/docs/WEB-PLAN.md b/docs/WEB-PLAN.md new file mode 100644 index 0000000..895687b --- /dev/null +++ b/docs/WEB-PLAN.md @@ -0,0 +1,129 @@ +# 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. -- cgit v1.2.3