@bountyboard/arcade-sdk 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,265 @@
1
+ # External authoritative multiplayer servers
2
+
3
+ Use this integration when your game already has a mature authoritative server
4
+ that must remain responsible for admission, inputs, simulation, state, and
5
+ results. Bounty Board connects the Arcade SDK to that server through a small
6
+ adapter contract; it does not relay traffic or run a second simulation.
7
+
8
+ External authorities are reviewed and enabled per game. Registration is not
9
+ self-service. Contact Bounty Board before implementing the adapter so the game
10
+ slug, endpoints, room-code rules, ticket keys, and result model can be agreed
11
+ for staging and production.
12
+
13
+ ## What stays authoritative
14
+
15
+ Your server remains the only source of truth. It must:
16
+
17
+ - admit a socket only after verifying its Bounty Board ticket;
18
+ - treat `joinData` and every client input as untrusted;
19
+ - run the only game simulation;
20
+ - clear held controls and other transient input on disconnect;
21
+ - send each player only the state that player may see;
22
+ - decide when a finite match is over and author the final standings; and
23
+ - keep endless rooms endless instead of inventing a terminal result.
24
+
25
+ The SDK handles ticket acquisition, WebSocket connection, reconnection, and a
26
+ small room-message envelope. It never grants a client authority over identity,
27
+ state, roles, or results.
28
+
29
+ ## Registration information
30
+
31
+ Provide Bounty Board with the following for each environment:
32
+
33
+ - the exact Arcade game slug;
34
+ - one fixed `wss:` endpoint dedicated to authenticated SDK rooms;
35
+ - the room-code format and maximum length your server accepts;
36
+ - whether missing room codes may create rooms;
37
+ - any validated, game-defined `joinData` fields;
38
+ - whether the game has finite matches or endless sessions; and
39
+ - an operational contact for key rotation or incident response.
40
+
41
+ The registered endpoint must not contain userinfo, query parameters, or a
42
+ fragment. Use `ws:` only for loopback development. Staging and production need
43
+ different endpoints and different secrets.
44
+
45
+ Bounty Board provisions a dedicated ticket-verification secret for the game
46
+ and environment. Store it only on servers. Do not put it in the game bundle,
47
+ browser storage, logs, analytics, crash reports, or client-visible environment
48
+ variables. Do not reuse credentials from another game or room service.
49
+
50
+ ## Connection flow
51
+
52
+ 1. The game calls `joinRoom()` from the Arcade SDK.
53
+ 2. The SDK asks the trusted Bounty Board host for a room ticket.
54
+ 3. Bounty Board verifies the game approval, multiplayer approval, active play
55
+ session, player or guest identity, game slug, and requested room code.
56
+ 4. Bounty Board returns a 60-second ticket and the registered WebSocket URL.
57
+ 5. The SDK appends the encoded ticket and optional encoded `joinData`, then
58
+ opens the socket.
59
+ 6. Your server verifies the ticket before admitting the socket and sends a
60
+ `welcome` frame within 10 seconds.
61
+
62
+ The sandboxed game never receives a Bounty Board session cookie and never calls
63
+ the ticket HTTP endpoint itself.
64
+
65
+ ## Ticket format
66
+
67
+ Tickets are compact HMAC envelopes, not JWTs:
68
+
69
+ ```text
70
+ payloadB64 = base64url(UTF8(JSON(payload)))
71
+ signatureB64 = base64url(HMAC-SHA256(payloadB64, ticketSecret))
72
+ ticket = payloadB64 + "." + signatureB64
73
+ ```
74
+
75
+ The HMAC input is the UTF-8 byte sequence of the base64url payload string.
76
+ `iat` and `exp` are Unix milliseconds. The current lifetime is 60 seconds.
77
+
78
+ ```json
79
+ {
80
+ "v": 1,
81
+ "sub": "player:<opaque-game-scoped-value>",
82
+ "name": "Display Name",
83
+ "avatarUrl": null,
84
+ "slug": "your-game-slug",
85
+ "roomId": "ROOM-CODE",
86
+ "guest": false,
87
+ "iat": 1700000000000,
88
+ "exp": 1700000060000
89
+ }
90
+ ```
91
+
92
+ Before accepting a connection, verify all of the following:
93
+
94
+ - the ticket contains exactly one separator and both parts decode;
95
+ - the HMAC matches using a constant-time comparison;
96
+ - `v` is the supported ticket version;
97
+ - `exp` is still in the future and `iat` is reasonable;
98
+ - `slug` exactly matches the registered game;
99
+ - `roomId` exactly matches the requested room and your registered rules;
100
+ - `sub` is non-empty; and
101
+ - required claim types and lengths are valid.
102
+
103
+ Treat `sub` as an opaque game-scoped identity. Never attempt to map it to a
104
+ Bounty Board account or correlate it with another game. `name` and `avatarUrl`
105
+ are display data, not authorization data. `avatarUrl` may be `null` for any
106
+ player.
107
+
108
+ ## SDK call
109
+
110
+ Module builds import multiplayer from its dedicated entry point:
111
+
112
+ ```ts
113
+ import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';
114
+
115
+ const room = await joinRoom({
116
+ code: 'ROOM-CODE', // or create: true to ask the SDK for a new code
117
+ joinData: {
118
+ schemaVersion: 1,
119
+ loadout: 'starter',
120
+ mode: 'friends',
121
+ },
122
+ });
123
+
124
+ room.on('snapshot', ({ state }) => renderAuthoritativeState(state));
125
+ room.on('connection', ({ connected }) => setReconnecting(!connected));
126
+
127
+ if (!room.trySend({ type: 'move', x: 1, y: 0 })) {
128
+ setReconnecting(true);
129
+ }
130
+ ```
131
+
132
+ Script-tag builds use `BBArcade.multiplayer.joinRoom(...)`.
133
+
134
+ `joinData` is an optional JSON object capped at 1 KiB. Validate every field,
135
+ reject unknown or oversized values, and never accept credentials or identity
136
+ claims from it. If your server distinguishes create from join, use a documented
137
+ game-defined intent field and rate-limit room creation. That field is still
138
+ untrusted and grants no role or permission by itself.
139
+
140
+ ## WebSocket URL
141
+
142
+ The SDK opens the registered endpoint with URL-encoded query parameters:
143
+
144
+ ```text
145
+ wss://multiplayer.example.com/bountyboard?ticket=<encoded>&join=<encoded-json>
146
+ ```
147
+
148
+ The `join` parameter is omitted when no `joinData` was supplied. Keep this path
149
+ separate from any anonymous or legacy socket endpoint so a guessed room code
150
+ cannot cross the authentication boundary. Redact or disable request-URI logging
151
+ on this route because the short-lived ticket is in the query string.
152
+
153
+ ## SDK room envelope
154
+
155
+ All frames are JSON text. After successful verification and admission, send a
156
+ `welcome` frame within 10 seconds:
157
+
158
+ ```json
159
+ {
160
+ "t": "welcome",
161
+ "playerId": "room-scoped-player-id",
162
+ "players": [{ "id": "room-scoped-player-id", "name": "Display Name", "avatarUrl": null }],
163
+ "state": { "phase": "lobby" }
164
+ }
165
+ ```
166
+
167
+ Send authoritative state snapshots as:
168
+
169
+ ```json
170
+ {
171
+ "t": "snapshot",
172
+ "tick": 42,
173
+ "state": { "phase": "playing", "players": [] }
174
+ }
175
+ ```
176
+
177
+ Send game-defined transient events without changing their payload:
178
+
179
+ ```json
180
+ { "t": "event", "data": { "type": "round_started", "round": 2 } }
181
+ ```
182
+
183
+ The SDK also recognizes player roster events:
184
+
185
+ ```json
186
+ { "t": "player_join", "player": { "id": "p2", "name": "Guest", "avatarUrl": null } }
187
+ { "t": "player_leave", "playerId": "p2" }
188
+ ```
189
+
190
+ Clients send inputs, never state:
191
+
192
+ ```json
193
+ { "t": "input", "seq": 7, "data": { "type": "move", "x": 1, "y": 0 } }
194
+ ```
195
+
196
+ Validate the envelope, sequence, payload shape, value ranges, rate, current
197
+ player state, and game rules before applying an input. Bound both inbound and
198
+ outbound frame sizes.
199
+
200
+ Reject an invalid ticket before `welcome` with an error and close the socket:
201
+
202
+ ```json
203
+ { "t": "error", "code": "unauthenticated" }
204
+ ```
205
+
206
+ Use `rejected` for invalid admission or `joinData`. After admission, error codes
207
+ remain game-defined strings and are surfaced through `room.on('error', ...)`.
208
+
209
+ ## Reconnection
210
+
211
+ The SDK automatically makes a small number of reconnect attempts. It requests
212
+ a fresh ticket for the same room and reuses the original `joinData`. Rebind the
213
+ seat using the verified `(ticket.sub, ticket.roomId)` pair. Never trust a
214
+ client-supplied player id or reconnect token as the proof of identity.
215
+
216
+ Send the latest complete viewer-safe state in the new `welcome` frame. If your
217
+ client uses prediction, include the last processed input sequence in that state
218
+ so it can discard acknowledged inputs. Clear held controls while the socket is
219
+ absent.
220
+
221
+ ## Finite matches and endless rooms
222
+
223
+ Only a server-authoritative finite game may emit `match_end`:
224
+
225
+ ```json
226
+ {
227
+ "t": "match_end",
228
+ "results": [
229
+ {
230
+ "playerId": "p1",
231
+ "name": "Display Name",
232
+ "placement": 1,
233
+ "score": 1200,
234
+ "outcome": "win"
235
+ }
236
+ ]
237
+ }
238
+ ```
239
+
240
+ The client receives this as `room.on('end', ({ results }) => ...)`. Persistent
241
+ Bounty Board results, when enabled, use a separately provisioned
242
+ server-to-server reporting credential. Never accept a client-originated result
243
+ or reporting credential.
244
+
245
+ Endless rooms must not emit `match_end`. Death, respawn, a round transition, or
246
+ a player leaving is not automatically a terminal match result.
247
+
248
+ ## Production checklist
249
+
250
+ - Use an isolated staging endpoint and secrets before production.
251
+ - Confirm tampered, expired, wrong-slug, and wrong-room tickets fail closed.
252
+ - Test both guest and logged-in opaque subjects.
253
+ - Reject malformed, unknown, or oversized `joinData`.
254
+ - Send `welcome` only after ticket and admission validation succeeds.
255
+ - Verify every snapshot is safe for its specific viewer.
256
+ - Test disconnect, held-input cleanup, fresh-ticket reconnect, and `leave()`.
257
+ - Rate-limit upgrades, room creation, joins, and inputs.
258
+ - Redact tickets and join payloads from access logs and error telemetry.
259
+ - Rotate the dedicated ticket secret without reusing another environment's key.
260
+ - Emit final results only from a real authoritative terminal condition.
261
+ - Keep the game playable when multiplayer is unsupported or temporarily down.
262
+
263
+ For SDK integration basics, see https://www.bountyboard.gg/arcade/sdk. For the
264
+ agent-readable API contract, see
265
+ https://www.bountyboard.gg/arcade/sdk/llms.txt.