castle-web-sdk 0.4.38 → 0.4.40
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/dist/multiplayer.d.ts +21 -0
- package/dist/server/platformHandle.d.ts +2 -0
- package/dist/server/wrapper.d.ts +34 -0
- package/dist/server/wrapper.js +195 -9
- package/package.json +1 -1
package/dist/multiplayer.d.ts
CHANGED
|
@@ -90,6 +90,21 @@ export type MultiplayerWorldCharacterQuery = {
|
|
|
90
90
|
limit?: number;
|
|
91
91
|
cursor?: string;
|
|
92
92
|
};
|
|
93
|
+
/** Authenticated, participant-scoped projection of a shared character world. */
|
|
94
|
+
export interface MultiplayerWorldCharacterInspectResult<Gameplay = unknown, SharedState = unknown, PrivateState = unknown, Source = unknown> {
|
|
95
|
+
/** Durable identity used by gameplay holders and promise debtors. */
|
|
96
|
+
participantId: string;
|
|
97
|
+
worldRevision: string;
|
|
98
|
+
gameplay: Gameplay;
|
|
99
|
+
characters: Array<{
|
|
100
|
+
characterId: string;
|
|
101
|
+
npcRevision: string;
|
|
102
|
+
sharedState: SharedState;
|
|
103
|
+
privateState: PrivateState;
|
|
104
|
+
privateRevision: string;
|
|
105
|
+
}>;
|
|
106
|
+
sources: Source[];
|
|
107
|
+
}
|
|
93
108
|
export interface MultiplayerWorldCharacterCommandResult {
|
|
94
109
|
operationId: string;
|
|
95
110
|
status: "applied" | "ineligible";
|
|
@@ -118,6 +133,12 @@ export interface MultiplayerConnection {
|
|
|
118
133
|
operationId: string;
|
|
119
134
|
command: MultiplayerWorldCharacterCommand;
|
|
120
135
|
}): Promise<MultiplayerWorldCharacterCommandResult>;
|
|
136
|
+
readCharacters(request: {
|
|
137
|
+
requestId: string;
|
|
138
|
+
query: {
|
|
139
|
+
type: "inspect";
|
|
140
|
+
};
|
|
141
|
+
}): Promise<MultiplayerWorldCharacterInspectResult>;
|
|
121
142
|
readCharacters<T = unknown>(request: {
|
|
122
143
|
requestId: string;
|
|
123
144
|
query: MultiplayerWorldCharacterQuery;
|
|
@@ -53,6 +53,8 @@ export interface PlatformHandle {
|
|
|
53
53
|
worldCharacterSend?(grantId: string): Promise<string>;
|
|
54
54
|
worldCharacterCommand?(grantId: string): Promise<string>;
|
|
55
55
|
worldCharacterRead?(grantId: string): Promise<string>;
|
|
56
|
+
worldCharacterRuntimeCreate?(argsJson: string): Promise<string>;
|
|
57
|
+
worldCharacterRuntimeList?(): Promise<string>;
|
|
56
58
|
worldCharacterReject?(grantId: string, code?: string): void;
|
|
57
59
|
readonly config: PlatformSessionConfig;
|
|
58
60
|
}
|
package/dist/server/wrapper.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export interface MultiplayerServerWorld {
|
|
|
55
55
|
readonly characters: MultiplayerServerWorldCharacters;
|
|
56
56
|
}
|
|
57
57
|
export interface MultiplayerServerWorldCharacters {
|
|
58
|
+
create(input: MultiplayerWorldRuntimeCharacterCreateInput): Promise<MultiplayerWorldRuntimeCharacterCreateReceipt>;
|
|
59
|
+
list(): Promise<MultiplayerWorldCharacterList>;
|
|
58
60
|
publicInspect<T = unknown>(): Promise<T>;
|
|
59
61
|
publicRelay(input: {
|
|
60
62
|
operationId: string;
|
|
@@ -63,6 +65,38 @@ export interface MultiplayerServerWorldCharacters {
|
|
|
63
65
|
recipientCharacterIds: string[];
|
|
64
66
|
}): Promise<unknown>;
|
|
65
67
|
}
|
|
68
|
+
export interface MultiplayerWorldRuntimeCharacterKnowledge {
|
|
69
|
+
kind: "belief";
|
|
70
|
+
text: string;
|
|
71
|
+
}
|
|
72
|
+
export interface MultiplayerWorldRuntimeCharacterDefinition {
|
|
73
|
+
name: string;
|
|
74
|
+
instructions: string;
|
|
75
|
+
background?: string;
|
|
76
|
+
initialKnowledge?: readonly MultiplayerWorldRuntimeCharacterKnowledge[];
|
|
77
|
+
}
|
|
78
|
+
export interface MultiplayerWorldRuntimeCharacterCreateInput {
|
|
79
|
+
operationId: string;
|
|
80
|
+
definition: MultiplayerWorldRuntimeCharacterDefinition;
|
|
81
|
+
capabilities: {
|
|
82
|
+
actionIds: readonly string[];
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export interface MultiplayerWorldCharacterSummary {
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
source: "published" | "runtime";
|
|
89
|
+
}
|
|
90
|
+
export interface MultiplayerWorldRuntimeCharacterCreateReceipt {
|
|
91
|
+
operationId: string;
|
|
92
|
+
character: MultiplayerWorldCharacterSummary & {
|
|
93
|
+
source: "runtime";
|
|
94
|
+
};
|
|
95
|
+
replayed: boolean;
|
|
96
|
+
}
|
|
97
|
+
export interface MultiplayerWorldCharacterList {
|
|
98
|
+
characters: readonly MultiplayerWorldCharacterSummary[];
|
|
99
|
+
}
|
|
66
100
|
export interface MultiplayerWorldCharacterCommandRequest {
|
|
67
101
|
readonly requestId: string;
|
|
68
102
|
readonly operationId: string;
|
package/dist/server/wrapper.js
CHANGED
|
@@ -49,6 +49,7 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
49
49
|
platformHandle.on("worldCharacterTurn", async (raw) => {
|
|
50
50
|
const request = assertPlatformWorldCharacterTurn(raw);
|
|
51
51
|
let settled = false;
|
|
52
|
+
const brokerFailure = { rejection: null };
|
|
52
53
|
const turn = Object.freeze({
|
|
53
54
|
requestId: request.requestId,
|
|
54
55
|
operationId: request.operationId,
|
|
@@ -64,7 +65,14 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
64
65
|
if (settled)
|
|
65
66
|
throw new TypeError("This shared character request is already settled.");
|
|
66
67
|
settled = true;
|
|
67
|
-
|
|
68
|
+
let rawReceipt;
|
|
69
|
+
try {
|
|
70
|
+
rawReceipt = await platformHandle.worldCharacterSend(request.grantId);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
brokerFailure.rejection = { error };
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
68
76
|
return parseWorldCharacterAdmission(rawReceipt, request.operationId);
|
|
69
77
|
},
|
|
70
78
|
reject(code) {
|
|
@@ -77,6 +85,10 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
77
85
|
try {
|
|
78
86
|
await callbacks.onWorldCharacterTurn(session, turn);
|
|
79
87
|
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (brokerFailure.rejection === null || brokerFailure.rejection.error !== error)
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
80
92
|
finally {
|
|
81
93
|
if (!settled)
|
|
82
94
|
turn.reject();
|
|
@@ -88,6 +100,7 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
88
100
|
platformHandle.on("worldCharacterCommand", async (raw) => {
|
|
89
101
|
const request = assertPlatformWorldCharacterCommand(raw);
|
|
90
102
|
let settled = false;
|
|
103
|
+
const brokerFailure = { rejection: null };
|
|
91
104
|
const command = Object.freeze({
|
|
92
105
|
requestId: request.requestId,
|
|
93
106
|
operationId: request.operationId,
|
|
@@ -97,7 +110,15 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
97
110
|
if (settled)
|
|
98
111
|
throw new TypeError("This shared character command is already settled.");
|
|
99
112
|
settled = true;
|
|
100
|
-
|
|
113
|
+
let rawResult;
|
|
114
|
+
try {
|
|
115
|
+
rawResult = await platformHandle.worldCharacterCommand(request.grantId);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
brokerFailure.rejection = { error };
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
return parseSharedCharacterJson(rawResult);
|
|
101
122
|
},
|
|
102
123
|
reject(code) {
|
|
103
124
|
if (settled)
|
|
@@ -109,6 +130,10 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
109
130
|
try {
|
|
110
131
|
await callbacks.onWorldCharacterCommand(session, command);
|
|
111
132
|
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (brokerFailure.rejection === null || brokerFailure.rejection.error !== error)
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
112
137
|
finally {
|
|
113
138
|
if (!settled)
|
|
114
139
|
command.reject();
|
|
@@ -120,6 +145,7 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
120
145
|
platformHandle.on("worldCharacterRead", async (raw) => {
|
|
121
146
|
const request = assertPlatformWorldCharacterRead(raw);
|
|
122
147
|
let settled = false;
|
|
148
|
+
const brokerFailure = { rejection: null };
|
|
123
149
|
const read = Object.freeze({
|
|
124
150
|
requestId: request.requestId,
|
|
125
151
|
speaker: Object.freeze({ ...request.speaker }),
|
|
@@ -128,7 +154,15 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
128
154
|
if (settled)
|
|
129
155
|
throw new TypeError("This shared character read is already settled.");
|
|
130
156
|
settled = true;
|
|
131
|
-
|
|
157
|
+
let rawResult;
|
|
158
|
+
try {
|
|
159
|
+
rawResult = await platformHandle.worldCharacterRead(request.grantId);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
brokerFailure.rejection = { error };
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
return parseSharedCharacterJson(rawResult);
|
|
132
166
|
},
|
|
133
167
|
reject(code) {
|
|
134
168
|
if (settled)
|
|
@@ -140,6 +174,10 @@ async function bootSession(platformHandle, callbacks) {
|
|
|
140
174
|
try {
|
|
141
175
|
await callbacks.onWorldCharacterRead(session, read);
|
|
142
176
|
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (brokerFailure.rejection === null || brokerFailure.rejection.error !== error)
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
143
181
|
finally {
|
|
144
182
|
if (!settled)
|
|
145
183
|
read.reject();
|
|
@@ -252,16 +290,28 @@ function createServerWorld(platformHandle) {
|
|
|
252
290
|
}
|
|
253
291
|
function createServerWorldCharacters(platformHandle) {
|
|
254
292
|
return Object.freeze({
|
|
255
|
-
async
|
|
256
|
-
if (typeof platformHandle.
|
|
257
|
-
throw new TypeError("The
|
|
293
|
+
async create(input) {
|
|
294
|
+
if (typeof platformHandle.worldCharacterRuntimeCreate !== "function") {
|
|
295
|
+
throw new TypeError("The runtime character broker is unavailable.");
|
|
296
|
+
}
|
|
297
|
+
const operationId = assertWorldOperationId(input?.operationId);
|
|
298
|
+
const definition = assertRuntimeCharacterDefinition(input?.definition);
|
|
299
|
+
const capabilities = assertRuntimeCharacterCapabilities(input?.capabilities);
|
|
300
|
+
const result = parseSharedCharacterJson(await platformHandle.worldCharacterRuntimeCreate(JSON.stringify({ operationId, definition, capabilities })));
|
|
301
|
+
return parseRuntimeCharacterCreateReceipt(result, operationId);
|
|
302
|
+
},
|
|
303
|
+
async list() {
|
|
304
|
+
if (typeof platformHandle.worldCharacterRuntimeList !== "function") {
|
|
305
|
+
throw new TypeError("The runtime character broker is unavailable.");
|
|
258
306
|
}
|
|
307
|
+
return parseRuntimeCharacterList(parseSharedCharacterJson(await platformHandle.worldCharacterRuntimeList()));
|
|
308
|
+
},
|
|
309
|
+
async publicInspect() {
|
|
310
|
+
requireSharedCharacterBroker(platformHandle);
|
|
259
311
|
return parseSharedCharacterJson(await platformHandle.storage("worldCharacterPublicInspect", "{}"));
|
|
260
312
|
},
|
|
261
313
|
async publicRelay(input) {
|
|
262
|
-
|
|
263
|
-
throw new TypeError("The shared character broker is unavailable.");
|
|
264
|
-
}
|
|
314
|
+
requireSharedCharacterBroker(platformHandle);
|
|
265
315
|
const operationId = assertWorldOperationId(input?.operationId);
|
|
266
316
|
const sourceId = assertWorldCharacterId(input?.sourceId, "sourceId", 128);
|
|
267
317
|
const transmittingCharacterId = assertWorldCharacterId(input?.transmittingCharacterId, "transmittingCharacterId");
|
|
@@ -282,6 +332,142 @@ function createServerWorldCharacters(platformHandle) {
|
|
|
282
332
|
},
|
|
283
333
|
});
|
|
284
334
|
}
|
|
335
|
+
const RUNTIME_CHARACTER_MAX_COUNT = 16;
|
|
336
|
+
const RUNTIME_CHARACTER_MAX_KNOWLEDGE = 8;
|
|
337
|
+
const RUNTIME_CHARACTER_PROMPT_MAX_CHARACTERS = 8_000;
|
|
338
|
+
const RUNTIME_CHARACTER_ID = /^runtime_[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
339
|
+
const SAFE_RUNTIME_ACTION_ID = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
340
|
+
const FORBIDDEN_RUNTIME_ACTION_IDS = new Set(["__proto__", "prototype", "constructor"]);
|
|
341
|
+
function requireSharedCharacterBroker(platformHandle) {
|
|
342
|
+
if (typeof platformHandle.storage !== "function") {
|
|
343
|
+
throw new TypeError("The shared character broker is unavailable.");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function assertRuntimeCharacterDefinition(value) {
|
|
347
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
348
|
+
throw new TypeError("session.world.characters.create: definition must be an object.");
|
|
349
|
+
}
|
|
350
|
+
const definition = value;
|
|
351
|
+
if (Object.keys(definition).some((key) => !["name", "instructions", "background", "initialKnowledge"].includes(key))) {
|
|
352
|
+
throw new TypeError("session.world.characters.create: definition contains an unsupported field.");
|
|
353
|
+
}
|
|
354
|
+
const name = assertRuntimeCharacterText(definition.name, "definition.name", 100, 400);
|
|
355
|
+
const instructions = assertRuntimeCharacterText(definition.instructions, "definition.instructions", 8_000, 32_000);
|
|
356
|
+
if (JSON.stringify(instructions).length - 2 > 10_000) {
|
|
357
|
+
throw new TypeError("session.world.characters.create: definition.instructions is too large when serialized.");
|
|
358
|
+
}
|
|
359
|
+
const background = definition.background === undefined
|
|
360
|
+
? undefined
|
|
361
|
+
: assertRuntimeCharacterText(definition.background, "definition.background", 2_000, 8_000);
|
|
362
|
+
let initialKnowledge;
|
|
363
|
+
if (definition.initialKnowledge !== undefined) {
|
|
364
|
+
if (!Array.isArray(definition.initialKnowledge) ||
|
|
365
|
+
definition.initialKnowledge.length > RUNTIME_CHARACTER_MAX_KNOWLEDGE) {
|
|
366
|
+
throw new TypeError(`session.world.characters.create: definition.initialKnowledge may contain at most ${RUNTIME_CHARACTER_MAX_KNOWLEDGE} beliefs.`);
|
|
367
|
+
}
|
|
368
|
+
initialKnowledge = definition.initialKnowledge.map((entry, index) => {
|
|
369
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
370
|
+
throw new TypeError(`session.world.characters.create: definition.initialKnowledge[${index}] must be a belief.`);
|
|
371
|
+
}
|
|
372
|
+
const knowledge = entry;
|
|
373
|
+
if (Object.keys(knowledge).some((key) => key !== "kind" && key !== "text") ||
|
|
374
|
+
knowledge.kind !== "belief") {
|
|
375
|
+
throw new TypeError(`session.world.characters.create: definition.initialKnowledge[${index}] must contain only kind "belief" and text.`);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
kind: "belief",
|
|
379
|
+
text: assertRuntimeCharacterText(knowledge.text, `definition.initialKnowledge[${index}].text`, 500, 2_000),
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const promptCharacters = instructions.length + (background?.length ?? 0) +
|
|
384
|
+
(initialKnowledge?.reduce((sum, item) => sum + item.text.length, 0) ?? 0);
|
|
385
|
+
if (promptCharacters > RUNTIME_CHARACTER_PROMPT_MAX_CHARACTERS) {
|
|
386
|
+
throw new TypeError(`session.world.characters.create: definition prompt fields may total at most ${RUNTIME_CHARACTER_PROMPT_MAX_CHARACTERS} characters.`);
|
|
387
|
+
}
|
|
388
|
+
return Object.freeze({
|
|
389
|
+
name,
|
|
390
|
+
instructions,
|
|
391
|
+
...(background === undefined ? {} : { background }),
|
|
392
|
+
...(initialKnowledge === undefined
|
|
393
|
+
? {}
|
|
394
|
+
: { initialKnowledge: Object.freeze(initialKnowledge.map((item) => Object.freeze(item))) }),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function assertRuntimeCharacterCapabilities(value) {
|
|
398
|
+
if (!value ||
|
|
399
|
+
typeof value !== "object" ||
|
|
400
|
+
Array.isArray(value) ||
|
|
401
|
+
Object.keys(value).some((key) => key !== "actionIds")) {
|
|
402
|
+
throw new TypeError("session.world.characters.create: capabilities must contain only actionIds.");
|
|
403
|
+
}
|
|
404
|
+
const actionIds = value.actionIds;
|
|
405
|
+
if (!Array.isArray(actionIds) || actionIds.length > RUNTIME_CHARACTER_MAX_COUNT) {
|
|
406
|
+
throw new TypeError(`session.world.characters.create: capabilities.actionIds may contain at most ${RUNTIME_CHARACTER_MAX_COUNT} ids.`);
|
|
407
|
+
}
|
|
408
|
+
const normalized = actionIds.map((id, index) => {
|
|
409
|
+
if (typeof id !== "string" ||
|
|
410
|
+
!SAFE_RUNTIME_ACTION_ID.test(id) ||
|
|
411
|
+
FORBIDDEN_RUNTIME_ACTION_IDS.has(id)) {
|
|
412
|
+
throw new TypeError(`session.world.characters.create: capabilities.actionIds[${index}] is invalid.`);
|
|
413
|
+
}
|
|
414
|
+
return id;
|
|
415
|
+
});
|
|
416
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
417
|
+
throw new TypeError("session.world.characters.create: capabilities.actionIds may not contain duplicates.");
|
|
418
|
+
}
|
|
419
|
+
return Object.freeze({ actionIds: Object.freeze([...normalized].sort()) });
|
|
420
|
+
}
|
|
421
|
+
function assertRuntimeCharacterText(value, field, maxCharacters, maxBytes) {
|
|
422
|
+
if (typeof value !== "string" ||
|
|
423
|
+
value.length === 0 ||
|
|
424
|
+
value.length > maxCharacters ||
|
|
425
|
+
value.includes("\0") ||
|
|
426
|
+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(value) ||
|
|
427
|
+
new TextEncoder().encode(value).byteLength > maxBytes) {
|
|
428
|
+
throw new TypeError(`session.world.characters.create: ${field} must be valid non-empty text of at most ${maxCharacters} characters.`);
|
|
429
|
+
}
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
function parseRuntimeCharacterCreateReceipt(value, operationId) {
|
|
433
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
434
|
+
throw new TypeError("The platform returned an invalid runtime character receipt.");
|
|
435
|
+
}
|
|
436
|
+
const receipt = value;
|
|
437
|
+
const character = parseRuntimeCharacterSummary(receipt.character, true);
|
|
438
|
+
if (receipt.operationId !== operationId || typeof receipt.replayed !== "boolean") {
|
|
439
|
+
throw new TypeError("The platform returned an invalid runtime character receipt.");
|
|
440
|
+
}
|
|
441
|
+
return Object.freeze({ operationId, character, replayed: receipt.replayed });
|
|
442
|
+
}
|
|
443
|
+
function parseRuntimeCharacterList(value) {
|
|
444
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
445
|
+
throw new TypeError("The platform returned an invalid runtime character list.");
|
|
446
|
+
}
|
|
447
|
+
const characters = value.characters;
|
|
448
|
+
if (!Array.isArray(characters) || characters.length > RUNTIME_CHARACTER_MAX_COUNT) {
|
|
449
|
+
throw new TypeError("The platform returned an invalid runtime character list.");
|
|
450
|
+
}
|
|
451
|
+
const normalized = characters.map((character) => parseRuntimeCharacterSummary(character));
|
|
452
|
+
if (new Set(normalized.map((character) => character.id)).size !== normalized.length) {
|
|
453
|
+
throw new TypeError("The platform returned an invalid runtime character list.");
|
|
454
|
+
}
|
|
455
|
+
return Object.freeze({ characters: Object.freeze(normalized) });
|
|
456
|
+
}
|
|
457
|
+
function parseRuntimeCharacterSummary(value, requireRuntime = false) {
|
|
458
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
459
|
+
throw new TypeError("The platform returned an invalid runtime character summary.");
|
|
460
|
+
}
|
|
461
|
+
const summary = value;
|
|
462
|
+
const id = assertWorldCharacterId(summary.id, "character.id");
|
|
463
|
+
const name = assertRuntimeCharacterText(summary.name, "character.name", 100, 400);
|
|
464
|
+
if ((summary.source !== "runtime" && summary.source !== "published") ||
|
|
465
|
+
(requireRuntime && summary.source !== "runtime") ||
|
|
466
|
+
(summary.source === "runtime" && !RUNTIME_CHARACTER_ID.test(id))) {
|
|
467
|
+
throw new TypeError("The platform returned an invalid runtime character summary.");
|
|
468
|
+
}
|
|
469
|
+
return Object.freeze({ id, name, source: summary.source });
|
|
470
|
+
}
|
|
285
471
|
function parseWorldSnapshot(value, operation) {
|
|
286
472
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
287
473
|
throw worldError("The platform returned an invalid world checkpoint.", "read");
|