@helixdev/helix-sdk 0.1.1-staging.10

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hypersonic Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @hypersoniclabs/helix-sdk
2
+
3
+ The **HELIX Instant browser SDK** — the runtime a world embeds to talk to the HELIX shell
4
+ (the play page that iframes it). v0.1 provides identity; later versions add multiplayer, voice,
5
+ wallet, and inventory.
6
+
7
+ A world is a static bundle that runs inside a **sandboxed iframe**. The shell hands it a
8
+ short-lived, world-scoped session token (minted by the backend at
9
+ `POST /api/v1/instant-worlds/:worldId/session`) over `postMessage`. The SDK wraps that handshake.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @hypersoniclabs/helix-sdk
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { Helix } from '@hypersoniclabs/helix-sdk';
21
+
22
+ const { embedded, world, user } = await Helix.init(); // call once, at startup
23
+
24
+ if (Helix.auth.isAuthenticated()) {
25
+ const me = await Helix.auth.getUser(); // { id, username, displayName }
26
+ }
27
+
28
+ // Raise the shell's login overlay (no-op reload — world state is preserved):
29
+ loginButton.onclick = async () => {
30
+ try {
31
+ const user = await Helix.auth.requestLogin();
32
+ console.log('signed in as', user.username);
33
+ } catch {
34
+ /* dismissed or unavailable */
35
+ }
36
+ };
37
+
38
+ Helix.auth.onAuthChanged((user) => updateUi(user)); // login + logout
39
+ ```
40
+
41
+ When the world is opened directly (e.g. a local `vite dev` server with no shell), `init()` resolves
42
+ with `embedded: false` and all identity APIs return `null`/`false` — so the same build runs locally
43
+ and embedded.
44
+
45
+ ### API
46
+
47
+ - `Helix.init(): Promise<{ embedded, world, user }>` — handshake; call once before anything else.
48
+ - `Helix.auth.getUser(): Promise<HelixUser | null>`
49
+ - `Helix.auth.isAuthenticated(): boolean`
50
+ - `Helix.auth.requestLogin(): Promise<HelixUser>` — resolves on login, rejects on dismiss/unavailable.
51
+ - `Helix.auth.onAuthChanged(fn): () => void` — fires on login and logout; returns an unsubscribe fn.
52
+ - `Helix.getSessionToken(): string | null` — the world-scoped token, for advanced direct API calls.
53
+
54
+ ## Protocol
55
+
56
+ [`src/protocol.ts`](src/protocol.ts) is the wire contract (`postMessage` messages between world and
57
+ shell) and is imported by both sides. Any breaking change must bump `PROTOCOL_VERSION`. The
58
+ `HelixSession` shape matches the backend's `InstantWorldSessionResponseDto`, so the shell forwards
59
+ the backend response verbatim.
60
+
61
+ ## Develop
62
+
63
+ ```bash
64
+ npm install
65
+ npm test # jest + jsdom
66
+ npm run build # tsc → dist/ (ESM)
67
+ npm run lint
68
+ ```
@@ -0,0 +1,48 @@
1
+ import { HelixWorldContext, HelixUser, DebugLogEntry } from './protocol';
2
+ import { HelixMultiplayer } from './multiplayer';
3
+ export type { HelixWorldContext, HelixSession, HelixUser, DebugLogEntry, DebugLogLevel } from './protocol';
4
+ export type { HelixRoom, JoinRoomOptions, ReplicaInput } from './multiplayer';
5
+ export * from './multiplayer-contract';
6
+ export type HelixInitResult = {
7
+ embedded: boolean;
8
+ world: HelixWorldContext | null;
9
+ user: HelixUser | null;
10
+ };
11
+ type AuthListener = (user: HelixUser | null) => void;
12
+ declare class HelixSdk {
13
+ private shellOrigin;
14
+ private session;
15
+ private world;
16
+ private initialized;
17
+ private listeners;
18
+ private debugEnabled;
19
+ private debugRing;
20
+ private debugListeners;
21
+ private consoleHooked;
22
+ private pendingLogins;
23
+ init(): Promise<HelixInitResult>;
24
+ readonly multiplayer: HelixMultiplayer;
25
+ readonly debug: {
26
+ enabled: () => boolean;
27
+ log: (...args: unknown[]) => void;
28
+ onLog: (cb: (entry: DebugLogEntry) => void) => (() => void);
29
+ recent: () => DebugLogEntry[];
30
+ };
31
+ readonly auth: {
32
+ getUser: () => Promise<HelixUser | null>;
33
+ isAuthenticated: () => boolean;
34
+ requestLogin: () => Promise<HelixUser>;
35
+ onAuthChanged: (listener: AuthListener) => (() => void);
36
+ };
37
+ getSessionToken(): string | null;
38
+ private snapshot;
39
+ private assertInitialized;
40
+ private waitForInit;
41
+ private onMessage;
42
+ private setSession;
43
+ private enableDebug;
44
+ private hookConsole;
45
+ private captureLog;
46
+ private post;
47
+ }
48
+ export declare const Helix: HelixSdk;
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ import { isShellMessage, PROTOCOL_VERSION, } from './protocol';
2
+ import { HelixMultiplayer } from './multiplayer';
3
+ // The multiplayer wire contract. Re-exported here so `@hypersoniclabs/helix-sdk` root consumers (and
4
+ // classic node-resolution type imports, e.g. the backend minting RoomCredentialClaims) get it; bundler
5
+ // consumers that want only the contract import the lighter '@hypersoniclabs/helix-sdk/multiplayer-contract'.
6
+ export * from './multiplayer-contract';
7
+ const INIT_TIMEOUT_MS = 3000;
8
+ const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
9
+ const DEBUG_LOG_MAX = 200; // bounded ring of recent log entries (overlay backfill + pre-init forward flush)
10
+ class HelixSdk {
11
+ shellOrigin = null;
12
+ session = null;
13
+ world = null;
14
+ initialized = false;
15
+ listeners = new Set();
16
+ debugEnabled = false;
17
+ debugRing = [];
18
+ debugListeners = new Set();
19
+ consoleHooked = false;
20
+ pendingLogins = new Map();
21
+ // Performs the shell handshake. Call once at world start, before any other
22
+ // Helix API. Resolves with embedded=false if no shell answers (local dev).
23
+ async init() {
24
+ if (this.initialized)
25
+ return this.snapshot();
26
+ window.addEventListener('message', this.onMessage);
27
+ if (window.parent !== window) {
28
+ this.post({ type: 'helix:ready', protocolVersion: PROTOCOL_VERSION }, '*');
29
+ await this.waitForInit();
30
+ }
31
+ this.initialized = true;
32
+ return this.snapshot();
33
+ }
34
+ // Multiplayer (pillar D): Helix.multiplayer.joinRoom(). Reads the world_session token + world id off
35
+ // this singleton; the heavy Colyseus client is dynamically imported only on join.
36
+ multiplayer = new HelixMultiplayer({
37
+ isInitialized: () => this.initialized,
38
+ getToken: () => this.session?.token ?? null,
39
+ getWorldId: () => this.world?.id ?? null,
40
+ });
41
+ // Debug surface. A world's own console lives in its cross-origin iframe — invisible to DevTools / an
42
+ // automation harness attached to the shell, and to teammates without that frame selected. debug.log feeds
43
+ // a bounded ring an in-world overlay can render (onLog/recent), and — when the shell enabled debug
44
+ // (?debug) — is mirrored to the shell frame's console via helix:log. enableDebug() also hooks console.*
45
+ // so ALL world output is captured, not just explicit debug.log calls. enabled() is a fn (the flag flips
46
+ // at init, after the field initializer runs).
47
+ debug = {
48
+ enabled: () => this.debugEnabled,
49
+ log: (...args) => this.captureLog('info', args),
50
+ onLog: (cb) => {
51
+ this.debugListeners.add(cb);
52
+ return () => this.debugListeners.delete(cb);
53
+ },
54
+ recent: () => this.debugRing.slice(),
55
+ };
56
+ auth = {
57
+ getUser: async () => {
58
+ this.assertInitialized();
59
+ return this.session?.user ?? null;
60
+ },
61
+ isAuthenticated: () => {
62
+ this.assertInitialized();
63
+ return this.session !== null;
64
+ },
65
+ // Asks the shell to raise its login overlay. Resolves with the user after
66
+ // a successful login; rejects if the player dismisses it or no shell is
67
+ // present. World state is untouched either way — no reload happens.
68
+ requestLogin: () => {
69
+ this.assertInitialized();
70
+ if (this.session)
71
+ return Promise.resolve(this.session.user);
72
+ if (!this.shellOrigin) {
73
+ return Promise.reject(new Error('Helix: not embedded in a HELIX shell — login unavailable'));
74
+ }
75
+ const requestId = Math.random().toString(36).slice(2);
76
+ return new Promise((resolve, reject) => {
77
+ const timer = setTimeout(() => {
78
+ this.pendingLogins.delete(requestId);
79
+ reject(new Error('Helix: login request timed out'));
80
+ }, LOGIN_TIMEOUT_MS);
81
+ this.pendingLogins.set(requestId, {
82
+ resolve: (u) => {
83
+ clearTimeout(timer);
84
+ resolve(u);
85
+ },
86
+ reject: (e) => {
87
+ clearTimeout(timer);
88
+ reject(e);
89
+ },
90
+ });
91
+ this.post({ type: 'helix:request-login', requestId }, this.shellOrigin);
92
+ });
93
+ },
94
+ // Fires on login and logout (user becomes null). Returns an unsubscribe fn.
95
+ onAuthChanged: (listener) => {
96
+ this.listeners.add(listener);
97
+ return () => this.listeners.delete(listener);
98
+ },
99
+ };
100
+ // World-scoped session token for direct platform API calls (used by later
101
+ // SDK modules; exposed for advanced cases). Null when unauthenticated.
102
+ getSessionToken() {
103
+ return this.session?.token ?? null;
104
+ }
105
+ snapshot() {
106
+ return {
107
+ embedded: this.shellOrigin !== null,
108
+ world: this.world,
109
+ user: this.session?.user ?? null,
110
+ };
111
+ }
112
+ assertInitialized() {
113
+ if (!this.initialized) {
114
+ throw new Error('Helix: call Helix.init() before using the SDK');
115
+ }
116
+ }
117
+ waitForInit() {
118
+ return new Promise((resolve) => {
119
+ const timer = setTimeout(resolve, INIT_TIMEOUT_MS);
120
+ const check = () => {
121
+ if (this.shellOrigin) {
122
+ clearTimeout(timer);
123
+ resolve();
124
+ }
125
+ else {
126
+ setTimeout(check, 10);
127
+ }
128
+ };
129
+ check();
130
+ });
131
+ }
132
+ onMessage = (event) => {
133
+ if (!isShellMessage(event.data))
134
+ return;
135
+ // First valid helix:init pins the shell origin; everything after must match it.
136
+ if (this.shellOrigin && event.origin !== this.shellOrigin)
137
+ return;
138
+ const msg = event.data;
139
+ switch (msg.type) {
140
+ case 'helix:init':
141
+ if (this.shellOrigin)
142
+ return;
143
+ this.shellOrigin = event.origin;
144
+ this.world = msg.world;
145
+ this.setSession(msg.session);
146
+ if (msg.debug)
147
+ this.enableDebug();
148
+ break;
149
+ case 'helix:session':
150
+ this.setSession(msg.session);
151
+ break;
152
+ case 'helix:login-result': {
153
+ const pending = this.pendingLogins.get(msg.requestId);
154
+ if (!pending)
155
+ return;
156
+ this.pendingLogins.delete(msg.requestId);
157
+ if (msg.ok && this.session)
158
+ pending.resolve(this.session.user);
159
+ else
160
+ pending.reject(new Error(`Helix: login ${msg.reason ?? 'failed'}`));
161
+ break;
162
+ }
163
+ }
164
+ };
165
+ setSession(session) {
166
+ const before = this.session?.user.id ?? null;
167
+ this.session = session;
168
+ const after = session?.user.id ?? null;
169
+ if (before !== after) {
170
+ for (const listener of this.listeners)
171
+ listener(session?.user ?? null);
172
+ }
173
+ }
174
+ enableDebug() {
175
+ if (this.debugEnabled)
176
+ return;
177
+ this.debugEnabled = true;
178
+ this.hookConsole();
179
+ // Flush anything buffered before the shell turned debug on (e.g. early lifecycle logs).
180
+ if (this.shellOrigin)
181
+ for (const entry of this.debugRing)
182
+ this.post({ type: 'helix:log', entry }, this.shellOrigin);
183
+ }
184
+ // Capture ALL console output once debug is on (three.js warnings, SDK errors, uncaught logs) — not just
185
+ // explicit debug.log calls. Pass-through preserves normal console behaviour in the iframe.
186
+ hookConsole() {
187
+ if (this.consoleHooked || typeof console === 'undefined')
188
+ return;
189
+ this.consoleHooked = true;
190
+ const levels = ['log', 'info', 'warn', 'error', 'debug'];
191
+ const c = console;
192
+ for (const level of levels) {
193
+ const orig = c[level]?.bind(console);
194
+ if (!orig)
195
+ continue;
196
+ c[level] = (...args) => {
197
+ this.captureLog(level, args);
198
+ orig(...args);
199
+ };
200
+ }
201
+ }
202
+ captureLog(level, args) {
203
+ const entry = { level, args: args.map(stringifyArg), t: Date.now() };
204
+ this.debugRing.push(entry);
205
+ if (this.debugRing.length > DEBUG_LOG_MAX)
206
+ this.debugRing.shift();
207
+ for (const cb of this.debugListeners) {
208
+ try {
209
+ cb(entry);
210
+ }
211
+ catch {
212
+ /* a listener must never break logging */
213
+ }
214
+ }
215
+ if (this.debugEnabled && this.shellOrigin)
216
+ this.post({ type: 'helix:log', entry }, this.shellOrigin);
217
+ }
218
+ post(message, targetOrigin) {
219
+ window.parent.postMessage(message, targetOrigin);
220
+ }
221
+ }
222
+ // Serialize a console arg to a string the shell can print: strings as-is, Errors to their stack, other
223
+ // objects to compact JSON (falling back to String() on cyclic/unserializable values).
224
+ function stringifyArg(a) {
225
+ if (typeof a === 'string')
226
+ return a;
227
+ if (a instanceof Error)
228
+ return a.stack ?? `${a.name}: ${a.message}`;
229
+ try {
230
+ return typeof a === 'object' && a !== null ? JSON.stringify(a) : String(a);
231
+ }
232
+ catch {
233
+ return String(a);
234
+ }
235
+ }
236
+ // Worlds import this singleton: `import { Helix } from '@hypersoniclabs/helix-sdk'`.
237
+ export const Helix = new HelixSdk();
238
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAKjD,qGAAqG;AACrG,uGAAuG;AACvG,6GAA6G;AAC7G,cAAc,wBAAwB,CAAC;AAYvC,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AACvC,MAAM,aAAa,GAAG,GAAG,CAAC,CAAC,iFAAiF;AAE5G,MAAM,QAAQ;IACJ,WAAW,GAAkB,IAAI,CAAC;IAClC,OAAO,GAAwB,IAAI,CAAC;IACpC,KAAK,GAA6B,IAAI,CAAC;IACvC,WAAW,GAAG,KAAK,CAAC;IACpB,SAAS,GAAG,IAAI,GAAG,EAAgB,CAAC;IACpC,YAAY,GAAG,KAAK,CAAC;IACrB,SAAS,GAAoB,EAAE,CAAC;IAChC,cAAc,GAAG,IAAI,GAAG,EAAkC,CAAC;IAC3D,aAAa,GAAG,KAAK,CAAC;IACtB,aAAa,GAAG,IAAI,GAAG,EAG5B,CAAC;IAEJ,2EAA2E;IAC3E,2EAA2E;IAC3E,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC7C,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAEnD,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,EAAE,GAAG,CAAC,CAAC;YAC3E,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;IACzB,CAAC;IAED,qGAAqG;IACrG,kFAAkF;IACzE,WAAW,GAAG,IAAI,gBAAgB,CAAC;QAC1C,aAAa,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW;QACrC,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,IAAI;QAC3C,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,IAAI;KACzC,CAAC,CAAC;IAEH,qGAAqG;IACrG,0GAA0G;IAC1G,mGAAmG;IACnG,wGAAwG;IACxG,wGAAwG;IACxG,8CAA8C;IACrC,KAAK,GAAG;QACf,OAAO,EAAE,GAAY,EAAE,CAAC,IAAI,CAAC,YAAY;QACzC,GAAG,EAAE,CAAC,GAAG,IAAe,EAAQ,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,EAAkC,EAAgB,EAAE;YAC1D,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC5B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC9C,CAAC;QACD,MAAM,EAAE,GAAoB,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE;KACtD,CAAC;IAEO,IAAI,GAAG;QACd,OAAO,EAAE,KAAK,IAA+B,EAAE;YAC7C,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC;QACpC,CAAC;QAED,eAAe,EAAE,GAAY,EAAE;YAC7B,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC;QAC/B,CAAC;QAED,0EAA0E;QAC1E,wEAAwE;QACxE,oEAAoE;QACpE,YAAY,EAAE,GAAuB,EAAE;YACrC,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,OAAO;gBAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC5D,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;gBACtB,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,KAAK,CAAC,0DAA0D,CAAC,CACtE,CAAC;YACJ,CAAC;YAED,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACtD,OAAO,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAChD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBAC5B,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;oBACrC,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC,CAAC;gBACtD,CAAC,EAAE,gBAAgB,CAAC,CAAC;gBACrB,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE;oBAChC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;wBACb,YAAY,CAAC,KAAK,CAAC,CAAC;wBACpB,OAAO,CAAC,CAAC,CAAC,CAAC;oBACb,CAAC;oBACD,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;wBACZ,YAAY,CAAC,KAAK,CAAC,CAAC;wBACpB,MAAM,CAAC,CAAC,CAAC,CAAC;oBACZ,CAAC;iBACF,CAAC,CAAC;gBACH,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,SAAS,EAAE,EAAE,IAAI,CAAC,WAAY,CAAC,CAAC;YAC3E,CAAC,CAAC,CAAC;QACL,CAAC;QAED,4EAA4E;QAC5E,aAAa,EAAE,CAAC,QAAsB,EAAgB,EAAE;YACtD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC7B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC/C,CAAC;KACF,CAAC;IAEF,0EAA0E;IAC1E,uEAAuE;IACvE,eAAe;QACb,OAAO,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC;IACrC,CAAC;IAEO,QAAQ;QACd,OAAO;YACL,QAAQ,EAAE,IAAI,CAAC,WAAW,KAAK,IAAI;YACnC,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,IAAI;SACjC,CAAC;IACJ,CAAC;IAEO,iBAAiB;QACvB,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IAEO,WAAW;QACjB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;YACnD,MAAM,KAAK,GAAG,GAAG,EAAE;gBACjB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;oBACrB,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,OAAO,EAAE,CAAC;gBACZ,CAAC;qBAAM,CAAC;oBACN,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACxB,CAAC;YACH,CAAC,CAAC;YACF,KAAK,EAAE,CAAC;QACV,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,SAAS,GAAG,CAAC,KAAmB,EAAE,EAAE;QAC1C,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,OAAO;QACxC,gFAAgF;QAChF,IAAI,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC,WAAW;YAAE,OAAO;QAElE,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC;QACvB,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;YACjB,KAAK,YAAY;gBACf,IAAI,IAAI,CAAC,WAAW;oBAAE,OAAO;gBAC7B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC;gBAChC,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;gBACvB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC7B,IAAI,GAAG,CAAC,KAAK;oBAAE,IAAI,CAAC,WAAW,EAAE,CAAC;gBAClC,MAAM;YACR,KAAK,eAAe;gBAClB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC7B,MAAM;YACR,KAAK,oBAAoB,CAAC,CAAC,CAAC;gBAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACtD,IAAI,CAAC,OAAO;oBAAE,OAAO;gBACrB,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACzC,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,OAAO;oBAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;;oBAC1D,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,gBAAgB,GAAG,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC;gBACzE,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEM,UAAU,CAAC,OAA4B;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC;QAC7C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,MAAM,KAAK,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC;QACvC,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACrB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS;gBAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IAEO,WAAW;QACjB,IAAI,IAAI,CAAC,YAAY;YAAE,OAAO;QAC9B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QACzB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,wFAAwF;QACxF,IAAI,IAAI,CAAC,WAAW;YAAE,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,SAAS;gBAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACtH,CAAC;IAED,wGAAwG;IACxG,2FAA2F;IACnF,WAAW;QACjB,IAAI,IAAI,CAAC,aAAa,IAAI,OAAO,OAAO,KAAK,WAAW;YAAE,OAAO;QACjE,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,MAAM,MAAM,GAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAC1E,MAAM,CAAC,GAAG,OAA+D,CAAC;QAC1E,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;YACrC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;gBAChC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;gBAC7B,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;YAChB,CAAC,CAAC;QACJ,CAAC;IACH,CAAC;IAEO,UAAU,CAAC,KAAoB,EAAE,IAAe;QACtD,MAAM,KAAK,GAAkB,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACpF,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,aAAa;YAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QAClE,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACrC,IAAI,CAAC;gBACH,EAAE,CAAC,KAAK,CAAC,CAAC;YACZ,CAAC;YAAC,MAAM,CAAC;gBACP,yCAAyC;YAC3C,CAAC;QACH,CAAC;QACD,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,WAAW;YAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACvG,CAAC;IAEO,IAAI,CAAC,OAA4B,EAAE,YAAoB;QAC7D,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACnD,CAAC;CACF;AAED,uGAAuG;AACvG,sFAAsF;AACtF,SAAS,YAAY,CAAC,CAAU;IAC9B,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC,YAAY,KAAK;QAAE,OAAO,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;IACpE,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,34 @@
1
+ export declare const ROOM_CREDENTIAL_AUDIENCE: "helix-instant-room";
2
+ /**
3
+ * Claims the backend signs (HS384) and the room verifies. Mirrors the world_session claim shape
4
+ * (sub/displayName/worldId/scopes/jti) plus the immutable buildId the room loads its config from.
5
+ */
6
+ export interface RoomCredentialClaims {
7
+ /** Player/user id. */
8
+ sub: string;
9
+ /** Sanitized display name for nameplates. */
10
+ displayName: string;
11
+ /** World the room belongs to. */
12
+ worldId: string;
13
+ /** Immutable build the room loads its declarative config (allowed messages, room-var shape) from. */
14
+ buildId: string;
15
+ /** Scopes from manifest.permissions; must include 'multiplayer' to join a room. */
16
+ scopes: string[];
17
+ /** Manifest maxPlayers (authoritative) → the room's maxClients cap, so it auto-locks + spills when full. */
18
+ maxPlayers: number;
19
+ /** Monotonic per-world build counter (1, 2, 3 …) of the active build — telemetry + config cache key. */
20
+ buildNumber: number;
21
+ /**
22
+ * Signed URL of this build's `multiplayer.json` — the validated declarative config baked into the
23
+ * published bundle. Derived server-side from the build, NEVER client-supplied, so the room fetches its
24
+ * config from an authentic, immutable location and caches the parsed result by buildId. The room
25
+ * tolerates its absence (older credentials / a build with no declared config) by running the fixed schema.
26
+ */
27
+ configUrl: string;
28
+ /** Always ROOM_CREDENTIAL_AUDIENCE. */
29
+ aud: typeof ROOM_CREDENTIAL_AUDIENCE;
30
+ iss: string;
31
+ exp: number;
32
+ iat: number;
33
+ jti: string;
34
+ }
@@ -0,0 +1,5 @@
1
+ // Room credential — the short-lived JWT the backend mints (createRoomSession) and the Colyseus room
2
+ // verifies in onAuth with a shared HMAC secret (ROOM_JWT_SECRET, Option C). Distinct audience from the
3
+ // world_session token (aud 'helix-instant-world') so a world token can't be replayed to join a room.
4
+ export const ROOM_CREDENTIAL_AUDIENCE = 'helix-instant-room';
5
+ //# sourceMappingURL=credential.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential.js","sourceRoot":"","sources":["../../src/multiplayer-contract/credential.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,uGAAuG;AACvG,qGAAqG;AAErG,MAAM,CAAC,MAAM,wBAAwB,GAAG,oBAA6B,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Wire format version. Bump on any incompatible change to PlayerReplicaState, the message table, the
3
+ * credential claims, or the realized declared-state shape (roomVars / per-player vars / entities).
4
+ *
5
+ * The SDK sends this in its join options; the room compares it in onAuth and LOGS a mismatch (P1.C2) — it
6
+ * does NOT reject. The SDK is frozen into each published bundle, so a newer room must keep admitting older
7
+ * builds; rejecting on mismatch would brick every published world on a bump. The detection is for
8
+ * cross-version diagnostics, and contract changes are kept additive/forward so older clients still play.
9
+ */
10
+ export declare const CONTRACT_VERSION: 11;
11
+ export * from './state';
12
+ export * from './room';
13
+ export * from './messages';
14
+ export * from './credential';
@@ -0,0 +1,30 @@
1
+ // @hypersoniclabs/helix-sdk/multiplayer-contract — the multiplayer wire contract.
2
+ //
3
+ // The single source of truth for the data shapes three independently-built pieces must agree on:
4
+ // • the Colyseus room (@colyseus/schema state + message dispatch + runtime validation),
5
+ // • this SDK's Helix.multiplayer client (send path + room.state),
6
+ // • the engine's NetworkDriver adapter (consumes the replicated params → blackboard).
7
+ // Plus the backend (mints RoomCredentialClaims) and the room (verifies them).
8
+ //
9
+ // Pure types + constants, ZERO runtime deps — so the standalone three.js engine adapter can import
10
+ // the types at zero runtime cost. Runtime message validation lives in the room (the only place
11
+ // untrusted inbound messages arrive), not here.
12
+ //
13
+ // The state has two layers (see room.ts): players are FIXED (the engine character contract); roomVars,
14
+ // per-player vars, and entities are DECLARED per-world — that's how an agent syncs custom game state
15
+ // without writing server code. The generic 'action' message (messages.ts) is the channel for it.
16
+ /**
17
+ * Wire format version. Bump on any incompatible change to PlayerReplicaState, the message table, the
18
+ * credential claims, or the realized declared-state shape (roomVars / per-player vars / entities).
19
+ *
20
+ * The SDK sends this in its join options; the room compares it in onAuth and LOGS a mismatch (P1.C2) — it
21
+ * does NOT reject. The SDK is frozen into each published bundle, so a newer room must keep admitting older
22
+ * builds; rejecting on mismatch would brick every published world on a bump. The detection is for
23
+ * cross-version diagnostics, and contract changes are kept additive/forward so older clients still play.
24
+ */
25
+ export const CONTRACT_VERSION = 11; // v11: EntityStateBatch — many owner-entity uploads in one message (Tier 2 Phase 4.10, beats the per-connection message-count cap)
26
+ export * from './state';
27
+ export * from './room';
28
+ export * from './messages';
29
+ export * from './credential';
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/multiplayer-contract/index.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,EAAE;AACF,iGAAiG;AACjG,0FAA0F;AAC1F,oEAAoE;AACpE,wFAAwF;AACxF,8EAA8E;AAC9E,EAAE;AACF,mGAAmG;AACnG,+FAA+F;AAC/F,gDAAgD;AAChD,EAAE;AACF,uGAAuG;AACvG,qGAAqG;AACrG,iGAAiG;AAEjG;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAW,CAAC,CAAC,mIAAmI;AAEhL,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,173 @@
1
+ import type { Vec3 } from './state';
2
+ import type { RoomVarValue } from './room';
3
+ export declare const ClientMessageType: {
4
+ /** The player's predicted kinematic state for this tick (seq-tagged for reconciliation). */
5
+ readonly State: "state";
6
+ /** Activate or deactivate an ability on this player (the built-in, engine-level intent). */
7
+ readonly Ability: "ability";
8
+ /** A world-authored declared action (the generic extensibility channel — see ActionMessage). */
9
+ readonly Action: "action";
10
+ /** An owner-authoritative entity's client-simulated state for this tick (Tier 2 Phase 4.6c — see EntityStateMessage). */
11
+ readonly EntityState: "entityState";
12
+ /**
13
+ * A BATCH of owner-authoritative entity uploads for this tick — ALL the entities this connection hosts in one
14
+ * message (Tier 2 Phase 4.10 — see EntityStateBatchMessage). The per-connection rate cap counts MESSAGES, so a
15
+ * host of N entities must batch (one message/tick) instead of N single EntityState messages, or it starves its
16
+ * own upload budget. EntityScene uses this; the single EntityState stays for one-off uploads (e.g. a flag).
17
+ */
18
+ readonly EntityStateBatch: "entityStateBatch";
19
+ /** Clock-sync probe: the client's local timestamp, which the room echoes in a Pong (RTT + offset). */
20
+ readonly Ping: "ping";
21
+ };
22
+ export type ClientMessageType = (typeof ClientMessageType)[keyof typeof ClientMessageType];
23
+ /** Server → client messages (the room sends these; SDK exposes them via room.onMessage(type, cb)). */
24
+ export declare const ServerMessageType: {
25
+ /** Reply to a Ping: the client's echoed timestamp + the server's current time, for offset estimation. */
26
+ readonly Pong: "pong";
27
+ };
28
+ export type ServerMessageType = (typeof ServerMessageType)[keyof typeof ServerMessageType];
29
+ /** Per-tick state upload. Units match PlayerReplicaState (position m; all angles degrees, see *Deg). */
30
+ export interface StateMessage {
31
+ /** Monotonic per-client sequence; the room echoes the last applied seq for client reconciliation. */
32
+ seq: number;
33
+ position: Vec3;
34
+ facingYawDeg: number;
35
+ speed: number;
36
+ moveDirectionDeg: number;
37
+ verticalVelocity: number;
38
+ grounded: boolean;
39
+ crouched: boolean;
40
+ aimYawDeg: number;
41
+ aimPitchDeg: number;
42
+ }
43
+ /**
44
+ * An owner-authoritative entity's client-simulated state for this tick (Tier 2 Phase 4.6c, spec §9/§10.4). The
45
+ * client that the server marked as the entity's `controller` (see EntityState.controller) simulates the entity
46
+ * and uploads its transform + declared vars; the room sanity-validates (it owns this entity? movement within
47
+ * its declared `maxSpeed`? vars within their declared bounds?) and relays. Cross-player consequences still go
48
+ * through server-validated rules/actions — never this channel (the publish-time firewall enforces that).
49
+ */
50
+ export interface EntityStateMessage {
51
+ /** The entity id this upload is for (the state.entities map key). Rejected unless the sender is its controller. */
52
+ entity: string;
53
+ /** Monotonic per-entity sequence; the room ignores a stale/out-of-order frame. */
54
+ seq: number;
55
+ /** The entity's world position, METERS — gated against the kind's declared maxSpeed. */
56
+ position: Vec3;
57
+ /** Declared per-kind vars to set (same vocabulary as roomVars); each is clamped to its declared type/bounds on ingest. */
58
+ vars?: Record<string, RoomVarValue>;
59
+ /**
60
+ * The authority EPOCH the client is simulating under (Tier 2 Phase 4.5.12, spec §9). Mirror the entity's synced
61
+ * `authorityEpoch`; the room bumps it on every controller handoff (host migration AND voluntary ownership
62
+ * transfer) and rejects an upload whose epoch ≠ the entity's current one — fencing a revoked/stale authority
63
+ * era even from a still-connected prior owner whose sessionId would otherwise pass. Omit only for the very
64
+ * first frame before the field is observed; the room tolerates absence (sessionId ownership still gates).
65
+ */
66
+ epoch?: number;
67
+ }
68
+ /**
69
+ * One entity's slot in an EntityStateBatchMessage. Compact, short keys (sent verbatim in msgpack), with position
70
+ * as a 3-tuple instead of a {x,y,z} object — the same authoritative meaning as EntityStateMessage's per-entity
71
+ * fields. `v`/`ep` are omitted when unchanged/unobserved to keep the batch small.
72
+ */
73
+ export interface EntityStateBatchEntry {
74
+ /** Entity id (the state.entities map key). Rejected unless the sender is its controller. */
75
+ e: string;
76
+ /** World position as [x, y, z], METERS — gated against the kind's declared maxSpeed. */
77
+ p: [number, number, number];
78
+ /** Declared per-kind vars to set (clamped to declared type/bounds on ingest). Omit when unchanged this tick. */
79
+ v?: Record<string, RoomVarValue>;
80
+ /** Authority epoch this entity is simulated under (mirror its synced authorityEpoch). Omit before observed. */
81
+ ep?: number;
82
+ }
83
+ /**
84
+ * A batch of owner-authoritative entity uploads (Tier 2 Phase 4.10, spec §10.4) — every entity this connection
85
+ * hosts, in ONE message. The per-connection rate cap (MESSAGE_RATE.entityStateHz) counts messages, so batching
86
+ * lets a host refresh many entities per tick within one token instead of spending one token per entity. The room
87
+ * applies each entry exactly like a single EntityStateMessage (ownership + epoch + maxSpeed gate + var clamp).
88
+ */
89
+ export interface EntityStateBatchMessage {
90
+ /** Monotonic per-connection batch sequence; the room ignores a stale/out-of-order batch per entity (gate seq). */
91
+ seq: number;
92
+ /** One entry per hosted entity changed this tick (bounded by the room's per-owner entity cap). */
93
+ states: EntityStateBatchEntry[];
94
+ }
95
+ /** Discrete ability activation intent. */
96
+ export interface AbilityMessage {
97
+ /** Ability id (must be one the world's build declares). */
98
+ ability: string;
99
+ /** true = activate, false = deactivate. */
100
+ active: boolean;
101
+ }
102
+ /**
103
+ * A world-authored declared action — the generic extensibility channel (e.g. 'captureFlag',
104
+ * 'scorePoint'). The room validates `name` against the world's declared action set and `args` against
105
+ * that action's declared arg schema before mutating authoritative state (table-driven dispatch); this
106
+ * module carries the envelope + arg vocabulary only, never the per-world rules.
107
+ */
108
+ export interface ActionMessage {
109
+ /** Action name; must be one the world's build declares. */
110
+ name: string;
111
+ /** Primitive/Vec3 args, validated against the declared action's schema by the room. */
112
+ args?: Record<string, RoomVarValue>;
113
+ }
114
+ /**
115
+ * Clock-sync probe. The client sends its local send time; the room replies (Pong) with this value echoed
116
+ * plus its own clock, so the client estimates RTT (recv − send) and offset (serverTimeMs − send − RTT/2).
117
+ */
118
+ export interface PingMessage {
119
+ /** The client's local clock (epoch ms) at send — echoed verbatim in the Pong. */
120
+ clientTimeMs: number;
121
+ }
122
+ /** Reply to a Ping — the echoed client time + the server's clock at reply, for offset/RTT estimation. */
123
+ export interface PongMessage {
124
+ clientTimeMs: number;
125
+ serverTimeMs: number;
126
+ }
127
+ /**
128
+ * A server→client broadcast event (Tier 2 Phase 2.5, spec §10.3). A world declares named events with a
129
+ * payload schema; a `broadcast` rule-effect sends one on the event's NAME (not a fixed ServerMessageType),
130
+ * received via `room.onMessage(name, cb)`. Payload values are the declared fields; a `ref` field serializes
131
+ * to its `playerKey` string on the wire (so `who:"self"` and `who:{aggregate argmax}` are the same type).
132
+ */
133
+ export type BroadcastPayload = Record<string, RoomVarValue>;
134
+ /**
135
+ * Wire message-type names a world's declared event/action names MUST NOT shadow — the fixed client→server
136
+ * and server→client channels. The manifest validator rejects a declared name in this set so a `broadcast`
137
+ * can't collide with `pong` (or confuse the fixed channels). Inlined in @hypersoniclabs/helix-manifest with
138
+ * a cross-ref (the manifest takes no SDK dependency); keep the two in sync.
139
+ */
140
+ export declare const RESERVED_MESSAGE_TYPES: readonly ["state", "ability", "action", "entityState", "entityStateBatch", "ping", "pong"];
141
+ /** Maps each client→server message type to its payload, so senders/handlers stay in lockstep. */
142
+ export interface ClientMessagePayloads {
143
+ [ClientMessageType.State]: StateMessage;
144
+ [ClientMessageType.Ability]: AbilityMessage;
145
+ [ClientMessageType.Action]: ActionMessage;
146
+ [ClientMessageType.EntityState]: EntityStateMessage;
147
+ [ClientMessageType.EntityStateBatch]: EntityStateBatchMessage;
148
+ [ClientMessageType.Ping]: PingMessage;
149
+ }
150
+ /** Maps each server→client message type to its payload. */
151
+ export interface ServerMessagePayloads {
152
+ [ServerMessageType.Pong]: PongMessage;
153
+ }
154
+ /**
155
+ * Per-connection rate ceilings (messages/second) the room enforces; over-rate clients are dropped.
156
+ * State is the throttled input channel (≤~10Hz, plan D3); ability is bursty but bounded.
157
+ */
158
+ export declare const MESSAGE_RATE: {
159
+ readonly stateHz: 10;
160
+ readonly abilityHz: 20;
161
+ readonly actionHz: 20;
162
+ readonly entityStateHz: 10;
163
+ readonly pingHz: 4;
164
+ };
165
+ /**
166
+ * Hard payload bound the room rejects above (defense against oversized frames), measured on the JSON form (a
167
+ * cheap proxy; the msgpack wire is smaller). Sized (P1.B4) to admit a full EntityStateBatch — MAX_ENTITIES_PER_OWNER
168
+ * (128) entities with modest vars in one message (≈75 JSON bytes/entry) — so the byte cap and the per-owner entity
169
+ * cap don't disagree. MUST stay below the Colyseus transport `maxPayload` (16 KB, helix-colyseus-server src/index.ts)
170
+ * in WIRE terms, so the room's reject fires before the transport drops the frame / closes the socket. A var-heavy
171
+ * batch past this is a CLEAN DROP (the room's guard does not strike on an over-byte payload), never a kick.
172
+ */
173
+ export declare const MAX_MESSAGE_BYTES = 12288;