summaryrefslogtreecommitdiffstats
path: root/docs/adr
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--docs/adr/0001-gui-frontend-architecture.md3
-rw-r--r--docs/adr/0003-web-gateway.md85
-rw-r--r--docs/adr/0004-protocol-v5.md119
3 files changed, 206 insertions, 1 deletions
diff --git a/docs/adr/0001-gui-frontend-architecture.md b/docs/adr/0001-gui-frontend-architecture.md
index 44dca32..e4dd5ac 100644
--- a/docs/adr/0001-gui-frontend-architecture.md
+++ b/docs/adr/0001-gui-frontend-architecture.md
@@ -1,6 +1,7 @@
1# 0001 — GUI is a second frontend over a shared core 1# 0001 — GUI is a second frontend over a shared core
2 2
3Status: accepted, 2026-11-01 3Status: accepted, 2026-11-01; the "no browser" constraint is superseded by
4[0003](0003-web-gateway.md), which adds a browser frontend through a gateway.
4 5
5## Context 6## Context
6 7
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.
diff --git a/docs/adr/0004-protocol-v5.md b/docs/adr/0004-protocol-v5.md
new file mode 100644
index 0000000..c74ad83
--- /dev/null
+++ b/docs/adr/0004-protocol-v5.md
@@ -0,0 +1,119 @@
1# 0004 — Protocol v5: frames, envelopes, time and commit–reveal
2
3Status: accepted, 2026-11-08
4
5## Context
6
7The current wire is newline-delimited text over TLS. It has served the TUI and
8GUI well, but four problems are now worth fixing before a browser gateway is
9built on top:
10
11- `LineBuffer` grows without bound if a peer never sends a newline, and there
12 is no explicit maximum message size.
13- A reply is assumed to answer the request that is currently pending. A late
14 reply, a retry or a second in-flight request can clobber state.
15- There is no idempotency: a gateway retry after a timeout can charge a bet
16 twice.
17- The crash multiplier is anchored to each frontend's local clock, and the
18 crash point is only "trust the server".
19
20The web gateway makes the first three worse (it retries and multiplexes) and
21the browser makes the fourth visible (a page and the desktop can disagree).
22
23## Decision
24
25This is one wire change and one `MAJOR` release (`5.0.0`); there is no
26compatibility mode.
27
28### Framing
29
30- Every packet is `[u32 length][payload]`, length big-endian.
31- `payload` is UTF-8 text, at most 64 KiB. A larger declared length is a
32 protocol error and the connection closes; buffers never grow past one frame.
33- The payload has no trailing newline.
34
35### Envelope
36
37- `REQ <id> [idem=<token>] <command>` — client request. `<id>` is a `u64`
38 chosen by the client, monotonically increasing on the connection.
39- `RES <id> <body>` — the reply to that request.
40- `EVT <seq> <body>` — a server-initiated event. `<seq>` is a `u64`,
41 incremented once per event on the connection.
42
43The existing text grammar is the `<command>` and `<body>`; `HELLO 5.0.0`,
44`PLAY_START crash 50`, `GAME_RESULT ...` keep their meaning.
45
46`HELLO` is the first framed request. The server ignores every other command
47before a successful handshake. The reply is
48`HELLO <version> latest=<v> url=<url> time=<unix_ms>`.
49
50### Correlation and idempotency
51
52- The client keeps a request id until its `RES` arrives. Replies are routed by
53 id, never by "the next line".
54- The server keeps a bounded 256-entry FIFO of `(id, payload, reply)` on each
55 connection. A repeated id with the same payload returns the stored reply
56 without executing the command again. A repeated id with a different payload
57 returns `ERROR Request id reused with a different command`.
58- The cache is per connection and in memory. A reconnect is a new game session
59 by design; durable idempotency is deferred.
60- `idem=<token>` is optional metadata for logs and for a future cross-
61 connection cache. The request id is the cache key.
62
63### Clock and ticks
64
65- `HELLO` and `PONG` carry the server's Unix time in milliseconds. `PING`
66 replies `PONG <unix_ms>`.
67- Clients keep an offset between server time and their own clock. The offset is
68 for display and interpolation only; the server clock settles a cash-out.
69- A crash start carries `started_at=<unix_ms>` and `commit=<hex>`.
70- While a crash round runs, the server pushes at most one
71 `EVT <seq> crash.tick session=<id> t=<unix_ms> m=<multiplier>` per 100 ms.
72 The client interpolates between ticks and may keep rendering locally.
73- `CRASH_BUSTED` remains an event, now with the reveal attached.
74
75### Commit–reveal crash
76
77- At round start the server draws 32 random bytes as the round seed and sends
78 `commit = sha256(seed)` in hex. It also sends the crash point's start time.
79- The crash point is a pure function of the seed:
80 `u = u64_be(seed[0..8]) / 2^64`, then
81 `crash_point_from_uniform(u)`. The seed never reaches the client before the
82 round ends.
83- When the round ends (`CRASH_STATE` after cash-out, or `CRASH_BUSTED`), the
84 server includes `seed=<hex>`. A client verifies `sha256(seed) == commit` and
85 recomputes the crash point. The UI can show "verified".
86- This proves the crash point was fixed before the player acted. It does not
87 prove the seed was not ground to a favourable value; a hash chain with client
88 entropy is the documented next step if that matters.
89
90## Consequences
91
92- The TUI, GUI, server and gateway all move to v5 together; a v4 peer is
93 refused at `HELLO`.
94- `LineBuffer` is replaced by a bounded `FrameBuffer`. Existing newline-based
95 tests and helpers are rewritten around frames.
96- `ClientCore` allocates request ids and routes replies by id. Its `Effect`
97 carries the id, so an adapter writes the envelope and hands the `RES` back
98 with the same id.
99- Crash rendering can be smoother and consistent across frontends, and the
100 crash result is independently checkable from the revealed seed.
101- The per-connection cache is deliberately small and forgets on reconnect; a
102 retry after a reconnect is a new action, not a replay.
103
104## Alternatives considered
105
106- **JSON or binary frames**: cleaner types or smaller payloads, but a rewrite
107 of every parser for no scale need. Text inside a bounded frame keeps logs
108 readable and the change small.
109- **Newline-delimited frames with a maximum line length**: still needs a
110 separate envelope and does not bound the buffer as cleanly; a declared length
111 is simpler.
112- **Durable idempotency keyed by `(user, idem)`**: survives reconnects, but the
113 gateway owns retries on one connection and a reconnect is a fresh session.
114 Deferred.
115- **Server seed plus client seed without a commit round-trip**: the server
116 sees the client seed before committing and can bias the pair; a half-measure
117 is worse than an honest per-round commitment.
118- **Hash-chain epoch**: stronger anti-grinding, but requires persistent chain
119 state and a restart story. Deferred.