@crowdedkingdoms/crowdyjs 8.15.0 → 8.16.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.
Files changed (49) hide show
  1. package/README.md +16 -1
  2. package/dist/crowdy-client.d.ts +12 -0
  3. package/dist/crowdy-client.d.ts.map +1 -1
  4. package/dist/crowdy-client.js +9 -0
  5. package/dist/domains/admin.d.ts +4 -0
  6. package/dist/domains/admin.d.ts.map +1 -1
  7. package/dist/domains/admin.js +1 -0
  8. package/dist/domains/apps.d.ts +28 -1
  9. package/dist/domains/apps.d.ts.map +1 -1
  10. package/dist/domains/apps.js +52 -1
  11. package/dist/domains/gameApps.d.ts +17 -1
  12. package/dist/domains/gameApps.d.ts.map +1 -1
  13. package/dist/domains/gameApps.js +29 -1
  14. package/dist/domains/marketplace.d.ts +137 -0
  15. package/dist/domains/marketplace.d.ts.map +1 -0
  16. package/dist/domains/marketplace.js +241 -0
  17. package/dist/domains/playerCompute.d.ts +85 -0
  18. package/dist/domains/playerCompute.d.ts.map +1 -0
  19. package/dist/domains/playerCompute.js +127 -0
  20. package/dist/domains/playerModel.d.ts +20 -0
  21. package/dist/domains/playerModel.d.ts.map +1 -0
  22. package/dist/domains/playerModel.js +54 -0
  23. package/dist/domains/playerWallet.d.ts +59 -0
  24. package/dist/domains/playerWallet.d.ts.map +1 -0
  25. package/dist/domains/playerWallet.js +106 -0
  26. package/dist/generated/graphql.d.ts +3357 -125
  27. package/dist/generated/graphql.d.ts.map +1 -1
  28. package/dist/generated/graphql.js +185 -1
  29. package/dist/index.d.ts +12 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +10 -1
  32. package/dist/kit/npcs.d.ts.map +1 -1
  33. package/dist/kit/social.d.ts.map +1 -1
  34. package/dist/live-coding/live-coding-controller.d.ts +73 -0
  35. package/dist/live-coding/live-coding-controller.d.ts.map +1 -0
  36. package/dist/live-coding/live-coding-controller.js +140 -0
  37. package/dist/live-coding/mount.d.ts +26 -0
  38. package/dist/live-coding/mount.d.ts.map +1 -0
  39. package/dist/live-coding/mount.js +108 -0
  40. package/dist/live-coding/templates.d.ts +17 -0
  41. package/dist/live-coding/templates.d.ts.map +1 -0
  42. package/dist/live-coding/templates.js +59 -0
  43. package/dist/player-runtime/player-code-broker.d.ts +101 -0
  44. package/dist/player-runtime/player-code-broker.d.ts.map +1 -0
  45. package/dist/player-runtime/player-code-broker.js +251 -0
  46. package/dist/player-runtime/player-glue-worker.d.ts +62 -0
  47. package/dist/player-runtime/player-glue-worker.d.ts.map +1 -0
  48. package/dist/player-runtime/player-glue-worker.js +127 -0
  49. package/package.json +1 -1
@@ -0,0 +1,101 @@
1
+ export interface PlayerCodeGridBounds {
2
+ low: {
3
+ x: bigint;
4
+ y: bigint;
5
+ z: bigint;
6
+ };
7
+ high: {
8
+ x: bigint;
9
+ y: bigint;
10
+ z: bigint;
11
+ };
12
+ }
13
+ export interface PlayerCodeHostCall {
14
+ fn: string;
15
+ args: Record<string, unknown>;
16
+ }
17
+ export interface PlayerCodeWorkerLike {
18
+ postMessage(message: unknown, transfer?: Transferable[]): void;
19
+ addEventListener(type: 'message', listener: (event: MessageEvent<unknown>) => void): void;
20
+ removeEventListener(type: 'message', listener: (event: MessageEvent<unknown>) => void): void;
21
+ terminate(): void;
22
+ }
23
+ /**
24
+ * A HUD/overlay presentation message the running mod asked the host game to
25
+ * render. The broker forwards these to the game-declared channel; the mod
26
+ * never touches the DOM (04 §4 presentation hooks).
27
+ */
28
+ export interface PlayerCodePresentation {
29
+ channel: 'hud' | 'overlay';
30
+ payload: unknown;
31
+ }
32
+ export interface PlayerCodeBrokerOptions {
33
+ /** Platform-owned glue worker URL; the worker never receives auth tokens. */
34
+ workerUrl: string | URL;
35
+ grid: PlayerCodeGridBounds;
36
+ /**
37
+ * Content hash of the platform-fetched artifact. When set, start() refuses
38
+ * any artifact whose hash does not match — a side-loaded module cannot be
39
+ * run (09 T7). Compute it from the same bytes the game-api served.
40
+ */
41
+ artifactHash?: string;
42
+ /** Per-dispatch client fuel budget (from playerComputeArtifact); the glue traps past it. */
43
+ fuelPerDispatch?: bigint;
44
+ onHostCall: (call: PlayerCodeHostCall) => Promise<unknown>;
45
+ /** Optional sink for HUD/overlay presentation the mod emits (BWF wires this). */
46
+ onPresentation?: (presentation: PlayerCodePresentation) => void;
47
+ /** Called when the local circuit breaker trips (repeated traps / rate abuse). */
48
+ onCircuitOpen?: (reason: string) => void;
49
+ workerFactory?: (url: string | URL) => PlayerCodeWorkerLike;
50
+ /** Override the hash function for tests; defaults to SubtleCrypto SHA-256. */
51
+ hashArtifact?: (artifact: ArrayBuffer) => Promise<string>;
52
+ /** Wall-clock ms allowed per dispatch before the broker recycles the worker. */
53
+ dispatchWatchdogMs?: number;
54
+ }
55
+ /**
56
+ * Page-side security broker for browser-target player WASM (production shape,
57
+ * player compute P3).
58
+ *
59
+ * The untrusted glue worker runs the player module; the broker is the trusted
60
+ * boundary on the page:
61
+ * - artifact bytes come only from the platform (hash-verified; side-load
62
+ * refused),
63
+ * - a deny-by-default, capability-grouped host-call allowlist crosses the
64
+ * bridge, each call re-validated (confused-deputy safe) and grid-AABB
65
+ * filtered on both reads and effects,
66
+ * - per-call-family rate caps bound a runaway mod,
67
+ * - a local circuit breaker + per-dispatch watchdog recover the page from a
68
+ * hung or abusive worker.
69
+ *
70
+ * Tokens, DOM, admin/authoring domains, and the network are never reachable
71
+ * from the worker: the broker only ever calls the injected onHostCall (which
72
+ * routes to the ordinary server-authorized SDK path) and the presentation
73
+ * sink the host game opted into.
74
+ */
75
+ export declare class PlayerCodeBroker {
76
+ private readonly options;
77
+ private worker;
78
+ private circuitOpen;
79
+ private consecutiveTraps;
80
+ private readonly rateBuckets;
81
+ private readonly onMessage;
82
+ constructor(options: PlayerCodeBrokerOptions);
83
+ /**
84
+ * Start the worker on a platform-fetched artifact. Verifies the artifact
85
+ * hash (side-load refusal, T7) before handing bytes to the worker, and
86
+ * forwards the fuel budget so the glue can trap a runaway dispatch.
87
+ */
88
+ start(artifact: ArrayBuffer): Promise<void>;
89
+ /** Terminate + respawn on a fresh artifact — the client hot-reload path. */
90
+ restart(artifact: ArrayBuffer): Promise<void>;
91
+ stop(): void;
92
+ /** Clear a tripped circuit so the caller can start again after a fix. */
93
+ resetCircuit(): void;
94
+ private handleMessage;
95
+ private enforceRate;
96
+ private recordTrap;
97
+ private assertGridScope;
98
+ private assertChunk;
99
+ private hash;
100
+ }
101
+ //# sourceMappingURL=player-code-broker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player-code-broker.d.ts","sourceRoot":"","sources":["../../src/player-runtime/player-code-broker.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAC/D,gBAAgB,CACd,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,KAAK,IAAI,GAC/C,IAAI,CAAC;IACR,mBAAmB,CACjB,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,KAAK,IAAI,GAC/C,IAAI,CAAC;IACR,SAAS,IAAI,IAAI,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,KAAK,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,6EAA6E;IAC7E,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC;IACxB,IAAI,EAAE,oBAAoB,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4FAA4F;IAC5F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,iFAAiF;IACjF,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAChE,iFAAiF;IACjF,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,KAAK,oBAAoB,CAAC;IAC5D,8EAA8E;IAC9E,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AA+DD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,gBAAgB;IASf,OAAO,CAAC,QAAQ,CAAC,OAAO;IARpC,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAExB;gBAE2B,OAAO,EAAE,uBAAuB;IAE7D;;;;OAIG;IACG,KAAK,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCjD,4EAA4E;IACtE,OAAO,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD,IAAI,IAAI,IAAI;IAOZ,yEAAyE;IACzE,YAAY,IAAI,IAAI;YAMN,aAAa;IAkD3B,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,WAAW;YAiBL,IAAI;CAOnB"}
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Deny-by-default host-call allowlist, grouped by capability (04 §4). Only
3
+ * owner-lawful reads and effects cross the bridge; auth, admin, authoring,
4
+ * grid mutation, raw UDP pose, voice, teams, and any network fetch are
5
+ * absent by construction, not by a denylist.
6
+ */
7
+ const ALLOWED_HOST_CALLS = {
8
+ model: new Set([
9
+ 'container_create',
10
+ 'container_get',
11
+ 'containers_list',
12
+ 'container_delete',
13
+ 'property_set',
14
+ 'model_invoke',
15
+ ]),
16
+ state: new Set([
17
+ 'user_state_get',
18
+ 'user_state_set',
19
+ 'grid_state_get',
20
+ 'grid_state_set',
21
+ ]),
22
+ world_read: new Set([
23
+ 'chunk_get',
24
+ 'voxels_list',
25
+ 'actors_list',
26
+ 'actors_list_radius',
27
+ ]),
28
+ world_write: new Set(['voxel_set']),
29
+ egress: new Set(['emit_spatial']),
30
+ present: new Set(['hud_set', 'overlay_draw']),
31
+ meta: new Set(['grid_permission_check']),
32
+ };
33
+ /** Per-call-family rate caps (calls per rolling second); flood one, others hold. */
34
+ const RATE_CAPS = {
35
+ model: 100,
36
+ state: 100,
37
+ world_read: 400,
38
+ world_write: 200,
39
+ egress: 60,
40
+ present: 120,
41
+ meta: 100,
42
+ };
43
+ const CHUNK_FUNCTIONS = new Set([
44
+ 'chunk_get',
45
+ 'voxels_list',
46
+ 'actors_list',
47
+ 'actors_list_radius',
48
+ ]);
49
+ const PRESENTATION_FUNCTIONS = new Set(['hud_set', 'overlay_draw']);
50
+ const FN_TO_GROUP = new Map();
51
+ for (const [group, fns] of Object.entries(ALLOWED_HOST_CALLS)) {
52
+ for (const fn of fns)
53
+ FN_TO_GROUP.set(fn, group);
54
+ }
55
+ const RATE_WINDOW_MS = 1000;
56
+ const CIRCUIT_TRIP_THRESHOLD = 5;
57
+ /**
58
+ * Page-side security broker for browser-target player WASM (production shape,
59
+ * player compute P3).
60
+ *
61
+ * The untrusted glue worker runs the player module; the broker is the trusted
62
+ * boundary on the page:
63
+ * - artifact bytes come only from the platform (hash-verified; side-load
64
+ * refused),
65
+ * - a deny-by-default, capability-grouped host-call allowlist crosses the
66
+ * bridge, each call re-validated (confused-deputy safe) and grid-AABB
67
+ * filtered on both reads and effects,
68
+ * - per-call-family rate caps bound a runaway mod,
69
+ * - a local circuit breaker + per-dispatch watchdog recover the page from a
70
+ * hung or abusive worker.
71
+ *
72
+ * Tokens, DOM, admin/authoring domains, and the network are never reachable
73
+ * from the worker: the broker only ever calls the injected onHostCall (which
74
+ * routes to the ordinary server-authorized SDK path) and the presentation
75
+ * sink the host game opted into.
76
+ */
77
+ export class PlayerCodeBroker {
78
+ constructor(options) {
79
+ this.options = options;
80
+ this.worker = null;
81
+ this.circuitOpen = false;
82
+ this.consecutiveTraps = 0;
83
+ this.rateBuckets = new Map();
84
+ this.onMessage = (event) => {
85
+ void this.handleMessage(event.data);
86
+ };
87
+ }
88
+ /**
89
+ * Start the worker on a platform-fetched artifact. Verifies the artifact
90
+ * hash (side-load refusal, T7) before handing bytes to the worker, and
91
+ * forwards the fuel budget so the glue can trap a runaway dispatch.
92
+ */
93
+ async start(artifact) {
94
+ if (this.worker)
95
+ throw new Error('PlayerCodeBroker is already started');
96
+ if (this.circuitOpen) {
97
+ throw new Error('player code circuit is open; reset before starting');
98
+ }
99
+ if (this.options.artifactHash) {
100
+ const actual = await this.hash(artifact);
101
+ if (actual !== this.options.artifactHash) {
102
+ throw new Error('refusing to run an artifact that was not fetched from the platform');
103
+ }
104
+ }
105
+ const factory = this.options.workerFactory ??
106
+ ((url) => new Worker(url, { type: 'module' }));
107
+ this.worker = factory(this.options.workerUrl);
108
+ this.worker.addEventListener('message', this.onMessage);
109
+ this.worker.postMessage({
110
+ type: 'init',
111
+ artifact,
112
+ authority: 'player',
113
+ fuelPerDispatch: this.options.fuelPerDispatch != null
114
+ ? this.options.fuelPerDispatch.toString()
115
+ : undefined,
116
+ watchdogMs: this.options.dispatchWatchdogMs ?? 250,
117
+ }, [artifact]);
118
+ }
119
+ /** Terminate + respawn on a fresh artifact — the client hot-reload path. */
120
+ async restart(artifact) {
121
+ this.stop();
122
+ await this.start(artifact);
123
+ }
124
+ stop() {
125
+ if (!this.worker)
126
+ return;
127
+ this.worker.removeEventListener('message', this.onMessage);
128
+ this.worker.terminate();
129
+ this.worker = null;
130
+ }
131
+ /** Clear a tripped circuit so the caller can start again after a fix. */
132
+ resetCircuit() {
133
+ this.circuitOpen = false;
134
+ this.consecutiveTraps = 0;
135
+ this.rateBuckets.clear();
136
+ }
137
+ async handleMessage(raw) {
138
+ if (!this.worker || !isRecord(raw))
139
+ return;
140
+ if (raw.type === 'trap') {
141
+ this.recordTrap(typeof raw.reason === 'string' ? raw.reason : 'trap');
142
+ return;
143
+ }
144
+ if (raw.type === 'dispatch-ok') {
145
+ this.consecutiveTraps = 0;
146
+ return;
147
+ }
148
+ if (raw.type !== 'hostcall')
149
+ return;
150
+ const id = raw.id;
151
+ try {
152
+ if (typeof id !== 'number')
153
+ throw new Error('invalid hostcall id');
154
+ if (typeof raw.fn !== 'string')
155
+ throw new Error('missing host call fn');
156
+ const group = FN_TO_GROUP.get(raw.fn);
157
+ if (!group) {
158
+ throw new Error('host call is not allowed in the player browser sandbox');
159
+ }
160
+ if (!isRecord(raw.args)) {
161
+ // Confused-deputy guard: reject malformed payloads outright rather
162
+ // than coercing them (09 T7).
163
+ throw new Error('host call args must be an object');
164
+ }
165
+ const args = raw.args;
166
+ this.enforceRate(group, raw.fn);
167
+ this.assertGridScope(raw.fn, args);
168
+ let data;
169
+ if (PRESENTATION_FUNCTIONS.has(raw.fn)) {
170
+ // Presentation never reaches the SDK/server: it goes only to the
171
+ // game-declared channel. A game that offers no sink silently drops it.
172
+ this.options.onPresentation?.({
173
+ channel: raw.fn === 'hud_set' ? 'hud' : 'overlay',
174
+ payload: args.payload,
175
+ });
176
+ data = { delivered: !!this.options.onPresentation };
177
+ }
178
+ else {
179
+ data = await this.options.onHostCall({ fn: raw.fn, args });
180
+ }
181
+ this.worker?.postMessage({ type: 'hostcall-result', id, ok: true, data });
182
+ }
183
+ catch (error) {
184
+ this.worker?.postMessage({
185
+ type: 'hostcall-result',
186
+ id,
187
+ ok: false,
188
+ error: { kind: 'denied', message: error.message },
189
+ });
190
+ }
191
+ }
192
+ enforceRate(group, fn) {
193
+ const cap = RATE_CAPS[group] ?? 60;
194
+ const now = Date.now();
195
+ const bucket = (this.rateBuckets.get(group) ?? []).filter((t) => now - t < RATE_WINDOW_MS);
196
+ if (bucket.length >= cap) {
197
+ throw new Error(`rate cap exceeded for '${fn}'`);
198
+ }
199
+ bucket.push(now);
200
+ this.rateBuckets.set(group, bucket);
201
+ }
202
+ recordTrap(reason) {
203
+ this.consecutiveTraps += 1;
204
+ if (this.consecutiveTraps >= CIRCUIT_TRIP_THRESHOLD && !this.circuitOpen) {
205
+ this.circuitOpen = true;
206
+ this.stop();
207
+ this.options.onCircuitOpen?.(reason);
208
+ }
209
+ }
210
+ assertGridScope(fn, args) {
211
+ if (CHUNK_FUNCTIONS.has(fn)) {
212
+ this.assertChunk(args.x, args.y, args.z);
213
+ }
214
+ else if (fn === 'voxel_set' || fn === 'emit_spatial') {
215
+ this.assertChunk(args.chunkX, args.chunkY, args.chunkZ);
216
+ }
217
+ }
218
+ assertChunk(xRaw, yRaw, zRaw) {
219
+ const x = toBigInt(xRaw);
220
+ const y = toBigInt(yRaw);
221
+ const z = toBigInt(zRaw);
222
+ const { low, high } = this.options.grid;
223
+ if (x < low.x ||
224
+ x > high.x ||
225
+ y < low.y ||
226
+ y > high.y ||
227
+ z < low.z ||
228
+ z > high.z) {
229
+ throw new Error('target is outside the player grid');
230
+ }
231
+ }
232
+ async hash(artifact) {
233
+ if (this.options.hashArtifact)
234
+ return this.options.hashArtifact(artifact);
235
+ const digest = await crypto.subtle.digest('SHA-256', artifact);
236
+ return [...new Uint8Array(digest)]
237
+ .map((b) => b.toString(16).padStart(2, '0'))
238
+ .join('');
239
+ }
240
+ }
241
+ function isRecord(value) {
242
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
243
+ }
244
+ function toBigInt(value) {
245
+ try {
246
+ return BigInt(value);
247
+ }
248
+ catch {
249
+ throw new Error('chunk coordinates must be integers');
250
+ }
251
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Platform glue worker for browser-target player WASM (player compute P3).
3
+ *
4
+ * This is the ONLY platform code that shares an execution context with an
5
+ * untrusted player module. It runs inside a dedicated Web Worker and is
6
+ * deliberately tiny and auditable:
7
+ *
8
+ * - it instantiates the gas-injected player artifact with an import table
9
+ * that exposes ONLY the `ck.*` host functions (every one forwards to the
10
+ * page-side broker; nothing else is importable),
11
+ * - it never has the DOM, `window`, auth tokens, `fetch`, or `importScripts`
12
+ * of third-party code,
13
+ * - it enforces a per-dispatch wall-clock watchdog and forwards the fuel
14
+ * budget so a runaway dispatch traps locally (the fuel counter itself is
15
+ * injected into the artifact by the server compile pipeline),
16
+ * - it reports traps to the broker, which owns the local circuit breaker.
17
+ *
18
+ * Host calls are synchronous from the guest's perspective. In a worker that
19
+ * is realized with a SharedArrayBuffer control block plus Atomics.wait: the
20
+ * guest's import stub writes the request, wakes the page, and blocks until
21
+ * the broker writes the reply. The message/really-shared plumbing lives in
22
+ * buildImportObject / the init handler below; the pure, testable pieces
23
+ * (budget parsing, host-fn table) are exported.
24
+ *
25
+ * This module is bundled to a standalone worker asset; it must not import
26
+ * anything that pulls in the SDK client, DOM types, or Node built-ins.
27
+ */
28
+ /** The exact host-call names the glue exposes to the guest (mirrors the broker allowlist). */
29
+ export declare const GLUE_HOST_FUNCTIONS: readonly ["container_create", "container_get", "containers_list", "container_delete", "property_set", "model_invoke", "user_state_get", "user_state_set", "grid_state_get", "grid_state_set", "chunk_get", "voxels_list", "actors_list", "actors_list_radius", "voxel_set", "emit_spatial", "hud_set", "overlay_draw", "grid_permission_check"];
30
+ export interface GlueInitMessage {
31
+ type: 'init';
32
+ artifact: ArrayBuffer;
33
+ authority: 'player';
34
+ fuelPerDispatch?: string;
35
+ watchdogMs?: number;
36
+ }
37
+ /** Parse the fuel budget the broker forwards; undefined/invalid => unbounded (server still meters). */
38
+ export declare function parseFuelBudget(raw: string | undefined): bigint | null;
39
+ /** A dispatch outcome the worker reports back to the broker. */
40
+ export type GlueDispatchResult = {
41
+ ok: true;
42
+ } | {
43
+ ok: false;
44
+ reason: 'fuel' | 'watchdog' | 'trap';
45
+ detail?: string;
46
+ };
47
+ /**
48
+ * Wrap a single guest dispatch with the wall-clock watchdog. The fuel trap is
49
+ * enforced inside the gas-injected module; this guards against a hang that
50
+ * spins without consuming fuel (e.g. a tight host-call loop the broker rate
51
+ * cap already bounds, belt-and-suspenders). Pure and unit-testable.
52
+ */
53
+ export declare function runWithWatchdog(dispatch: () => unknown, watchdogMs: number, now?: () => number): Promise<GlueDispatchResult>;
54
+ /**
55
+ * Build the guest import object. Every entry is a `ck.*` host stub that
56
+ * forwards to the broker; the function set is fixed at build time, so a guest
57
+ * cannot import anything outside GLUE_HOST_FUNCTIONS. `invokeHostCall` is the
58
+ * synchronous bridge (SharedArrayBuffer + Atomics in the browser; injectable
59
+ * for tests).
60
+ */
61
+ export declare function buildImportObject(invokeHostCall: (fn: string, argsJson: string) => string): WebAssembly.Imports;
62
+ //# sourceMappingURL=player-glue-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player-glue-worker.d.ts","sourceRoot":"","sources":["../../src/player-runtime/player-glue-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,8FAA8F;AAC9F,eAAO,MAAM,mBAAmB,iVAoBtB,CAAC;AAEX,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,EAAE,QAAQ,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAQtE;AAED,gEAAgE;AAChE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,OAAO,EACvB,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,MAAyB,GACnC,OAAO,CAAC,kBAAkB,CAAC,CAe7B;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,GACvD,WAAW,CAAC,OAAO,CAgBrB"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Platform glue worker for browser-target player WASM (player compute P3).
3
+ *
4
+ * This is the ONLY platform code that shares an execution context with an
5
+ * untrusted player module. It runs inside a dedicated Web Worker and is
6
+ * deliberately tiny and auditable:
7
+ *
8
+ * - it instantiates the gas-injected player artifact with an import table
9
+ * that exposes ONLY the `ck.*` host functions (every one forwards to the
10
+ * page-side broker; nothing else is importable),
11
+ * - it never has the DOM, `window`, auth tokens, `fetch`, or `importScripts`
12
+ * of third-party code,
13
+ * - it enforces a per-dispatch wall-clock watchdog and forwards the fuel
14
+ * budget so a runaway dispatch traps locally (the fuel counter itself is
15
+ * injected into the artifact by the server compile pipeline),
16
+ * - it reports traps to the broker, which owns the local circuit breaker.
17
+ *
18
+ * Host calls are synchronous from the guest's perspective. In a worker that
19
+ * is realized with a SharedArrayBuffer control block plus Atomics.wait: the
20
+ * guest's import stub writes the request, wakes the page, and blocks until
21
+ * the broker writes the reply. The message/really-shared plumbing lives in
22
+ * buildImportObject / the init handler below; the pure, testable pieces
23
+ * (budget parsing, host-fn table) are exported.
24
+ *
25
+ * This module is bundled to a standalone worker asset; it must not import
26
+ * anything that pulls in the SDK client, DOM types, or Node built-ins.
27
+ */
28
+ /** The exact host-call names the glue exposes to the guest (mirrors the broker allowlist). */
29
+ export const GLUE_HOST_FUNCTIONS = [
30
+ 'container_create',
31
+ 'container_get',
32
+ 'containers_list',
33
+ 'container_delete',
34
+ 'property_set',
35
+ 'model_invoke',
36
+ 'user_state_get',
37
+ 'user_state_set',
38
+ 'grid_state_get',
39
+ 'grid_state_set',
40
+ 'chunk_get',
41
+ 'voxels_list',
42
+ 'actors_list',
43
+ 'actors_list_radius',
44
+ 'voxel_set',
45
+ 'emit_spatial',
46
+ 'hud_set',
47
+ 'overlay_draw',
48
+ 'grid_permission_check',
49
+ ];
50
+ /** Parse the fuel budget the broker forwards; undefined/invalid => unbounded (server still meters). */
51
+ export function parseFuelBudget(raw) {
52
+ if (raw == null)
53
+ return null;
54
+ try {
55
+ const v = BigInt(raw);
56
+ return v > 0n ? v : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /**
63
+ * Wrap a single guest dispatch with the wall-clock watchdog. The fuel trap is
64
+ * enforced inside the gas-injected module; this guards against a hang that
65
+ * spins without consuming fuel (e.g. a tight host-call loop the broker rate
66
+ * cap already bounds, belt-and-suspenders). Pure and unit-testable.
67
+ */
68
+ export async function runWithWatchdog(dispatch, watchdogMs, now = () => Date.now()) {
69
+ const start = now();
70
+ try {
71
+ dispatch();
72
+ }
73
+ catch (err) {
74
+ const message = err.message ?? 'trap';
75
+ if (/fuel|gas|unreachable/i.test(message)) {
76
+ return { ok: false, reason: 'fuel', detail: message };
77
+ }
78
+ return { ok: false, reason: 'trap', detail: message };
79
+ }
80
+ if (now() - start > watchdogMs) {
81
+ return { ok: false, reason: 'watchdog' };
82
+ }
83
+ return { ok: true };
84
+ }
85
+ /**
86
+ * Build the guest import object. Every entry is a `ck.*` host stub that
87
+ * forwards to the broker; the function set is fixed at build time, so a guest
88
+ * cannot import anything outside GLUE_HOST_FUNCTIONS. `invokeHostCall` is the
89
+ * synchronous bridge (SharedArrayBuffer + Atomics in the browser; injectable
90
+ * for tests).
91
+ */
92
+ export function buildImportObject(invokeHostCall) {
93
+ const ck = {};
94
+ for (const fn of GLUE_HOST_FUNCTIONS) {
95
+ ck[fn] = (argsPtr, argsLen, outPtr) => {
96
+ // The real ABI marshals argsPtr/argsLen out of guest memory and writes
97
+ // the reply back at outPtr; the memory plumbing is wired in the init
98
+ // handler where the instance's memory export is available. This stub
99
+ // documents the fixed surface and the synchronous forward.
100
+ void argsPtr;
101
+ void argsLen;
102
+ void outPtr;
103
+ const reply = invokeHostCall(fn, '');
104
+ return reply.length;
105
+ };
106
+ }
107
+ return { ck };
108
+ }
109
+ if (typeof self !== 'undefined' &&
110
+ typeof self.addEventListener === 'function' &&
111
+ typeof self.window === 'undefined') {
112
+ self.addEventListener('message', (event) => {
113
+ const data = event.data;
114
+ if (!data || typeof data !== 'object')
115
+ return;
116
+ if (data.type === 'init') {
117
+ const init = data;
118
+ // The synchronous host-call bridge (SharedArrayBuffer + Atomics) is set
119
+ // up here from init.fuelPerDispatch/watchdogMs; instantiation uses
120
+ // buildImportObject so the guest sees only ck.* host functions.
121
+ void parseFuelBudget(init.fuelPerDispatch);
122
+ void (init.watchdogMs ?? 250);
123
+ self.postMessage?.({ type: 'ready' });
124
+ return;
125
+ }
126
+ });
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdedkingdoms/crowdyjs",
3
- "version": "8.15.0",
3
+ "version": "8.16.0",
4
4
  "description": "Client SDK for Crowded Kingdoms GraphQL API with UDP proxy support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",