@helix3/helix-sdk 0.1.1-helix3.20

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,334 @@
1
+ // Helix.multiplayer — the client half of HELIX multiplayer (plan pillar D). Wraps the official
2
+ // Colyseus browser client (@colyseus/sdk, schema v4 — the renamed colyseus.js, paired with the 0.17
3
+ // room server) so world authors never import it directly. Flow: POST the platform
4
+ // /api/v1/instant-worlds/:worldId/join with the world_session token → { wsUrl, roomName, credential }
5
+ // → Client.joinOrCreate(token) → a curated HelixRoom handle. The engine NetworkDriver consumes the
6
+ // room state; this module owns the connect handshake, the throttled seq-tagged send path (D3), the
7
+ // reconciliation buffer (D4), and teardown/reconnection wiring (D5).
8
+ import { ClientMessageType, ServerMessageType, MESSAGE_RATE, CONTRACT_VERSION, } from './multiplayer-contract';
9
+ // ~12s of inputs at 10Hz — generous reconciliation window; bounded so a missing server ack can't grow it forever.
10
+ const INPUT_BUFFER_MAX = 120;
11
+ // sessionStorage key (per world) for the colyseus reconnectionToken + its wsUrl. sessionStorage is per-tab and
12
+ // SURVIVES A RELOAD but not a tab close — exactly the lifetime we want: a reload resumes the held seat; a new
13
+ // tab / post-close start does a fresh join. This is what makes a reload RECONNECT (bypassing the room's
14
+ // onAuth one-seat-per-account gate, which would otherwise reject the reload's fresh join during the grace
15
+ // window) instead of falling back to single-player.
16
+ const RECONNECT_KEY = (worldId) => `helix:mp:reconnect:${worldId}`;
17
+ export class HelixMultiplayer {
18
+ host;
19
+ apiBaseUrl = null;
20
+ room = null;
21
+ callbacks = null;
22
+ seq = 0;
23
+ entitySeq = {}; // per-entity monotonic upload seq (4.6c owner-entity channel)
24
+ entityBatchSeq = 0; // per-connection monotonic batch seq (4.10 owner-entity batch channel)
25
+ pendingInput = null;
26
+ flushTimer = null;
27
+ // The clamped player-seat flush cadence (Hz), set on join from JoinRoomOptions.uploadHz. Default = stateHz (10).
28
+ effectiveUploadHz = MESSAGE_RATE.stateHz;
29
+ inputBuffer = [];
30
+ worldId = null; // the joined world (the reconnect-token storage key)
31
+ // A2 (spec §9) engine consumers of the reconnection acks — set via the handle's onStateAck/onEntityStateAck.
32
+ onStateAckCb = null;
33
+ onEntityStateAckCb = null;
34
+ // Opt-in netlog accumulators (1 s windows) — the SOURCE cadence feeding the engine interpolators: the throttled
35
+ // upload (flush) rate and the server patch-arrival rate. Off unless globalThis.__HELIX_NETLOG__ is a sink fn.
36
+ nlSend = { last: 0, count: 0, win: 0, maxGap: 0 };
37
+ nlPatch = { last: 0, count: 0, win: 0, maxGap: 0 };
38
+ constructor(host) {
39
+ this.host = host;
40
+ }
41
+ // Pin the platform API base once (local dev / staging where the token issuer isn't the live host).
42
+ configure(options) {
43
+ this.apiBaseUrl = options.apiBaseUrl.replace(/\/$/, '');
44
+ }
45
+ // The API base other SDK modules should use for direct platform calls (Helix.avatar): the pinned
46
+ // configure() value, else the world_session token's `iss`. Null → the caller treats it as "no API".
47
+ resolveApiBase(token) {
48
+ return this.apiBaseUrl ?? (token !== null ? issuerOf(token) : null);
49
+ }
50
+ // Join (or create) this world's room. worldId defaults to the current world. Requires login: a guest
51
+ // has no world_session and can't reach /join — multiplayer is logged-in only (single-player for guests).
52
+ async joinRoom(worldId, options = {}) {
53
+ if (!this.host.isInitialized())
54
+ throw new Error('Helix: call Helix.init() before Helix.multiplayer.joinRoom()');
55
+ if (this.room)
56
+ throw new Error('Helix.multiplayer: already in a room — call leave() first');
57
+ const token = this.host.getToken();
58
+ if (!token)
59
+ throw new Error('Helix.multiplayer: multiplayer requires login (no session) — call Helix.auth.requestLogin() first');
60
+ const id = worldId ?? this.host.getWorldId();
61
+ if (!id)
62
+ throw new Error('Helix.multiplayer: no world id — pass joinRoom(worldId) when running outside a HELIX shell');
63
+ const apiBase = options.apiBaseUrl?.replace(/\/$/, '') ?? this.resolveApiBase(token);
64
+ if (!apiBase)
65
+ throw new Error('Helix.multiplayer: cannot resolve the API base URL — call Helix.multiplayer.configure({ apiBaseUrl })');
66
+ const colyseus = (await import('@colyseus/sdk'));
67
+ // RESUME FIRST: on a reload, a seat persisted in sessionStorage is still held in the server's grace window —
68
+ // reconnect to it (resumes the same seat + its vars, bypassing onAuth) instead of a fresh join that onAuth
69
+ // would reject as a duplicate. On any failure (grace expired / clean despawn / new tab) fall back to /join.
70
+ let room = await this.tryReconnect(colyseus, id);
71
+ if (!room) {
72
+ const reservation = await this.requestJoin(apiBase, id, token);
73
+ const client = new colyseus.Client(reservation.wsUrl);
74
+ // buildId is the matchmaking filter (room filterBy(['buildId'])) — it groups players per active build.
75
+ // The room re-validates it against the verified credential in onAuth, so a spoofed value can't cross builds.
76
+ // contractVersion is the wire-protocol version baked into THIS bundle's SDK — onAuth logs a mismatch against
77
+ // the room's version (diagnostics for cross-version play) but never rejects, since old bundles are frozen.
78
+ room = await client.joinOrCreate(reservation.roomName, { token: reservation.credential, buildId: reservation.buildId, contractVersion: CONTRACT_VERSION });
79
+ this.persistReconnect(id, reservation.wsUrl, room.reconnectionToken);
80
+ }
81
+ this.worldId = id;
82
+ this.room = room;
83
+ this.callbacks = colyseus.getStateCallbacks(room);
84
+ // Resolve the player-seat upload cadence from the world's uploadHz tier, clamped to the contract's [10,20] range.
85
+ this.effectiveUploadHz = Math.max(MESSAGE_RATE.stateHz, Math.min(options.uploadHz ?? MESSAGE_RATE.stateHz, MESSAGE_RATE.maxUploadHz));
86
+ this.startFlush();
87
+ // A2 (spec §9) reconnection acks. Registered ONCE here (not in the handle) so the reconciliation buffer is
88
+ // pruned through the accepted seq whether or not a consumer subscribed — then the divergence correction is
89
+ // handed to the engine consumer (onStateAck/onEntityStateAck). Both fire only when the room's gate rejected
90
+ // an upload, so they're silent in steady state.
91
+ room.onMessage(ServerMessageType.StateAck, (p) => {
92
+ const ack = p;
93
+ this.inputBuffer = this.inputBuffer.filter((m) => m.seq > ack.seq);
94
+ this.onStateAckCb?.(ack);
95
+ });
96
+ room.onMessage(ServerMessageType.EntityStateAck, (p) => this.onEntityStateAckCb?.(p));
97
+ // netlog (opt-in): measure server patch-arrival cadence — the rate at which synced state changes feed the
98
+ // engine interpolators. A jittery patch rate here is the upstream cause of an interp starve/lapse.
99
+ room.onStateChange(() => this.nlOnPatch());
100
+ // Keep the persisted token fresh as colyseus rotates it (e.g. after an in-page auto-reconnect).
101
+ room.onReconnect(() => this.persistReconnect(id, this.reconnectWsUrl(id), room.reconnectionToken));
102
+ room.onLeave(() => this.teardown());
103
+ return this.makeHandle(room);
104
+ }
105
+ async requestJoin(apiBase, worldId, token) {
106
+ const res = await fetch(`${apiBase}/api/v1/instant-worlds/${encodeURIComponent(worldId)}/join`, {
107
+ method: 'POST',
108
+ headers: { authorization: `Bearer ${token}` },
109
+ });
110
+ if (!res.ok) {
111
+ const detail = await res.json().catch(() => null);
112
+ const message = (detail && typeof detail.message === 'string' && detail.message) || `HTTP ${res.status}`;
113
+ throw new Error(`Helix.multiplayer: join failed — ${message}`);
114
+ }
115
+ return (await res.json());
116
+ }
117
+ makeHandle(room) {
118
+ const collection = (name) => {
119
+ if (!this.callbacks)
120
+ throw new Error('Helix.multiplayer: room not ready');
121
+ return this.callbacks(room.state)[name];
122
+ };
123
+ return {
124
+ roomId: room.roomId,
125
+ sessionId: room.sessionId,
126
+ effectiveUploadHz: this.effectiveUploadHz,
127
+ get state() {
128
+ return room.state;
129
+ },
130
+ onStateChange: (cb) => void room.onStateChange((s) => cb(s)),
131
+ onAdd: (name, cb) => void collection(name).onAdd((v, k) => cb(v, k), true),
132
+ onRemove: (name, cb) => void collection(name).onRemove((v, k) => cb(v, k)),
133
+ onMessage: (type, cb) => void room.onMessage(type, (p) => cb(p)),
134
+ onStateAck: (cb) => {
135
+ this.onStateAckCb = cb;
136
+ },
137
+ onEntityStateAck: (cb) => {
138
+ this.onEntityStateAckCb = cb;
139
+ },
140
+ sendState: (input) => {
141
+ this.pendingInput = input;
142
+ },
143
+ sendAbility: (ability, active) => room.send(ClientMessageType.Ability, { ability, active }),
144
+ sendAction: (name, args) => room.send(ClientMessageType.Action, { name, args }),
145
+ uploadEntity: (entityId, input) => {
146
+ const seq = (this.entitySeq[entityId] = (this.entitySeq[entityId] ?? 0) + 1);
147
+ // (P1.C1) forward the authority epoch (when the caller has it) so the server fence can drop a stale-era frame.
148
+ // networked-physics: a physics kind also carries velocity/angularVelocity/orientation (omitted for kinematic).
149
+ room.send(ClientMessageType.EntityState, {
150
+ entity: entityId, seq, position: input.position, vars: input.vars,
151
+ ...(input.velocity !== undefined ? { velocity: input.velocity } : {}),
152
+ ...(input.angularVelocity !== undefined ? { angularVelocity: input.angularVelocity } : {}),
153
+ ...(input.orientation !== undefined ? { orientation: input.orientation } : {}),
154
+ ...(input.epoch !== undefined ? { epoch: input.epoch } : {}), tMs: performance.now(),
155
+ });
156
+ },
157
+ uploadEntities: (entities) => {
158
+ if (entities.length === 0)
159
+ return;
160
+ const seq = ++this.entityBatchSeq;
161
+ const states = entities.map((e) => ({
162
+ e: e.id,
163
+ p: [e.position.x, e.position.y, e.position.z],
164
+ // networked-physics: compact lv/av/q tuples for a physics kind (omitted when the caller passes none).
165
+ ...(e.velocity !== undefined ? { lv: [e.velocity.x, e.velocity.y, e.velocity.z] } : {}),
166
+ ...(e.angularVelocity !== undefined ? { av: [e.angularVelocity.x, e.angularVelocity.y, e.angularVelocity.z] } : {}),
167
+ ...(e.orientation !== undefined ? { q: [e.orientation.x, e.orientation.y, e.orientation.z, e.orientation.w] } : {}),
168
+ ...(e.vars !== undefined ? { v: e.vars } : {}),
169
+ ...(e.epoch !== undefined ? { ep: e.epoch } : {}),
170
+ }));
171
+ room.send(ClientMessageType.EntityStateBatch, { seq, states, tMs: performance.now() });
172
+ },
173
+ pendingInputs: () => this.inputBuffer.slice(),
174
+ acknowledge: (seq) => {
175
+ this.inputBuffer = this.inputBuffer.filter((m) => m.seq > seq);
176
+ },
177
+ onDrop: (cb) => void room.onDrop(() => cb()),
178
+ onReconnect: (cb) => void room.onReconnect(() => cb()),
179
+ onLeave: (cb) => void room.onLeave((code) => cb(code)),
180
+ leave: () => this.leave(),
181
+ };
182
+ }
183
+ // D3: coalesce per-frame sendState calls and flush the latest at most stateHz times/second, tagging a
184
+ // monotonic seq. D4: buffer each sent state (bounded) so the engine can replay the unacknowledged tail.
185
+ startFlush() {
186
+ const periodMs = Math.round(1000 / this.effectiveUploadHz);
187
+ this.flushTimer = setInterval(() => this.flush(), periodMs);
188
+ }
189
+ flush() {
190
+ if (!this.pendingInput || !this.room)
191
+ return;
192
+ // tMs = the send-time stamp (monotonic performance.now()) the server sanitizes into PlayerState.posT for
193
+ // source-time interpolation. Stamped at flush ≈ sample time (pendingInput is overwritten every frame, so the
194
+ // latest is ~one frame old); consecutive tMs deltas track the real send cadence → jitter-free keyframe spacing.
195
+ const message = { ...this.pendingInput, seq: ++this.seq, tMs: performance.now() };
196
+ this.pendingInput = null;
197
+ this.room.send(ClientMessageType.State, message);
198
+ this.inputBuffer.push(message);
199
+ if (this.inputBuffer.length > INPUT_BUFFER_MAX)
200
+ this.inputBuffer.shift();
201
+ if (netSink())
202
+ this.nlTick(this.nlSend, `player-upload (this client → server state, ≤${this.effectiveUploadHz}Hz, ~${Math.round(1000 / this.effectiveUploadHz)}ms target)`);
203
+ }
204
+ // netlog: roll a 1 s window of an event cadence (send / patch) and emit count + max inter-event gap. Off (no
205
+ // sink) → not called. Diagnostic only.
206
+ nlOnPatch() {
207
+ if (netSink())
208
+ this.nlTick(this.nlPatch, 'server-patches (server → this client state deltas — the interp source)');
209
+ }
210
+ nlTick(a, label) {
211
+ const sink = netSink();
212
+ if (!sink)
213
+ return;
214
+ const now = performance.now();
215
+ if (a.last)
216
+ a.maxGap = Math.max(a.maxGap, now - a.last);
217
+ a.last = now;
218
+ a.count += 1;
219
+ if (!a.win)
220
+ a.win = now;
221
+ if (now - a.win >= 1000) {
222
+ sink(`sdk ${label}: ${a.count}/s gapMax=${a.maxGap | 0}ms`);
223
+ a.count = 0;
224
+ a.win = now;
225
+ a.maxGap = 0;
226
+ }
227
+ }
228
+ // D5 RECONNECTION (sessionStorage-backed): persist / read / clear the colyseus reconnectionToken + its wsUrl
229
+ // per world. sessionStorage is per-tab and survives a RELOAD but not a close — so a reload resumes the held
230
+ // seat (below) while a new tab does a fresh join. We deliberately do NOT consent-leave on tab unload anymore:
231
+ // letting the socket drop holds the seat in the server's grace window so a reload can RECONNECT to it (and
232
+ // keep its vars), instead of a fresh join that the room's one-seat-per-account onAuth gate would reject. A
233
+ // genuine close just lets the grace window expire (the seat despawns after RECONNECT_GRACE_SEC).
234
+ persistReconnect(worldId, wsUrl, reconnectionToken) {
235
+ if (typeof sessionStorage === 'undefined' || !wsUrl || !reconnectionToken)
236
+ return;
237
+ try {
238
+ sessionStorage.setItem(RECONNECT_KEY(worldId), JSON.stringify({ wsUrl, reconnectionToken }));
239
+ }
240
+ catch { /* storage unavailable */ }
241
+ }
242
+ readReconnect(worldId) {
243
+ if (typeof sessionStorage === 'undefined')
244
+ return null;
245
+ try {
246
+ const raw = sessionStorage.getItem(RECONNECT_KEY(worldId));
247
+ const v = raw ? JSON.parse(raw) : null;
248
+ return v?.wsUrl && v.reconnectionToken ? { wsUrl: v.wsUrl, reconnectionToken: v.reconnectionToken } : null;
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ }
254
+ reconnectWsUrl(worldId) {
255
+ return this.readReconnect(worldId)?.wsUrl ?? '';
256
+ }
257
+ clearReconnect(worldId) {
258
+ if (typeof sessionStorage === 'undefined')
259
+ return;
260
+ try {
261
+ sessionStorage.removeItem(RECONNECT_KEY(worldId));
262
+ }
263
+ catch { /* ignore */ }
264
+ }
265
+ // Resume a grace-held seat (a reload). Returns the resumed room, or null to fall through to a fresh join
266
+ // (no saved token / grace expired / seat despawned / stale wsUrl — reconnect() rejects and we drop the token).
267
+ async tryReconnect(colyseus, worldId) {
268
+ const saved = this.readReconnect(worldId);
269
+ if (!saved)
270
+ return null;
271
+ try {
272
+ const client = new colyseus.Client(saved.wsUrl);
273
+ const room = await client.reconnect(saved.reconnectionToken);
274
+ this.persistReconnect(worldId, saved.wsUrl, room.reconnectionToken); // the token may rotate on resume
275
+ return room;
276
+ }
277
+ catch {
278
+ this.clearReconnect(worldId);
279
+ return null;
280
+ }
281
+ }
282
+ // Leave for good: consented (frees the seat immediately, no grace) + drop the saved token so we don't try to
283
+ // resume a seat we deliberately left. (A reload does NOT call this — it just unloads.)
284
+ async leave() {
285
+ const room = this.room;
286
+ if (this.worldId)
287
+ this.clearReconnect(this.worldId);
288
+ this.teardown();
289
+ if (room)
290
+ await room.leave(true);
291
+ }
292
+ // Reset in-memory state. Called on a drop too (onLeave) — it does NOT clear the saved reconnect token, so a
293
+ // later reload can still resume the grace-held seat.
294
+ teardown() {
295
+ if (this.flushTimer)
296
+ clearInterval(this.flushTimer);
297
+ this.flushTimer = null;
298
+ this.room = null;
299
+ this.callbacks = null;
300
+ this.pendingInput = null;
301
+ this.inputBuffer = [];
302
+ this.worldId = null;
303
+ this.seq = 0;
304
+ this.entitySeq = {};
305
+ this.entityBatchSeq = 0;
306
+ this.onStateAckCb = null;
307
+ this.onEntityStateAckCb = null;
308
+ }
309
+ }
310
+ // netlog sink lookup (opt-in): the host installs globalThis.__HELIX_NETLOG__ = (msg) => ... to turn on the
311
+ // message/movement/interpolation diagnostics. Unset → a cheap typeof check, no logging. Shared shape with the
312
+ // engine's TransformInterpolator sink so one toggle lights up the whole pipeline.
313
+ function netSink() {
314
+ const s = globalThis.__HELIX_NETLOG__;
315
+ return typeof s === 'function' ? s : undefined;
316
+ }
317
+ // Read the `iss` claim from a JWT without verifying (the world_session issuer is the platform API
318
+ // origin — the backend sets iss = PUBLIC_API_BASE_URL on every world_session). Verification isn't this
319
+ // side's job; the room verifies the credential against JWKS. Returns null if there's no usable http(s)
320
+ // issuer — in which case the world must configure({ apiBaseUrl }).
321
+ function issuerOf(token) {
322
+ const segment = token.split('.')[1];
323
+ if (!segment)
324
+ return null;
325
+ try {
326
+ const json = atob(segment.replace(/-/g, '+').replace(/_/g, '/'));
327
+ const iss = JSON.parse(json).iss;
328
+ return typeof iss === 'string' && /^https?:\/\//.test(iss) ? iss.replace(/\/$/, '') : null;
329
+ }
330
+ catch {
331
+ return null;
332
+ }
333
+ }
334
+ //# sourceMappingURL=multiplayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multiplayer.js","sourceRoot":"","sources":["../src/multiplayer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,219 @@
1
+ export declare const PROTOCOL_VERSION = 2;
2
+ export type HelixUser = {
3
+ id: string;
4
+ username: string;
5
+ displayName: string | null;
6
+ };
7
+ export type HelixWorldContext = {
8
+ id: string;
9
+ slug: string;
10
+ title: string;
11
+ };
12
+ export type HelixSession = {
13
+ token: string;
14
+ expiresAt: string;
15
+ scopes: string[];
16
+ user: HelixUser;
17
+ };
18
+ export type DebugLogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';
19
+ export type DebugLogEntry = {
20
+ level: DebugLogLevel;
21
+ args: string[];
22
+ t: number;
23
+ };
24
+ export type Balance = {
25
+ lix: number;
26
+ coins: number;
27
+ updatedAt?: string;
28
+ };
29
+ export type PurchaseStatus = 'Pending' | 'Granted' | 'Claimed' | 'AlreadyOwned' | 'InsufficientFunds' | 'MaxPerUserReached' | 'ProductInactive' | 'ProductNotFound' | 'NotInWorld' | 'Unauthorized' | 'RateLimited' | 'Cancelled' | 'Failed' | 'Timeout' | 'NotFound';
30
+ export type PurchaseResult = {
31
+ status: PurchaseStatus;
32
+ completed: boolean;
33
+ reason?: string;
34
+ itemId?: string | null;
35
+ balanceAfterLix?: number;
36
+ priceLix?: number;
37
+ balanceLix?: number;
38
+ shortfallLix?: number;
39
+ ownedCount?: number;
40
+ maxPerUser?: number | null;
41
+ };
42
+ export type InventoryItem = {
43
+ id?: string;
44
+ itemId: string;
45
+ title: string;
46
+ kind?: string;
47
+ category?: string;
48
+ subCategory?: string | null;
49
+ quantity: number;
50
+ description?: string | null;
51
+ thumbnailUrl?: string | null;
52
+ equipped?: boolean;
53
+ avatarSlot?: string | null;
54
+ socket?: string | null;
55
+ metadata?: Record<string, unknown>;
56
+ };
57
+ export type EquippedAvatar = {
58
+ source: 'equipped' | 'auto' | 'default';
59
+ inventoryItemId?: string | null;
60
+ itemId: string | null;
61
+ glbUrl: string | null;
62
+ skeleton: string | null;
63
+ };
64
+ export type AvatarBaseShape = 'male' | 'female' | 'custom';
65
+ export type AvatarLoadoutSlot = {
66
+ slot: string;
67
+ itemId: string;
68
+ inventoryItemId: string;
69
+ socket?: string | null;
70
+ inventoryItem: InventoryItem;
71
+ };
72
+ export type UniversalAvatarBody = {
73
+ mode: 'preset';
74
+ baseMeshId: 'helix:humanoid:male' | 'helix:humanoid:female';
75
+ } | {
76
+ mode: 'customMesh';
77
+ avatarItemId: string;
78
+ inventoryItemId: string;
79
+ glbUrl: string | null;
80
+ skeleton: string | null;
81
+ };
82
+ export type AvatarLoadout = {
83
+ version: 1;
84
+ body: UniversalAvatarBody;
85
+ baseShape: AvatarBaseShape;
86
+ customAvatar: EquippedAvatar;
87
+ equippedSlots: AvatarLoadoutSlot[];
88
+ inventory: InventoryItem[];
89
+ updatedAt: string;
90
+ };
91
+ export type AvatarLoadoutPatch = {
92
+ baseShape?: AvatarBaseShape;
93
+ customAvatarInventoryItemId?: string | null;
94
+ equippedSlots?: Record<string, string | null>;
95
+ };
96
+ export type MarketplaceListing = {
97
+ itemId: string;
98
+ title: string;
99
+ description?: string | null;
100
+ priceLix: number;
101
+ isFree: boolean;
102
+ thumbnailUrl?: string | null;
103
+ kind?: string;
104
+ category?: string;
105
+ };
106
+ export type MarketplaceQuery = {
107
+ kind?: string;
108
+ category?: string;
109
+ search?: string;
110
+ limit?: number;
111
+ };
112
+ export type CameraAspect = 'portrait' | 'landscape' | 'square' | 'free';
113
+ export type CameraSavePayload = {
114
+ bytes: ArrayBuffer;
115
+ contentType: string;
116
+ caption?: string;
117
+ aspect?: CameraAspect;
118
+ };
119
+ export type SavedPhoto = {
120
+ assetId: string;
121
+ url: string;
122
+ thumbnailUrl: string;
123
+ worldName: string;
124
+ createdAt: string;
125
+ };
126
+ export type PurchaseContext = {
127
+ ref: string;
128
+ title: string;
129
+ description: string | null;
130
+ thumbnailUrl: string | null;
131
+ priceLix: number;
132
+ isFree: boolean;
133
+ balanceLix: number;
134
+ balanceAfterLix: number;
135
+ owned: boolean;
136
+ ownedCount: number;
137
+ maxPerUser: number | null;
138
+ eligible: boolean;
139
+ eligibilityStatus: PurchaseStatus | null;
140
+ };
141
+ export type HelixRequestMethod = 'wallet.getBalance' | 'marketplace.purchase' | 'marketplace.getListings' | 'inventory.hasItem' | 'inventory.getQuantity' | 'inventory.getMyItems' | 'inventory.equipItem' | 'avatar.getEquipped' | 'avatar.getLoadout' | 'avatar.updateLoadout' | 'avatar.openCreator' | 'iwp.getContext' | 'iwp.getPurchase' | 'dataStore.get' | 'dataStore.set' | 'dataStore.delete' | 'dataStore.list' | 'camera.savePhoto';
142
+ export type HelixEvent = 'balance-changed' | 'inventory-changed' | 'avatar-changed' | 'equipment-changed' | 'overlay-open' | 'overlay-closed';
143
+ export type HelixNotificationKind = 'success' | 'failure' | 'notification' | 'message';
144
+ export type HelixNotification = {
145
+ kind: HelixNotificationKind;
146
+ title: string;
147
+ message?: string;
148
+ timeoutMs?: number;
149
+ actionLabel?: string;
150
+ actionHref?: string;
151
+ };
152
+ export type HelixPromptState = 'default' | 'available' | 'locked' | 'busy';
153
+ export type HelixDeviceFormFactor = 'phone' | 'tablet';
154
+ export type HelixInteractionPrompt = {
155
+ id: string;
156
+ title: string;
157
+ description?: string;
158
+ input?: {
159
+ key?: string;
160
+ button?: string;
161
+ label?: string;
162
+ };
163
+ actionLabel?: string;
164
+ state?: HelixPromptState;
165
+ priority?: number;
166
+ };
167
+ export type WorldToShellMessage = {
168
+ type: 'helix:ready';
169
+ protocolVersion: number;
170
+ } | {
171
+ type: 'helix:request-login';
172
+ requestId: string;
173
+ } | {
174
+ type: 'helix:log';
175
+ entry: DebugLogEntry;
176
+ } | {
177
+ type: 'helix:open-device';
178
+ form?: HelixDeviceFormFactor;
179
+ } | {
180
+ type: 'helix:request';
181
+ requestId: string;
182
+ method: HelixRequestMethod;
183
+ payload?: unknown;
184
+ } | {
185
+ type: 'helix:notify';
186
+ notification: HelixNotification;
187
+ } | {
188
+ type: 'helix:set-interaction-prompt';
189
+ prompt: HelixInteractionPrompt;
190
+ } | {
191
+ type: 'helix:clear-interaction-prompt';
192
+ promptId?: string;
193
+ };
194
+ export type ShellToWorldMessage = {
195
+ type: 'helix:init';
196
+ protocolVersion: number;
197
+ world: HelixWorldContext;
198
+ session: HelixSession | null;
199
+ debug?: boolean;
200
+ } | {
201
+ type: 'helix:session';
202
+ session: HelixSession | null;
203
+ } | {
204
+ type: 'helix:login-result';
205
+ requestId: string;
206
+ ok: boolean;
207
+ reason?: 'dismissed' | 'unavailable';
208
+ } | {
209
+ type: 'helix:response';
210
+ requestId: string;
211
+ ok: boolean;
212
+ result?: unknown;
213
+ error?: string;
214
+ } | {
215
+ type: 'helix:event';
216
+ event: HelixEvent;
217
+ data?: unknown;
218
+ };
219
+ export declare function isShellMessage(data: unknown): data is ShellToWorldMessage;
@@ -0,0 +1,25 @@
1
+ // postMessage protocol between a world (sandboxed iframe) and the HELIX shell
2
+ // (the play page, or a local `helix dev` shell). This file IS the wire contract
3
+ // — both sides import it; version any breaking change.
4
+ //
5
+ // v2 is additive on two fronts: (1) debug — helix:init gained `debug`, and helix:log
6
+ // carries world logs to the shell when debug is on; (2) the shell-mediated platform
7
+ // API (wallet / marketplace / inventory) on a generic request/response envelope plus
8
+ // push events. The world SDK is a thin wrapper: it posts a request, the shell (which
9
+ // holds the player's session and renders the purchase popup) settles with the backend
10
+ // and posts the result.
11
+ export const PROTOCOL_VERSION = 2;
12
+ const SHELL_MESSAGE_TYPES = new Set([
13
+ 'helix:init',
14
+ 'helix:session',
15
+ 'helix:login-result',
16
+ 'helix:response',
17
+ 'helix:event',
18
+ ]);
19
+ export function isShellMessage(data) {
20
+ return (typeof data === 'object' &&
21
+ data !== null &&
22
+ typeof data.type === 'string' &&
23
+ SHELL_MESSAGE_TYPES.has(data.type));
24
+ }
25
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,uDAAuD;AACvD,EAAE;AACF,qFAAqF;AACrF,oFAAoF;AACpF,qFAAqF;AACrF,qFAAqF;AACrF,sFAAsF;AACtF,wBAAwB;AAExB,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAqTlC,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,YAAY;IACZ,eAAe;IACf,oBAAoB;IACpB,gBAAgB;IAChB,aAAa;CACd,CAAC,CAAC;AAEH,MAAM,UAAU,cAAc,CAAC,IAAa;IAC1C,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACb,OAAQ,IAA2B,CAAC,IAAI,KAAK,QAAQ;QACrD,mBAAmB,CAAC,GAAG,CAAE,IAAyB,CAAC,IAAI,CAAC,CACzD,CAAC;AACJ,CAAC"}