castle-web-sdk 0.4.39 → 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.
@@ -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;
@@ -290,16 +290,28 @@ function createServerWorld(platformHandle) {
290
290
  }
291
291
  function createServerWorldCharacters(platformHandle) {
292
292
  return Object.freeze({
293
- async publicInspect() {
294
- if (typeof platformHandle.storage !== "function") {
295
- 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.");
296
306
  }
307
+ return parseRuntimeCharacterList(parseSharedCharacterJson(await platformHandle.worldCharacterRuntimeList()));
308
+ },
309
+ async publicInspect() {
310
+ requireSharedCharacterBroker(platformHandle);
297
311
  return parseSharedCharacterJson(await platformHandle.storage("worldCharacterPublicInspect", "{}"));
298
312
  },
299
313
  async publicRelay(input) {
300
- if (typeof platformHandle.storage !== "function") {
301
- throw new TypeError("The shared character broker is unavailable.");
302
- }
314
+ requireSharedCharacterBroker(platformHandle);
303
315
  const operationId = assertWorldOperationId(input?.operationId);
304
316
  const sourceId = assertWorldCharacterId(input?.sourceId, "sourceId", 128);
305
317
  const transmittingCharacterId = assertWorldCharacterId(input?.transmittingCharacterId, "transmittingCharacterId");
@@ -320,6 +332,142 @@ function createServerWorldCharacters(platformHandle) {
320
332
  },
321
333
  });
322
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
+ }
323
471
  function parseWorldSnapshot(value, operation) {
324
472
  if (!value || typeof value !== "object" || Array.isArray(value)) {
325
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.39",
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"