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 /AGENTS.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 'AGENTS.md')
| -rw-r--r-- | AGENTS.md | 65 |
1 files changed, 62 insertions, 3 deletions
| @@ -37,20 +37,79 @@ | |||
| 37 | transaction. | 37 | transaction. |
| 38 | 38 | ||
| 39 | ## Crash | 39 | ## Crash |
| 40 | - The client draws the multiplier from its own clock for display only. A | 40 | - The client draws the multiplier from its own clock, corrected by the server |
| 41 | cashout carries a session id and nothing else; the multiplier and payout come | 41 | time in `HELLO`/`PONG`, for display only. A cashout carries a session id and |
| 42 | from the server's clock at receipt. | 42 | nothing else; the multiplier and payout come from the server's clock at |
| 43 | receipt. | ||
| 43 | - Never send the crash point to the client before the round ends. A client that | 44 | - Never send the crash point to the client before the round ends. A client that |
| 44 | knows it can cash out at `crash - epsilon` every round for a guaranteed win. | 45 | knows it can cash out at `crash - epsilon` every round for a guaranteed win. |
| 45 | - The crash point is `max(1.0, (1 - edge) / (1 - u))`; do not "fix" the 1% of | 46 | - The crash point is `max(1.0, (1 - edge) / (1 - u))`; do not "fix" the 1% of |
| 46 | rounds that crash at 1.00x. That is where the house edge is charged on an | 47 | rounds that crash at 1.00x. That is where the house edge is charged on an |
| 47 | instant cashout, and it is why no cashout lockout is needed. | 48 | instant cashout, and it is why no cashout lockout is needed. |
| 49 | - Each round draws a 32-byte seed. `commit = sha256(seed)` goes out with | ||
| 50 | `CRASH_START`; the seed itself is revealed only when the round ends. The | ||
| 51 | crash point is `crash_point_from_seed(seed)` (first eight bytes, big-endian, | ||
| 52 | over 2^64), so a client can recompute it. Never reveal the seed while the | ||
| 53 | round is live. The guarantee is "fixed before you acted", not "ungrindable"; | ||
| 54 | see ADR 0004. | ||
| 48 | - The crash event is a server-initiated line (`CRASH_BUSTED`), not a reply. | 55 | - The crash event is a server-initiated line (`CRASH_BUSTED`), not a reply. |
| 49 | Client code must drain the socket on a timer and route pushed lines by | 56 | Client code must drain the socket on a timer and route pushed lines by |
| 50 | prefix instead of assuming the next line answers the last request. | 57 | prefix instead of assuming the next line answers the last request. |
| 51 | - `ServerCore::poll` must be idempotent: it settles the round, removes the | 58 | - `ServerCore::poll` must be idempotent: it settles the round, removes the |
| 52 | session, and pushes once. A second poll must produce nothing. | 59 | session, and pushes once. A second poll must produce nothing. |
| 53 | 60 | ||
| 61 | ## Protocol v5 | ||
| 62 | - Every packet is `[u32 big-endian length][UTF-8 payload]`, at most 64 KiB. | ||
| 63 | Never read a declared length above the limit into memory; close instead. | ||
| 64 | - Payloads are `REQ <id> [idem=<token>] <command>`, `RES <id> <body>` and | ||
| 65 | `EVT <seq> <body>`. The command/body text grammar is unchanged. | ||
| 66 | - `ClientCore` allocates request ids. Adapters must return a reply with | ||
| 67 | `handle_reply_id`; a reply with an unknown id is ignored, never applied to | ||
| 68 | whatever request happens to be pending. | ||
| 69 | - The server caches the last 256 request ids **per connection** and replays a | ||
| 70 | matching reply. A reused id with a different payload is an error. Do not make | ||
| 71 | this durable without a new ADR. | ||
| 72 | - `HELLO` and `PONG` carry Unix milliseconds. The offset is for display and | ||
| 73 | interpolation; the server settles crash. Never derive a payout from a | ||
| 74 | browser or gateway clock. | ||
| 75 | - While a crash round runs, `poll` pushes at most one `crash.tick` per 100 ms | ||
| 76 | per session. Settlement (`CRASH_BUSTED`) is never dropped or replaced by a | ||
| 77 | tick. | ||
| 78 | - A wire change is a MAJOR bump. Do not add a compatibility mode for v4. | ||
| 79 | |||
| 80 | ## Web gateway (`webd/`) | ||
| 81 | - The browser is a frontend, not a client. It sends `Command` JSON and renders | ||
| 82 | `View` JSON. It must never parse the game wire, compute a payout, or hold a | ||
| 83 | credential. | ||
| 84 | - `webd/` is a separate workspace member. Its dependencies (tungstenite, | ||
| 85 | serde_json, cookie handling) must not enter the TUI, GUI or server build | ||
| 86 | graph. | ||
| 87 | - One session thread owns one `ClientCore` and one upstream TLS connection. | ||
| 88 | No async runtime, no browser TOFU: `ZAGAN_UPSTREAM_FINGERPRINT` is required | ||
| 89 | and a mismatch refuses to connect. | ||
| 90 | - The session cookie is HttpOnly, SameSite=Strict, and Secure on HTTPS. Never | ||
| 91 | store a password or credential in the browser or gateway. Session-only auth; | ||
| 92 | no remember-me without a new ADR. | ||
| 93 | - Close is not forfeit: the gateway holds upstream for `ZAGAN_GRACE_SECONDS` | ||
| 94 | (default 60). Expiry closes upstream, which is the server's forfeit. A new | ||
| 95 | socket with the same cookie takes over; tell the old tab it was replaced. | ||
| 96 | - Bind to loopback and let nginx terminate TLS. Check `Origin` against | ||
| 97 | `ZAGAN_WEB_ORIGIN`. `/ws` is the only gateway route; `/play/` is static. | ||
| 98 | - A gateway restart loses live sessions and forfeits their rounds. Do not add | ||
| 99 | protocol-level resume here without an ADR. | ||
| 100 | |||
| 101 | ## Web app (`web/play/`) | ||
| 102 | - Vanilla TypeScript + esbuild. No framework runtime, no game rules, no wire | ||
| 103 | protocol. DOM for the shell, canvas for the game surface. | ||
| 104 | - Colours and spacing come from `web/play/style.css`, which mirrors | ||
| 105 | `gui/src/theme.rs`. Felt green is only ever the roulette table. | ||
| 106 | - Build with `cd web/play && npm ci && npm run build`; commit the generated | ||
| 107 | `app.js` so the site needs no Node at deploy time. | ||
| 108 | - `/play/` is the app; `/` stays the download page. Web v1 is play-money only: | ||
| 109 | no deposits, withdrawals or KYC copy. | ||
| 110 | - The browser cookie covers refresh and the grace window only. A closed tab | ||
| 111 | that returns later logs in again. | ||
| 112 | |||
| 54 | ## GUI | 113 | ## GUI |
| 55 | - The GUI is a second frontend over `ClientCore`, not a second client. | 114 | - The GUI is a second frontend over `ClientCore`, not a second client. |
| 56 | Frontends send `Command` and render `View`; the core owns navigation and | 115 | Frontends send `Command` and render `View`; the core owns navigation and |
