1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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":"<sha256 hex>"}}}
{"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.
|