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.
- package/dist/devUnavailableNotice.d.ts +1 -0
- package/dist/devUnavailableNotice.js +36 -0
- package/dist/passes.js +2 -34
- package/dist/server/storage.d.ts +49 -0
- package/dist/server/storage.js +130 -11
- package/dist/snapshots.js +2 -37
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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.");
|
package/dist/server/storage.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/server/storage.js
CHANGED
|
@@ -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
|
|
402
|
-
if (
|
|
403
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
|
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
|
-
|
|
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.");
|