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.
@@ -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
  }
@@ -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;
@@ -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
- const rawReceipt = await platformHandle.worldCharacterSend(request.grantId);
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
- return parseSharedCharacterJson(await platformHandle.worldCharacterCommand(request.grantId));
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
- return parseSharedCharacterJson(await platformHandle.worldCharacterRead(request.grantId));
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 publicInspect() {
256
- if (typeof platformHandle.storage !== "function") {
257
- throw new TypeError("The shared character broker is unavailable.");
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
- if (typeof platformHandle.storage !== "function") {
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.38",
3
+ "version": "0.4.40",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/castle-xyz/castle-experimental-web.git"