summaryrefslogtreecommitdiffstats
path: root/docs/adr/0003-web-gateway.md
blob: 5337e2b0b851fd0bf3efa02aa531c63ba018cbda (plain) (blame)
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
# 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.