diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-08 03:29:35 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-08 03:29:35 +0100 |
| commit | 058432130628d00764663ecf819a9adc6793768d (patch) | |
| tree | d3f49f111fbfaa48647cca2600adcf9017af2d30 /docs/WEB-PLAN.md | |
| parent | web: serve latest.php through PHP and refuse stray PHP files (diff) | |
| download | zagan-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 '')
| -rw-r--r-- | docs/WEB-PLAN.md | 129 |
1 files changed, 129 insertions, 0 deletions
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 @@ | |||
| 1 | # Browser frontend plan | ||
| 2 | |||
| 3 | Status: v1 implemented in 5.0.0. This is the working plan and the deployment | ||
| 4 | contract; see `CONTEXT.md` and ADRs 0003/0004 for the decisions. | ||
| 5 | |||
| 6 | ## Shape | ||
| 7 | |||
| 8 | ``` | ||
| 9 | browser ──wss──> nginx ──ws──> webd ──TLS──> game server | ||
| 10 | /play/ /ws 127.0.0.1:8890 127.0.0.1:11525 | ||
| 11 | static files gateway one thread per authoritative rules, | ||
| 12 | browser session balances, crash clock | ||
| 13 | ``` | ||
| 14 | |||
| 15 | - `web/play/` is a static TypeScript app. It sends `Command` JSON and renders | ||
| 16 | `View` JSON. It has no rules and no wire parser. | ||
| 17 | - `webd/` is a workspace member. It owns one `ClientCore`, one upstream TLS | ||
| 18 | connection and a 60-second grace window per browser session. | ||
| 19 | - `src/protocol.rs` is v5: bounded frames, `REQ`/`RES`/`EVT`, request ids, | ||
| 20 | server time and crash commit–reveal. | ||
| 21 | |||
| 22 | ## Phases | ||
| 23 | |||
| 24 | 1. **Protocol v5** — frames, envelope, correlation, replay cache, clock sync, | ||
| 25 | ticks, commit–reveal, TUI/GUI updates. Done in the 5.0.0 change. | ||
| 26 | 2. **Gateway** — WS accept, cookie session registry, takeover, grace, pinned | ||
| 27 | upstream, `Command`/`View` JSON. Done in the 5.0.0 change. | ||
| 28 | 3. **Web shell** — login/register, lobby, history, notices, route at `/play/`. | ||
| 29 | Done in the 5.0.0 change. | ||
| 30 | 4. **Games** — slots, roulette (felt + chips), blackjack, crash (curve + | ||
| 31 | verification). Done in the 5.0.0 change. | ||
| 32 | 5. **Deployment** — nginx routes, static build, systemd unit, verification in | ||
| 33 | `scripts/deploy.sh`. Done in the 5.0.0 change. | ||
| 34 | |||
| 35 | ## Browser contract | ||
| 36 | |||
| 37 | Browser → gateway: | ||
| 38 | |||
| 39 | ```json | ||
| 40 | {"type":"command","command":{"Login":{"user":"alice","credential":"<sha256 hex>"}}} | ||
| 41 | {"type":"command","command":{"Play":{"game":"crash","bet":10}}} | ||
| 42 | {"type":"command","command":"Start"} | ||
| 43 | {"type":"ping"} | ||
| 44 | ``` | ||
| 45 | |||
| 46 | `Command` uses serde's external tagging because several variants carry a | ||
| 47 | primitive or a string. Unit variants are bare strings (`"Spin"`, `"Start"`). | ||
| 48 | |||
| 49 | Gateway → browser: | ||
| 50 | |||
| 51 | ```json | ||
| 52 | {"type":"hello","version":"5.0.0","grace_seconds":60,"user":"alice","time":1700000000000} | ||
| 53 | {"type":"view","view":{"screen":"lobby", "...": "..."}} | ||
| 54 | {"type":"notice","text":"Logged in as alice"} | ||
| 55 | {"type":"error","text":"Upstream connection failed: ..."} | ||
| 56 | {"type":"replaced"} | ||
| 57 | {"type":"pong"} | ||
| 58 | ``` | ||
| 59 | |||
| 60 | `View` is internally tagged by `screen` (`lobby`, `history`, `slots`, | ||
| 61 | `roulette`, `blackjack`, `crash`). The browser renders each screen; it never | ||
| 62 | computes a result. | ||
| 63 | |||
| 64 | The cookie is `zagan_session`, HttpOnly, SameSite=Strict, Secure on HTTPS. It | ||
| 65 | is set on the WebSocket handshake. It covers refresh and the grace window only; | ||
| 66 | there is no remember-me. | ||
| 67 | |||
| 68 | ## Build and run | ||
| 69 | |||
| 70 | ```sh | ||
| 71 | # static app | ||
| 72 | cd web/play | ||
| 73 | npm ci | ||
| 74 | npm run build # writes app.js; commit it | ||
| 75 | |||
| 76 | # gateway | ||
| 77 | cargo build -p zagan-bet-webd --release | ||
| 78 | ZAGAN_UPSTREAM=127.0.0.1:11525 \ | ||
| 79 | ZAGAN_UPSTREAM_FINGERPRINT=<64 hex> \ | ||
| 80 | ZAGAN_WEB_ORIGIN=https://zagan.2137697.xyz \ | ||
| 81 | ZAGAN_WEB_BIND=127.0.0.1:8890 \ | ||
| 82 | target/release/zagan-bet-webd | ||
| 83 | ``` | ||
| 84 | |||
| 85 | The fingerprint is the same SPKI SHA-256 the desktop client shows: | ||
| 86 | |||
| 87 | ```sh | ||
| 88 | openssl x509 -in cert.pem -pubkey -noout \ | ||
| 89 | | openssl pkey -pubin -outform DER \ | ||
| 90 | | sha256sum | ||
| 91 | ``` | ||
| 92 | |||
| 93 | Test against a staging site with | ||
| 94 | `ZAGAN_WEB_ORIGIN=http://127.0.0.1:8899 ZAGAN_WEB_COOKIE_SECURE=0`. | ||
| 95 | |||
| 96 | ## Security boundaries | ||
| 97 | |||
| 98 | - The gateway binds loopback. nginx terminates TLS and is the only public | ||
| 99 | entry. Do not expose `ZAGAN_WEB_BIND` to the internet. | ||
| 100 | - The upstream key is pinned; there is no TOFU and no unsafe mode. A mismatch | ||
| 101 | logs both fingerprints and refuses credentials. | ||
| 102 | - The browser has no certificate UI and no credential storage. XSS cannot read | ||
| 103 | the HttpOnly cookie; the backend still must escape every notice. | ||
| 104 | - The browser speaks JSON, not the game wire. A malicious page cannot use the | ||
| 105 | gateway as a protocol proxy: `Origin` is checked and the session cookie is | ||
| 106 | SameSite=Strict. | ||
| 107 | - The web app is play-money only. No deposits, withdrawals, KYC or real-money | ||
| 108 | claims belong in v1 copy. | ||
| 109 | |||
| 110 | ## Failure behaviour | ||
| 111 | |||
| 112 | | Event | Result | | ||
| 113 | | --- | --- | | ||
| 114 | | Refresh / new tab, within grace | Same `ClientCore`, current `View` restored | | ||
| 115 | | New tab, same cookie | Old tab gets `replaced` and closes; new tab continues | | ||
| 116 | | Socket closed, grace expires | Upstream closes; server forfeits live hand | | ||
| 117 | | Gateway restart | Sessions lost; players log in again | | ||
| 118 | | Upstream disconnect | Browser gets an error, core resets, reconnect attempts | | ||
| 119 | | Browser retry of a request | Gateway does not retry; request ids are per connection | | ||
| 120 | |||
| 121 | ## Verification | ||
| 122 | |||
| 123 | - `cargo test --workspace` covers frames, envelope parsing, idempotent replay, | ||
| 124 | commit verification, ticks, core routing and session JSON. | ||
| 125 | - `web/play`: `npm run typecheck` and `npm run build`. | ||
| 126 | - Manual WS smoke test: connect, login, play slots/crash, refresh in a live | ||
| 127 | crash round, verify the revealed seed. | ||
| 128 | - `scripts/deploy.sh` checks `/play/`, `app.js`, `style.css` and the existing | ||
| 129 | download feed. | ||
