summaryrefslogtreecommitdiffstats
path: root/docs/adr/0003-web-gateway.md
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
committersalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
commit058432130628d00764663ecf819a9adc6793768d (patch)
treed3f49f111fbfaa48647cca2600adcf9017af2d30 /docs/adr/0003-web-gateway.md
parentweb: serve latest.php through PHP and refuse stray PHP files (diff)
downloadzagan-bet-058432130628d00764663ecf819a9adc6793768d.tar.gz
zagan-bet-058432130628d00764663ecf819a9adc6793768d.zip
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.
Diffstat (limited to 'docs/adr/0003-web-gateway.md')
-rw-r--r--docs/adr/0003-web-gateway.md85
1 files changed, 85 insertions, 0 deletions
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 @@
1# 0003 — Browser frontend through a server-side gateway
2
3Status: accepted, 2026-11-08
4
5## Context
6
7The game already runs over TLS/TCP with a newline-delimited text protocol. A
8browser cannot open a TCP socket, and putting game rules into JavaScript would
9create a second implementation of payout, session and forfeit logic — exactly
10what the project forbids.
11
12The site must stay a website in the ordinary sense: open `zagan.bet`, log in,
13play. Nothing to install. Balances are play credits; there are no deposits,
14withdrawals or KYC in this decision.
15
16## Decision
17
181. **The browser is a third frontend over `ClientCore`.** It sends `Command`
19 JSON and renders `View` JSON. It contains no rules, no wire-protocol parser
20 and no balance arithmetic.
212. **A Rust gateway, `webd/`, owns each session.** One blocking thread per
22 browser session, one `ClientCore`, and one ordinary client TLS connection
23 upstream. The existing `client::transport` and `ClientCore` are reused. No
24 async runtime and no new concurrency style.
253. **`webd/` is a separate workspace member.** Like `gui/`, its dependencies
26 (tungstenite, HTTP cookie handling) must never enter the TUI, GUI or server
27 build graph.
284. **Everything is same-origin and operator-configured.** nginx terminates
29 TLS, serves the static app at `/play/`, and proxies `/ws` to the gateway on
30 loopback. The browser never sees a certificate, there is no CORS, and the
31 upstream address (`ZAGAN_UPSTREAM`, default `127.0.0.1:11525`) and SPKI
32 fingerprint (`ZAGAN_UPSTREAM_FINGERPRINT`, required) are gateway config.
33 A mismatch refuses to connect and logs both fingerprints. There is no TOFU
34 and no unsafe mode for the upstream leg.
355. **Session-only authentication.** Log in per visit. The gateway issues a
36 random HttpOnly, SameSite=Strict, Secure session cookie; the cookie covers a
37 refresh and the grace window only. No credential is stored in the browser or
38 in the gateway. A new connection with the same cookie takes over the session
39 and the old socket is told it was replaced.
406. **60-second grace window.** When the last browser socket for a session
41 closes, the session thread keeps the upstream connection and `ClientCore`
42 alive for 60 seconds (configurable). A reconnecting socket with the same
43 cookie reattaches and immediately receives the current `View`. Expiry closes
44 upstream, and the server forfeits a live hand per the existing disconnect
45 rule. There is no wire-level resume: the gateway, not the browser, holds the
46 TCP connection.
477. **Vanilla TypeScript + esbuild.** DOM for login, lobby, history and notices;
48 one canvas for the game surface. No framework runtime. Theme tokens are
49 reused from the desktop palette; felt green is reserved for roulette.
508. **Web v1 has full parity after trust.** Register, log in, lobby, all four
51 games, history, log out, notices, refresh grace. The connect/trust screens
52 do not exist in the browser because the gateway owns the upstream key.
53
54## Consequences
55
56- One implementation of the rules and parsers remains authoritative, and the
57 TUI/GUI/gateway all exercise the same `ClientCore` tests.
58- The gateway becomes an operational component: it needs a service unit,
59 deployment, logging, a required upstream fingerprint, and a restart story.
60 A gateway restart loses live sessions and therefore forfeits their rounds;
61 this is accepted for v1.
62- The browser cannot recover from a gateway crash by itself. The player logs in
63 again.
64- A 60-second grace window means a live round can outlive a refresh, but not a
65 closed laptop for a minute and a half.
66- WebSocket frames are same-origin JSON, so the page cannot be used as an open
67 proxy by another origin. The gateway also checks the `Origin` header against
68 `ZAGAN_WEB_ORIGIN`.
69- The web app is play-money only. Adding payments later would be a separate
70 product and compliance decision, not a UI change.
71
72## Alternatives considered
73
74- **WASM `ClientCore` in the browser, dumb relay**: reuses Rust in the page,
75 but needs transport, TOFU and credential shims in wasm and a multi-megabyte
76 download for every player. Rejected.
77- **JavaScript rewrite speaking the game protocol**: fastest UI iteration, but
78 a second implementation of rules and payout invariants. Rejected.
79- **Tokio/axum gateway**: idiomatic WebSocket server, but a large async tree
80 and a second concurrency style for one blocking, low-traffic product.
81 Rejected.
82- **Long-lived remember-me in the gateway**: better return visits, but a new
83 credential store and revocation flow. Deferred; session-only first.
84- **Protocol-level resume tokens**: survives a gateway restart, but is a wire
85 change and more state on the server. Deferred.