castle-web-sdk 0.4.32 → 0.4.34

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 @@
1
+ export declare function createDevUnavailableNotice(message: string): () => void;
@@ -0,0 +1,36 @@
1
+ export function createDevUnavailableNotice(message) {
2
+ let noticeEl = null;
3
+ let noticeTimer = null;
4
+ return () => {
5
+ if (typeof document === "undefined")
6
+ return;
7
+ if (!noticeEl) {
8
+ noticeEl = document.createElement("div");
9
+ noticeEl.textContent = message;
10
+ noticeEl.style.cssText = [
11
+ "position:fixed",
12
+ "left:50%",
13
+ "bottom:24px",
14
+ "transform:translateX(-50%)",
15
+ "max-width:80vw",
16
+ "padding:10px 16px",
17
+ "border-radius:8px",
18
+ "background:rgba(0,0,0,0.82)",
19
+ "color:#fff",
20
+ "font:500 13px/1.4 system-ui,sans-serif",
21
+ "text-align:center",
22
+ "z-index:2147483647",
23
+ "pointer-events:none",
24
+ "transition:opacity 0.3s ease",
25
+ ].join(";");
26
+ document.body.appendChild(noticeEl);
27
+ }
28
+ noticeEl.style.opacity = "1";
29
+ if (noticeTimer)
30
+ clearTimeout(noticeTimer);
31
+ noticeTimer = setTimeout(() => {
32
+ if (noticeEl)
33
+ noticeEl.style.opacity = "0";
34
+ }, 3200);
35
+ };
36
+ }
package/dist/passes.js CHANGED
@@ -9,6 +9,7 @@
9
9
  // notice itself (kept deliberately tiny), returns `unavailable`
10
10
  // There is no capability check for the deck to make — every platform returns a
11
11
  // PassOfferResult, so a single code path handles them all.
12
+ import { createDevUnavailableNotice } from "./devUnavailableNotice";
12
13
  import { CastleError } from "./errors";
13
14
  import { getCommandChannel, hostRequest } from "./transport";
14
15
  export const Pass = {
@@ -46,39 +47,6 @@ async function offer(passId) {
46
47
  }
47
48
  return result;
48
49
  }
49
- let devNoticeEl = null;
50
- let devNoticeTimer = null;
51
50
  // Minimal, dependency-free toast. Dev-only affordance — not the place for a
52
51
  // designed purchase UI.
53
- function showDevUnavailableNotice() {
54
- if (typeof document === "undefined")
55
- return;
56
- if (!devNoticeEl) {
57
- devNoticeEl = document.createElement("div");
58
- devNoticeEl.textContent = "Passes can only be purchased in the Castle app.";
59
- devNoticeEl.style.cssText = [
60
- "position:fixed",
61
- "left:50%",
62
- "bottom:24px",
63
- "transform:translateX(-50%)",
64
- "max-width:80vw",
65
- "padding:10px 16px",
66
- "border-radius:8px",
67
- "background:rgba(0,0,0,0.82)",
68
- "color:#fff",
69
- "font:500 13px/1.4 system-ui,sans-serif",
70
- "text-align:center",
71
- "z-index:2147483647",
72
- "pointer-events:none",
73
- "transition:opacity 0.3s ease",
74
- ].join(";");
75
- document.body.appendChild(devNoticeEl);
76
- }
77
- devNoticeEl.style.opacity = "1";
78
- if (devNoticeTimer)
79
- clearTimeout(devNoticeTimer);
80
- devNoticeTimer = setTimeout(() => {
81
- if (devNoticeEl)
82
- devNoticeEl.style.opacity = "0";
83
- }, 3200);
84
- }
52
+ const showDevUnavailableNotice = createDevUnavailableNotice("Passes can only be purchased in the Castle app.");
@@ -63,6 +63,15 @@ export interface SetCharacterGameStateInput {
63
63
  key: string;
64
64
  value: string | number | boolean;
65
65
  }>;
66
+ objectChanges?: CharacterObjectChange[];
67
+ }
68
+ export interface CharacterObjectChange {
69
+ objectId: string;
70
+ holderId?: string;
71
+ stateChanges?: Array<{
72
+ key: string;
73
+ value: string | number | boolean;
74
+ }>;
66
75
  }
67
76
  export interface ShareCharacterClaimInput {
68
77
  operationId: string;
@@ -93,17 +102,57 @@ export interface CompleteCharacterBeatInput {
93
102
  eventOccurrenceId: string;
94
103
  outcomeId: string;
95
104
  }
105
+ export interface EvaluateCharacterInformationInput {
106
+ operationId: string;
107
+ maxDeliveries?: number;
108
+ }
109
+ export interface CharacterInformationEvaluationResult {
110
+ generation: number;
111
+ evaluatedAt: string;
112
+ snapshotCandidateCount: number;
113
+ deliveries: Array<{
114
+ ruleId: string;
115
+ rootId: string;
116
+ recipientCharacterId: string;
117
+ status: "delivered" | "already_known";
118
+ }>;
119
+ cancelled: Array<{
120
+ ruleId: string;
121
+ rootId: string;
122
+ }>;
123
+ blocked: Array<{
124
+ ruleId: string;
125
+ rootId: string;
126
+ recipientCharacterId: string;
127
+ code: "testimony_backlog_full" | "information_limit" | "storage_capacity";
128
+ }>;
129
+ remainingCount: number;
130
+ }
131
+ export type CharacterInformationEvaluationOperation = Omit<CharacterOperation, "result"> & {
132
+ result: CharacterInformationEvaluationResult | null;
133
+ };
134
+ export interface SendCharacterInput {
135
+ operationId: string;
136
+ characterId: string;
137
+ input: string;
138
+ audience: {
139
+ listenerCharacterIds: string[];
140
+ };
141
+ }
96
142
  /**
97
143
  * Narrative commands do not run a model. They record creator-validated events,
98
144
  * knowledge delivery and beat completion inside this immutable playthrough.
99
145
  */
100
146
  export interface ServerPlaythroughCharacters {
147
+ /** Queues a funded turn whose admitted listeners are fixed at enqueue time. */
148
+ send(input: SendCharacterInput): Promise<CharacterOperation>;
101
149
  recordEvent(input: RecordCharacterEventInput): Promise<CharacterOperation>;
102
150
  setGameState(input: SetCharacterGameStateInput): Promise<CharacterOperation>;
103
151
  shareClaim(input: ShareCharacterClaimInput): Promise<CharacterOperation>;
104
152
  inspect(): Promise<Json>;
105
153
  deliverKnowledge(input: DeliverCharacterKnowledgeInput): Promise<CharacterOperation>;
106
154
  completeBeat(input: CompleteCharacterBeatInput): Promise<CharacterOperation>;
155
+ evaluateInformation(input: EvaluateCharacterInformationInput): Promise<CharacterInformationEvaluationOperation>;
107
156
  status(operationId: string): Promise<CharacterOperation>;
108
157
  history(options?: ServerCharacterHistoryOptions): Promise<CharacterHistory>;
109
158
  }
@@ -84,6 +84,17 @@ export function createServerStorage(platformHandle) {
84
84
  }
85
85
  function createPlaythrough(call, subject, generation) {
86
86
  const characters = Object.freeze({
87
+ async send(input) {
88
+ const characterId = assertNarrativeId(input?.characterId, "characterId");
89
+ return call("characterSendWithAudience", {
90
+ subject,
91
+ generation,
92
+ operationId: assertOperationId(input?.operationId),
93
+ characterId,
94
+ input: assertCharacterInput(input?.input),
95
+ audience: assertCharacterAudience(input?.audience, characterId),
96
+ });
97
+ },
87
98
  async recordEvent(input) {
88
99
  return call("characterRecordEvent", {
89
100
  subject,
@@ -94,11 +105,15 @@ function createPlaythrough(call, subject, generation) {
94
105
  });
95
106
  },
96
107
  async setGameState(input) {
108
+ const objectChanges = input?.objectChanges === undefined
109
+ ? undefined
110
+ : assertObjectChanges(input.objectChanges);
97
111
  return call("characterSetGameState", {
98
112
  subject, generation,
99
113
  operationId: assertOperationId(input?.operationId),
100
114
  expectedRevision: assertStateRevision(input?.expectedRevision),
101
- changes: assertStateChanges(input?.changes),
115
+ changes: assertStateChanges(input?.changes, objectChanges !== undefined),
116
+ ...(objectChanges === undefined ? {} : { objectChanges }),
102
117
  });
103
118
  },
104
119
  async shareClaim(input) {
@@ -135,6 +150,15 @@ function createPlaythrough(call, subject, generation) {
135
150
  outcomeId: assertNarrativeId(input?.outcomeId, "outcomeId"),
136
151
  });
137
152
  },
153
+ async evaluateInformation(input) {
154
+ const maxDeliveries = assertInformationMaxDeliveries(input?.maxDeliveries);
155
+ return call("characterEvaluateInformation", {
156
+ subject,
157
+ generation,
158
+ operationId: assertOperationId(input?.operationId),
159
+ ...(maxDeliveries === undefined ? {} : { maxDeliveries }),
160
+ });
161
+ },
138
162
  async status(operationId) {
139
163
  return call("characterStatus", {
140
164
  subject,
@@ -347,6 +371,33 @@ function assertNarrativeIds(value, field) {
347
371
  }
348
372
  return value.map((item) => assertNarrativeId(item, field));
349
373
  }
374
+ function assertCharacterInput(value) {
375
+ if (typeof value !== "string" ||
376
+ value.length === 0 ||
377
+ value.length > 2000 ||
378
+ value.includes("\0") ||
379
+ hasUnpairedSurrogate(value)) {
380
+ throw characterArgumentError("input must be valid non-empty text of at most 2000 characters.");
381
+ }
382
+ return value;
383
+ }
384
+ function assertCharacterAudience(value, targetCharacterId) {
385
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
386
+ throw characterArgumentError("audience must contain listenerCharacterIds.");
387
+ }
388
+ const audience = value;
389
+ if (Object.keys(audience).some((key) => key !== "listenerCharacterIds") ||
390
+ !Array.isArray(audience.listenerCharacterIds) ||
391
+ audience.listenerCharacterIds.length > 3) {
392
+ throw characterArgumentError("audience must contain at most three listener character ids.");
393
+ }
394
+ const listenerCharacterIds = audience.listenerCharacterIds.map((id) => assertNarrativeId(id, "audience.listenerCharacterIds"));
395
+ if (new Set(listenerCharacterIds).size !== listenerCharacterIds.length ||
396
+ listenerCharacterIds.includes(targetCharacterId)) {
397
+ throw characterArgumentError("audience listeners must be unique and must not include the target character.");
398
+ }
399
+ return { listenerCharacterIds };
400
+ }
350
401
  function assertKnowledgeSource(value) {
351
402
  if (!value || typeof value !== "object" || Array.isArray(value)) {
352
403
  throw storageError("CAULDRON_CHARACTER_INVALID_ARGUMENT", "source must name creator_event or a character.", "session.storage.playthrough.characters.deliverKnowledge");
@@ -398,24 +449,92 @@ function assertStateRevision(value) {
398
449
  }
399
450
  return value;
400
451
  }
401
- function assertStateChanges(value) {
402
- if (!Array.isArray(value) || value.length < 1 || value.length > 32) {
403
- throw characterArgumentError("changes must contain one to 32 fields.");
452
+ function assertInformationMaxDeliveries(value) {
453
+ if (value === undefined)
454
+ return undefined;
455
+ if (!Number.isSafeInteger(value) || value < 1 || value > 32) {
456
+ throw characterArgumentError("maxDeliveries must be an integer from 1 through 32.");
457
+ }
458
+ return value;
459
+ }
460
+ function assertStateChanges(value, hasObjectChanges = false) {
461
+ if (!Array.isArray(value) || value.length > 32 || (!hasObjectChanges && value.length < 1)) {
462
+ throw characterArgumentError(hasObjectChanges
463
+ ? "changes must contain at most 32 fields."
464
+ : "changes must contain one to 32 fields.");
465
+ }
466
+ const seen = new Set();
467
+ return value.map((change) => assertStateChange(change, seen, "state change"));
468
+ }
469
+ function assertObjectChanges(value) {
470
+ if (!Array.isArray(value) || value.length < 1 || value.length > 16) {
471
+ throw characterArgumentError("objectChanges must contain one to 16 objects.");
472
+ }
473
+ const seenObjects = new Set();
474
+ return value.map((change) => {
475
+ if (!change || typeof change !== "object" || Array.isArray(change)) {
476
+ throw characterArgumentError("object change must name an object and a holder or state change.");
477
+ }
478
+ const candidate = change;
479
+ if (Object.keys(candidate).some((key) => key !== "objectId" && key !== "holderId" && key !== "stateChanges")) {
480
+ throw characterArgumentError("object change has an unknown field.");
481
+ }
482
+ const objectId = assertMechanicsKey(candidate.objectId, "objectChanges.objectId");
483
+ if (seenObjects.has(objectId)) {
484
+ throw characterArgumentError("objectChanges contains a duplicate object.");
485
+ }
486
+ seenObjects.add(objectId);
487
+ const hasHolder = candidate.holderId !== undefined;
488
+ const holderId = hasHolder
489
+ ? assertNarrativeId(candidate.holderId, "objectChanges.holderId")
490
+ : undefined;
491
+ const stateChanges = candidate.stateChanges === undefined
492
+ ? undefined
493
+ : assertObjectStateChanges(candidate.stateChanges);
494
+ if (!hasHolder && !stateChanges?.length) {
495
+ throw characterArgumentError("object change must include holderId or stateChanges.");
496
+ }
497
+ return {
498
+ objectId,
499
+ ...(hasHolder ? { holderId } : {}),
500
+ ...(stateChanges === undefined ? {} : { stateChanges }),
501
+ };
502
+ });
503
+ }
504
+ function assertObjectStateChanges(value) {
505
+ if (!Array.isArray(value) || value.length > 8) {
506
+ throw characterArgumentError("object stateChanges must contain at most eight fields.");
404
507
  }
405
508
  const seen = new Set();
406
509
  return value.map((change) => {
407
510
  if (!change || typeof change !== "object" || Array.isArray(change)) {
408
- throw characterArgumentError("state change must contain a key and scalar value.");
511
+ throw characterArgumentError("object state change must contain a key and scalar value.");
409
512
  }
410
- const { key, value: field } = change;
411
- if (typeof key !== "string" || !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(key) ||
412
- key === "constructor" || key === "prototype" || seen.has(key) || !assertScalar(field)) {
413
- throw characterArgumentError("state change has an invalid or duplicate field.");
513
+ if (Object.keys(change).some((key) => key !== "key" && key !== "value")) {
514
+ throw characterArgumentError("object state change has an unknown field.");
414
515
  }
415
- seen.add(key);
416
- return { key, value: field };
516
+ return assertStateChange(change, seen, "object state change");
417
517
  });
418
518
  }
519
+ function assertStateChange(change, seen, label) {
520
+ if (!change || typeof change !== "object" || Array.isArray(change)) {
521
+ throw characterArgumentError(`${label} must contain a key and scalar value.`);
522
+ }
523
+ const { key, value } = change;
524
+ if (typeof key !== "string" || !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(key) ||
525
+ key === "constructor" || key === "prototype" || seen.has(key) || !assertScalar(value)) {
526
+ throw characterArgumentError(`${label} has an invalid or duplicate field.`);
527
+ }
528
+ seen.add(key);
529
+ return { key, value };
530
+ }
531
+ function assertMechanicsKey(value, field) {
532
+ if (typeof value !== "string" || !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(value) ||
533
+ value === "constructor" || value === "prototype") {
534
+ throw characterArgumentError(`${field} must be a safe key of at most 64 characters.`);
535
+ }
536
+ return value;
537
+ }
419
538
  function assertHistoryLimit(value) {
420
539
  if (value === undefined || value === null)
421
540
  return null;
package/dist/snapshots.js CHANGED
@@ -9,6 +9,7 @@
9
9
  // - mobile app : the collect card over the captured frame, then the confirm sheet
10
10
  // - web player : "collect this snapshot in the app", returns `unavailable`
11
11
  // - dev CLI : a small notice, returns `unavailable`
12
+ import { createDevUnavailableNotice } from "./devUnavailableNotice";
12
13
  import { CastleError } from "./errors";
13
14
  import { getCommandChannel, hostRequest } from "./transport";
14
15
  export const Snapshot = {
@@ -89,40 +90,4 @@ function nextPaintedFrame() {
89
90
  });
90
91
  });
91
92
  }
92
- let devNoticeEl = null;
93
- let devNoticeTimer = null;
94
- function showDevUnavailableNotice() {
95
- if (typeof document === "undefined") {
96
- return;
97
- }
98
- if (!devNoticeEl) {
99
- devNoticeEl = document.createElement("div");
100
- devNoticeEl.textContent = "Snapshots can only be collected in the Castle app.";
101
- devNoticeEl.style.cssText = [
102
- "position:fixed",
103
- "left:50%",
104
- "bottom:24px",
105
- "transform:translateX(-50%)",
106
- "max-width:80vw",
107
- "padding:10px 16px",
108
- "border-radius:8px",
109
- "background:rgba(0,0,0,0.82)",
110
- "color:#fff",
111
- "font:500 13px/1.4 system-ui,sans-serif",
112
- "text-align:center",
113
- "z-index:2147483647",
114
- "pointer-events:none",
115
- "transition:opacity 0.3s ease",
116
- ].join(";");
117
- document.body.appendChild(devNoticeEl);
118
- }
119
- devNoticeEl.style.opacity = "1";
120
- if (devNoticeTimer) {
121
- clearTimeout(devNoticeTimer);
122
- }
123
- devNoticeTimer = setTimeout(() => {
124
- if (devNoticeEl) {
125
- devNoticeEl.style.opacity = "0";
126
- }
127
- }, 3200);
128
- }
93
+ const showDevUnavailableNotice = createDevUnavailableNotice("Snapshots can only be collected in the Castle app.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.32",
3
+ "version": "0.4.34",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/castle-xyz/castle-experimental-web.git"