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/adr/0003-web-gateway.md | 85 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/adr/0003-web-gateway.md (limited to 'docs/adr/0003-web-gateway.md') diff --git a/docs/adr/0003-web-gateway.md b/docs/adr/0003-web-gateway.md new file mode 100644 index 0000000..5337e2b --- /dev/null +++ b/docs/adr/0003-web-gateway.md @@ -0,0 +1,85 @@ +# 0003 — Browser frontend through a server-side gateway + +Status: accepted, 2026-11-08 + +## Context + +The game already runs over TLS/TCP with a newline-delimited text protocol. A +browser cannot open a TCP socket, and putting game rules into JavaScript would +create a second implementation of payout, session and forfeit logic — exactly +what the project forbids. + +The site must stay a website in the ordinary sense: open `zagan.bet`, log in, +play. Nothing to install. Balances are play credits; there are no deposits, +withdrawals or KYC in this decision. + +## Decision + +1. **The browser is a third frontend over `ClientCore`.** It sends `Command` + JSON and renders `View` JSON. It contains no rules, no wire-protocol parser + and no balance arithmetic. +2. **A Rust gateway, `webd/`, owns each session.** One blocking thread per + browser session, one `ClientCore`, and one ordinary client TLS connection + upstream. The existing `client::transport` and `ClientCore` are reused. No + async runtime and no new concurrency style. +3. **`webd/` is a separate workspace member.** Like `gui/`, its dependencies + (tungstenite, HTTP cookie handling) must never enter the TUI, GUI or server + build graph. +4. **Everything is same-origin and operator-configured.** nginx terminates + TLS, serves the static app at `/play/`, and proxies `/ws` to the gateway on + loopback. The browser never sees a certificate, there is no CORS, and the + upstream address (`ZAGAN_UPSTREAM`, default `127.0.0.1:11525`) and SPKI + fingerprint (`ZAGAN_UPSTREAM_FINGERPRINT`, required) are gateway config. + A mismatch refuses to connect and logs both fingerprints. There is no TOFU + and no unsafe mode for the upstream leg. +5. **Session-only authentication.** Log in per visit. The gateway issues a + random HttpOnly, SameSite=Strict, Secure session cookie; the cookie covers a + refresh and the grace window only. No credential is stored in the browser or + in the gateway. A new connection with the same cookie takes over the session + and the old socket is told it was replaced. +6. **60-second grace window.** When the last browser socket for a session + closes, the session thread keeps the upstream connection and `ClientCore` + alive for 60 seconds (configurable). A reconnecting socket with the same + cookie reattaches and immediately receives the current `View`. Expiry closes + upstream, and the server forfeits a live hand per the existing disconnect + rule. There is no wire-level resume: the gateway, not the browser, holds the + TCP connection. +7. **Vanilla TypeScript + esbuild.** DOM for login, lobby, history and notices; + one canvas for the game surface. No framework runtime. Theme tokens are + reused from the desktop palette; felt green is reserved for roulette. +8. **Web v1 has full parity after trust.** Register, log in, lobby, all four + games, history, log out, notices, refresh grace. The connect/trust screens + do not exist in the browser because the gateway owns the upstream key. + +## Consequences + +- One implementation of the rules and parsers remains authoritative, and the + TUI/GUI/gateway all exercise the same `ClientCore` tests. +- The gateway becomes an operational component: it needs a service unit, + deployment, logging, a required upstream fingerprint, and a restart story. + A gateway restart loses live sessions and therefore forfeits their rounds; + this is accepted for v1. +- The browser cannot recover from a gateway crash by itself. The player logs in + again. +- A 60-second grace window means a live round can outlive a refresh, but not a + closed laptop for a minute and a half. +- WebSocket frames are same-origin JSON, so the page cannot be used as an open + proxy by another origin. The gateway also checks the `Origin` header against + `ZAGAN_WEB_ORIGIN`. +- The web app is play-money only. Adding payments later would be a separate + product and compliance decision, not a UI change. + +## Alternatives considered + +- **WASM `ClientCore` in the browser, dumb relay**: reuses Rust in the page, + but needs transport, TOFU and credential shims in wasm and a multi-megabyte + download for every player. Rejected. +- **JavaScript rewrite speaking the game protocol**: fastest UI iteration, but + a second implementation of rules and payout invariants. Rejected. +- **Tokio/axum gateway**: idiomatic WebSocket server, but a large async tree + and a second concurrency style for one blocking, low-traffic product. + Rejected. +- **Long-lived remember-me in the gateway**: better return visits, but a new + credential store and revocation flow. Deferred; session-only first. +- **Protocol-level resume tokens**: survives a gateway restart, but is a wire + change and more state on the server. Deferred. -- cgit v1.2.3