castle-web-sdk 0.4.44 → 0.4.46

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/castle.d.ts CHANGED
@@ -1,20 +1,25 @@
1
1
  export { isEdit } from "./context";
2
2
  export { Agent } from "./agent";
3
3
  export type { AgentChatDelta, AgentChatParams, AgentChatResult, AgentMessage, AgentReasoning, AgentTool, AgentToolCall, AgentToolChoice, AgentUsage, CastleAgentApi, } from "./agent";
4
+ export { Avatar } from "./avatar";
5
+ export type { AvatarResult, CastleAvatarApi } from "./avatar";
4
6
  export { Characters } from "./characters";
5
7
  export type { CastleCharactersApi, CharacterHandle, CharacterHistoryOptions, CharacterSendInput, } from "./characters";
6
8
  export { Decks } from "./decks";
7
9
  export type { CastleDecksApi, DeckSummary, DecksListParams, DecksListResult } from "./decks";
8
10
  export { CastleError } from "./errors";
11
+ export { experiment, experiments, experimentsReady } from "./experiments";
9
12
  export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
10
13
  export { Haptics } from "./haptics";
11
14
  export type { CastleHapticsApi, HapticsResult, HapticsStatus, HapticStyle } from "./haptics";
15
+ export { Hub } from "./hub";
16
+ export type { CastleHubApi, HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubGoParams, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, } from "./hub";
12
17
  export { Leaderboard } from "./leaderboard";
13
18
  export type { LeaderboardData, LeaderboardEntry, LeaderboardOptions, LeaderboardScope, LeaderboardShowOptions, LeaderboardShowResult, LeaderboardShowStatus, LeaderboardSort, } from "./leaderboard";
14
19
  export { Lifecycle } from "./lifecycle";
15
20
  export type { CastleLifecycleApi } from "./lifecycle";
16
21
  export { Multiplayer } from "./multiplayer";
17
- export type { CastleMultiplayerApi, MultiplayerConnection, MultiplayerConnectionState, MultiplayerDestinationJoinResult, MultiplayerDestinationOptions, MultiplayerJoinOptions, MultiplayerRoster, MultiplayerStateMetadata, } from "./multiplayer";
22
+ export type { CastleMultiplayerApi, MultiplayerConnection, MultiplayerConnectionState, MultiplayerCompleteScheduledBeatCommand, MultiplayerCompleteScheduledBeatResult, MultiplayerDestinationJoinResult, MultiplayerDestinationOptions, MultiplayerEvaluateBeatsCommand, MultiplayerEvaluateBeatsResult, MultiplayerJoinHint, MultiplayerJoinOptions, MultiplayerRoster, MultiplayerScheduledBeatReason, MultiplayerScheduledBeatSelection, MultiplayerStateMetadata, MultiplayerStoryOpportunity, MultiplayerWorldCharacterCommand, MultiplayerWorldCharacterCommandResult, MultiplayerWorldCharacterCommandResultValue, } from "./multiplayer";
18
23
  export type { MultiplayerVoice, VoiceJoinStatus, VoiceListener, VoiceMixEntry, VoiceSpatialOptions, VoiceState, VoiceVector, } from "./multiplayerVoice";
19
24
  export type { MultiplayerDestinationAudience, MultiplayerSoloReason } from "./commands";
20
25
  export { MAX_CLIENT_FRAME_BYTES, MAX_PLAYERS_CAP } from "./multiplayerProtocol";
@@ -41,3 +46,7 @@ export type { CastleClockZone, CastleDateParts, CastleTimeApi } from "./time";
41
46
  export type { Json } from "./types";
42
47
  export { User } from "./user";
43
48
  export type { CastleUser, CastleUserApi } from "./user";
49
+ export { createSaveCoordinator } from "./saveCoordinator";
50
+ export type { CoordinatedChange, CoordinatedSave, SaveCoordinator, SaveStatus, } from "./saveCoordinator";
51
+ export { SaveError } from "./save";
52
+ export type { SaveApi, SaveBucketApi, SaveBucketRead, SaveChange, SaveCheck, SaveClaimed, SaveClaimInput, SaveErrorEnvelope, SaveInitial, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveReceipt, SaveRevision, SaveRows, SaveScope, SaveSelection, SaveSnapshot, SaveTarget, SaveTransactionInput, SaveWriter, } from "./save";
package/dist/castle.js CHANGED
@@ -1,11 +1,14 @@
1
1
  // Castle Web SDK
2
2
  export { isEdit } from "./context";
3
3
  export { Agent } from "./agent";
4
+ export { Avatar } from "./avatar";
4
5
  export { Characters } from "./characters";
5
6
  export { Decks } from "./decks";
6
7
  export { CastleError } from "./errors";
8
+ export { experiment, experiments, experimentsReady } from "./experiments";
7
9
  export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
8
10
  export { Haptics } from "./haptics";
11
+ export { Hub } from "./hub";
9
12
  export { Leaderboard } from "./leaderboard";
10
13
  export { Lifecycle } from "./lifecycle";
11
14
  export { Multiplayer } from "./multiplayer";
@@ -21,3 +24,5 @@ export { SharedStorage, Storage } from "./storage";
21
24
  export { Store } from "./store";
22
25
  export { Time } from "./time";
23
26
  export { User } from "./user";
27
+ export { createSaveCoordinator } from "./saveCoordinator";
28
+ export { SaveError } from "./save";
@@ -1,3 +1,4 @@
1
+ import type { SaveClaimed, SaveClaimInput, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveReceipt, SaveSnapshot, SaveTransactionInput } from "./save";
1
2
  import type { Json } from "./types";
2
3
  export declare const CASTLE_SDK_PROTOCOL = 1;
3
4
  export type StorageBlob = Record<string, string>;
@@ -83,6 +84,174 @@ export type PortalInfoResult = {
83
84
  export interface PortalPrefetchResult {
84
85
  status: PortalPrefetchStatus;
85
86
  }
87
+ export interface HubGoParams {
88
+ doorId: string;
89
+ }
90
+ export type HubPlacementKind = "layer" | "pad" | "exact";
91
+ export type HubHandoffStatus = "ok" | "cancelled" | "full" | "unavailable" | "not_found" | "forbidden" | "party_required";
92
+ /** Session acquisition, not a completed scene swap or socket join. */
93
+ export type HubHandoffResult = {
94
+ status: "ok";
95
+ /** Destination session WebSocket URL, as in multiplayer.getSession. */
96
+ url: string;
97
+ /** Single-use join nonce, valid for 60 s. */
98
+ nonce: string;
99
+ sessionId: string;
100
+ /** The destination session's pinned server bundle (sha256). */
101
+ bundleHash: string;
102
+ /** Immutable URL of the content pack `bundleHash` names; null when the
103
+ * destination has none. */
104
+ contentUrl: string | null;
105
+ kind?: undefined;
106
+ } | {
107
+ /**
108
+ * A cabinet: the door leads to a classic Castle deck, which the host plays
109
+ * itself. There is no session to join; the trip is already committed, and
110
+ * `Hub.back()` is the way out.
111
+ */
112
+ status: "ok";
113
+ kind: "classic";
114
+ url: null;
115
+ nonce: null;
116
+ sessionId: null;
117
+ bundleHash: string;
118
+ contentUrl: null;
119
+ } | {
120
+ status: Exclude<HubHandoffStatus, "ok">;
121
+ url: null;
122
+ nonce: null;
123
+ sessionId: null;
124
+ bundleHash: null;
125
+ contentUrl: null;
126
+ };
127
+ export type HubGoResult = HubHandoffResult;
128
+ export type HubBackResult = HubHandoffResult;
129
+ export type HubJumpResult = HubHandoffResult;
130
+ export interface HubJumpParams {
131
+ userId: string;
132
+ }
133
+ /** A mutual friend ghost shows in a layer of the current place right now. */
134
+ export interface HubFriend {
135
+ userId: string;
136
+ username: string | null;
137
+ /** In the layer the player is in already: nothing to jump to. */
138
+ here: boolean;
139
+ /**
140
+ * Set when the friend is not in this place but inside the place behind one
141
+ * of its doors: that door's id. Null for a friend in a layer of this place.
142
+ */
143
+ door: string | null;
144
+ }
145
+ /** Hub.report: another player, by user id, and an optional short reason. */
146
+ export interface HubReportParams {
147
+ userId: string;
148
+ reason?: string;
149
+ }
150
+ /** `unavailable` on hosts without hub support or outside a hub session. */
151
+ export interface HubReportResult {
152
+ status: "ok" | "unavailable";
153
+ }
154
+ export type HubFriendsResult = {
155
+ status: "ok";
156
+ friends: HubFriend[];
157
+ } | {
158
+ status: "unavailable";
159
+ friends: null;
160
+ };
161
+ /** Hub.chat: only the messages after a cursor an earlier answer returned. */
162
+ export interface HubChatParams {
163
+ after?: string;
164
+ }
165
+ /** One chat message ghost lets this player read. */
166
+ export interface HubChatMessage {
167
+ id: string;
168
+ fromUserId: string;
169
+ fromUsername: string | null;
170
+ /** At most 200 characters. */
171
+ text: string;
172
+ /** When ghost took it, as an ISO 8601 time. */
173
+ at: string;
174
+ }
175
+ /**
176
+ * `canChat` is whether ghost lets this player chat at all; `peers` are the
177
+ * user ids in this hub session who would be sent what they say. `unavailable`
178
+ * on hosts without hub support or outside a hub session.
179
+ */
180
+ export type HubChatResult = {
181
+ status: "ok";
182
+ canChat: boolean;
183
+ peers: string[];
184
+ messages: HubChatMessage[];
185
+ /** Pass to the next `Hub.chat` to read only what is newer. */
186
+ cursor: string | null;
187
+ } | {
188
+ status: "unavailable";
189
+ canChat: false;
190
+ peers: null;
191
+ messages: null;
192
+ cursor: null;
193
+ };
194
+ /** Hub.say: free text, at most 200 characters once trimmed. */
195
+ export interface HubSayParams {
196
+ text: string;
197
+ }
198
+ /**
199
+ * `sent` carries the message's id; `none` when nobody in the session may read
200
+ * it, `refused` when ghost will not take it, `limited` when the player is
201
+ * saying too much too fast. `unavailable` on hosts without hub support or
202
+ * outside a hub session.
203
+ */
204
+ export interface HubSayResult {
205
+ status: "sent" | "none" | "refused" | "limited" | "unavailable";
206
+ id: string | null;
207
+ }
208
+ /** Hub.reportChat: a message this player was returned, by its id. */
209
+ export interface HubReportChatParams {
210
+ messageId: string;
211
+ }
212
+ /** Public door row; destination identity and return tickets stay host-owned. */
213
+ export interface HubDoor {
214
+ doorId: string;
215
+ launcherKind: string;
216
+ placementKind: HubPlacementKind;
217
+ /** The row's placement data: `{}`, or `{ key }` for an exact door. */
218
+ placement: Json;
219
+ padSize: number | null;
220
+ enabled: boolean;
221
+ featured: boolean;
222
+ /** Ghost-owned presence, not a count maintained by the place's engine. */
223
+ status: {
224
+ playersInside: number;
225
+ friendsInside: number;
226
+ };
227
+ }
228
+ export type HubDoorsResult = {
229
+ status: "ok";
230
+ doors: HubDoor[];
231
+ } | {
232
+ status: "unavailable";
233
+ doors: null;
234
+ };
235
+ export interface HubCommitParams {
236
+ /** The acquisition's nonce: unique to it, unlike its session. */
237
+ nonce: string;
238
+ }
239
+ export type HubCommitStatus = "committed" | "expired" | "superseded" | "cancelled" | "none" | "unavailable";
240
+ export interface HubCommitResult {
241
+ status: HubCommitStatus;
242
+ }
243
+ export type HubCancelStatus = "cancelled" | "expired" | "none" | "unavailable";
244
+ export interface HubCancelResult {
245
+ status: HubCancelStatus;
246
+ }
247
+ export type HubEvent = {
248
+ type: "back";
249
+ };
250
+ export interface HubEventEnvelope {
251
+ castleSdk: typeof CASTLE_SDK_PROTOCOL;
252
+ hub: HubEvent;
253
+ }
254
+ export declare function hubEventFrom(value: unknown): HubEvent | null;
86
255
  export type HapticStyle = "light" | "medium" | "heavy" | "selection" | "success" | "warning" | "error";
87
256
  export type HapticsStatus = "triggered" | "unavailable";
88
257
  export interface HapticsResult {
@@ -114,6 +283,10 @@ export interface MultiplayerGetSessionResult {
114
283
  /** Internal local-serve scope for restoring this Play panel after a reload. */
115
284
  devReconnectScope?: string;
116
285
  }
286
+ export interface MultiplayerJoinHint {
287
+ routeId: string;
288
+ slot: string;
289
+ }
117
290
  export interface MultiplayerJoinDestinationParams {
118
291
  routeId: string;
119
292
  slot: string;
@@ -327,10 +500,15 @@ export interface CommandParams {
327
500
  "user.get": {
328
501
  userId: string;
329
502
  };
503
+ "experiments.get": Record<string, never>;
330
504
  "time.getServerTime": Record<string, never>;
331
505
  "pass.has": {
332
506
  passId: string;
333
507
  };
508
+ "avatar.get": Record<string, never>;
509
+ "avatar.set": {
510
+ avatar: number;
511
+ };
334
512
  "pass.offer": {
335
513
  passId: string;
336
514
  };
@@ -348,10 +526,24 @@ export interface CommandParams {
348
526
  "portal.info": {
349
527
  targetDeckId: string;
350
528
  };
529
+ "hub.go": HubGoParams;
530
+ "hub.back": Record<string, never>;
531
+ "hub.doors": Record<string, never>;
532
+ "hub.friends": Record<string, never>;
533
+ "hub.jump": HubJumpParams;
534
+ "hub.report": HubReportParams;
535
+ "hub.chat": HubChatParams;
536
+ "hub.say": HubSayParams;
537
+ "hub.reportChat": HubReportChatParams;
538
+ "hub.commit": HubCommitParams;
539
+ "hub.cancel": {
540
+ nonce?: string;
541
+ };
351
542
  "haptics.play": {
352
543
  style: HapticStyle;
353
544
  };
354
545
  "multiplayer.getSession": MultiplayerGetSessionParams;
546
+ "multiplayer.getJoinHint": Record<string, never>;
355
547
  "multiplayer.joinDestination": MultiplayerJoinDestinationParams;
356
548
  "voice.getState": {
357
549
  sessionId: string;
@@ -393,6 +585,15 @@ export interface CommandParams {
393
585
  characterId: string;
394
586
  operationId: string;
395
587
  };
588
+ "cauldronStorage.snapshot": {
589
+ targets: SaveReadTarget[];
590
+ };
591
+ "cauldronStorage.transaction": SaveTransactionInput;
592
+ "cauldronStorage.claim": SaveClaimInput;
593
+ "cauldronStorage.initialize": SaveInitializeInput;
594
+ "cauldronStorage.operation": {
595
+ operationId: string;
596
+ };
396
597
  "cauldronStorage.get": {
397
598
  scope: StoreScope;
398
599
  subject?: string | null;
@@ -477,6 +678,9 @@ export interface CommandResult {
477
678
  "user.get": {
478
679
  user: UserPayload | null;
479
680
  };
681
+ "experiments.get": {
682
+ variants: Record<string, string>;
683
+ };
480
684
  "time.getServerTime": {
481
685
  timestamp: number;
482
686
  timezoneOffset: number;
@@ -485,13 +689,27 @@ export interface CommandResult {
485
689
  "pass.has": {
486
690
  hasPass: boolean;
487
691
  };
692
+ "avatar.get": AvatarResult;
693
+ "avatar.set": AvatarResult;
488
694
  "pass.offer": PassOfferResult;
489
695
  "snapshot.offer": SnapshotOfferResult;
490
696
  "portal.open": PortalOpenResult;
491
697
  "portal.prefetch": PortalPrefetchResult;
492
698
  "portal.info": PortalInfoResult;
699
+ "hub.go": HubGoResult;
700
+ "hub.back": HubBackResult;
701
+ "hub.doors": HubDoorsResult;
702
+ "hub.friends": HubFriendsResult;
703
+ "hub.jump": HubJumpResult;
704
+ "hub.report": HubReportResult;
705
+ "hub.chat": HubChatResult;
706
+ "hub.say": HubSayResult;
707
+ "hub.reportChat": HubReportResult;
708
+ "hub.commit": HubCommitResult;
709
+ "hub.cancel": HubCancelResult;
493
710
  "haptics.play": HapticsResult;
494
711
  "multiplayer.getSession": MultiplayerGetSessionResult;
712
+ "multiplayer.getJoinHint": MultiplayerJoinHint | null;
495
713
  "multiplayer.joinDestination": MultiplayerJoinDestinationResult;
496
714
  "voice.getState": {
497
715
  state: VoiceState;
@@ -513,6 +731,11 @@ export interface CommandResult {
513
731
  "characters.status": CharacterOperation;
514
732
  "characters.history": CharacterHistory;
515
733
  "characters.reset": CharacterAdmission;
734
+ "cauldronStorage.snapshot": SaveSnapshot;
735
+ "cauldronStorage.transaction": SaveReceipt;
736
+ "cauldronStorage.claim": SaveClaimed;
737
+ "cauldronStorage.initialize": SaveOpened;
738
+ "cauldronStorage.operation": SaveOperation;
516
739
  "cauldronStorage.get": {
517
740
  entries: StoreEntry[];
518
741
  };
@@ -551,6 +774,12 @@ export interface UserPayload {
551
774
  frameUrl: string | null;
552
775
  color: string | null;
553
776
  }
777
+ export interface AvatarResult {
778
+ status: "ok" | "unavailable";
779
+ avatar: number | null;
780
+ /** False while the player wears the default ghost gives every account. */
781
+ chosen: boolean;
782
+ }
554
783
  export type CommandName = keyof CommandParams;
555
784
  export declare const COMMAND_TIMEOUTS_MS: Partial<Record<CommandName, number>>;
556
785
  export declare const STREAM_IDLE_TIMEOUT_MS = 60000;
package/dist/commands.js CHANGED
@@ -6,6 +6,17 @@
6
6
  // Marker + protocol version. Doubles as a discriminator so host messages can't
7
7
  // be confused with embed.js `castlexyz:` strings or RN console messages.
8
8
  export const CASTLE_SDK_PROTOCOL = 1;
9
+ export function hubEventFrom(value) {
10
+ if (typeof value !== "object" || value === null)
11
+ return null;
12
+ const record = value;
13
+ if (record.castleSdk !== CASTLE_SDK_PROTOCOL)
14
+ return null;
15
+ const event = record.hub;
16
+ if (typeof event !== "object" || event === null)
17
+ return null;
18
+ return event.type === "back" ? { type: "back" } : null;
19
+ }
9
20
  // How long the deck waits for a reply to these commands, in place of the
10
21
  // data-command timeout. A deck list fetches every cover on the list. A host
11
22
  // from before streaming reads a whole model reply before answering agent.chat.
@@ -13,6 +24,11 @@ export const COMMAND_TIMEOUTS_MS = {
13
24
  "agent.chat": 180_000,
14
25
  "decks.mine": 30_000,
15
26
  "decks.saved": 30_000,
27
+ // A door used right after arriving can wait out ghost's roster lag (up to
28
+ // 15 s) before the host gives up on it.
29
+ "hub.go": 30_000,
30
+ "hub.back": 30_000,
31
+ "hub.jump": 30_000,
16
32
  };
17
33
  // Once a host streams a command, each stream envelope restarts the wait at this
18
34
  // length instead. The deck-agent service sends a heartbeat every 15 seconds, and the host
package/dist/context.d.ts CHANGED
@@ -7,6 +7,12 @@ export interface CastleEmbed {
7
7
  */
8
8
  feed?: boolean;
9
9
  host?: CastleHost;
10
+ /**
11
+ * The player's A/B arms on this deck, `{ experimentName: variant }`, when the
12
+ * host hands them over with the page. A host that does not answers the
13
+ * `experiments.get` command instead (experiments.ts).
14
+ */
15
+ experiments?: Record<string, string>;
10
16
  }
11
17
  declare global {
12
18
  interface Window {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The variant this player has in `name`, or `null` when they are in no such
3
+ * experiment. Synchronous; see `experimentsReady` for the one case where a read
4
+ * can come before the answer.
5
+ */
6
+ export declare function experiment(name: string): string | null;
7
+ /** Every experiment this player is in on this deck, `{ name: variant }`. */
8
+ export declare function experiments(): Record<string, string>;
9
+ /**
10
+ * Resolves, with the same map `experiments()` returns, once the host has
11
+ * answered. Await it once before the first read. On a host that hands the
12
+ * answer over with the page it is already resolved. Never rejects: a host that
13
+ * cannot answer means no experiments.
14
+ */
15
+ export declare function experimentsReady(): Promise<Record<string, string>>;
@@ -0,0 +1,83 @@
1
+ // A/B experiments. Castle's server gives each player one variant of every
2
+ // experiment this deck is in (castle.json `experiments`, plus any Castle runs
3
+ // across decks and lists this deck in), the same one on every load, and resolves
4
+ // them once per deck load. The deck only ever reads the answer.
5
+ //
6
+ // The reads are synchronous. A host that builds the deck's document puts the
7
+ // answer in `CastleEmbed.experiments`, and it is there from the first line the
8
+ // deck runs. Any other host answers one `experiments.get` command instead, so a
9
+ // deck awaits `experimentsReady()` once at boot and reads synchronously after.
10
+ //
11
+ // `null` is a normal answer: no such experiment, one that has ended, a player
12
+ // who is not in it, or a host that predates experiments. Always handle it.
13
+ import { getCastleEmbed } from "./context";
14
+ import { hostRequest } from "./transport";
15
+ const NONE = Object.freeze({});
16
+ let variants = null;
17
+ let loading = null;
18
+ let warnedEarlyRead = false;
19
+ /**
20
+ * The variant this player has in `name`, or `null` when they are in no such
21
+ * experiment. Synchronous; see `experimentsReady` for the one case where a read
22
+ * can come before the answer.
23
+ */
24
+ export function experiment(name) {
25
+ const known = current();
26
+ return Object.hasOwn(known, name) ? known[name] : null;
27
+ }
28
+ /** Every experiment this player is in on this deck, `{ name: variant }`. */
29
+ export function experiments() {
30
+ return { ...current() };
31
+ }
32
+ /**
33
+ * Resolves, with the same map `experiments()` returns, once the host has
34
+ * answered. Await it once before the first read. On a host that hands the
35
+ * answer over with the page it is already resolved. Never rejects: a host that
36
+ * cannot answer means no experiments.
37
+ */
38
+ export function experimentsReady() {
39
+ return load().then((known) => ({ ...known }));
40
+ }
41
+ function current() {
42
+ if (variants)
43
+ return variants;
44
+ const embedded = fromEmbed();
45
+ if (embedded) {
46
+ variants = embedded;
47
+ return variants;
48
+ }
49
+ if (!loading && !warnedEarlyRead) {
50
+ warnedEarlyRead = true;
51
+ console.warn("Castle experiment() was read before the host answered, so it returned null. " +
52
+ "Call `await experimentsReady()` once at boot, before the first read.");
53
+ }
54
+ // Asked for now, so a later read has the answer even without the await.
55
+ void load();
56
+ return NONE;
57
+ }
58
+ function load() {
59
+ if (variants)
60
+ return Promise.resolve(variants);
61
+ const embedded = fromEmbed();
62
+ if (embedded) {
63
+ variants = embedded;
64
+ return Promise.resolve(variants);
65
+ }
66
+ loading ??= hostRequest("experiments.get", {})
67
+ .then((result) => clean(result?.variants) ?? NONE)
68
+ // A host from before experiments does not know the command.
69
+ .catch(() => NONE)
70
+ .then((known) => {
71
+ variants = known;
72
+ return known;
73
+ });
74
+ return loading;
75
+ }
76
+ function fromEmbed() {
77
+ return clean(getCastleEmbed()?.experiments);
78
+ }
79
+ function clean(value) {
80
+ if (!value || typeof value !== "object" || Array.isArray(value))
81
+ return null;
82
+ return Object.freeze(Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string")));
83
+ }
package/dist/hub.d.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { HubBackResult, HubCancelResult, HubChatResult, HubDoorsResult, HubFriendsResult, HubGoResult, HubJumpResult, HubReportResult, HubSayResult } from "./commands";
2
+ export type { HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubGoParams, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, } from "./commands";
3
+ /**
4
+ * Doors between hub places. Every hub-kit deck is a place; ghost's door table
5
+ * is the allowlist, and the host supplies real authentication and the current
6
+ * place, session and party. A deck names only a door, never a destination
7
+ * deck, session or party.
8
+ *
9
+ * Going through a door is acquire, then join: `go` (or `back`) acquires the
10
+ * destination and changes nothing else; the deck loads the destination's
11
+ * content behind its door animation and joins with
12
+ * `Multiplayer.join({ mode: "hub", handoff })`. When that socket says hello the
13
+ * SDK tells the host, which commits the trip: presence moves and the return
14
+ * ticket is written. A failure before then leaves the player where they were.
15
+ */
16
+ export interface CastleHubApi {
17
+ /**
18
+ * Acquire the destination behind a door of the current place. A door used
19
+ * within seconds of arriving may take up to 15 s while ghost catches up with
20
+ * the arrival. `unavailable` on hosts without hub support.
21
+ */
22
+ go(doorId: string): Promise<HubGoResult>;
23
+ /**
24
+ * Acquire the way back through the door this player came in by: the layer
25
+ * left behind if it has room, else the party's, else the usual pick. The deck
26
+ * cannot choose the return place. `not_found` when there is no way back.
27
+ */
28
+ back(): Promise<HubBackResult>;
29
+ /** The current place's door rows and who is inside, read at boot. */
30
+ doors(): Promise<HubDoorsResult>;
31
+ /**
32
+ * Mutual friends in a layer of the current place right now (at most 20);
33
+ * `here` is true for those in the player's own layer. Ghost shows only the
34
+ * friends whose now-playing privacy lets this player see them. `unavailable`
35
+ * on hosts without hub support.
36
+ */
37
+ friends(): Promise<HubFriendsResult>;
38
+ /**
39
+ * Acquire the layer of this place a friend is in. The destination is this
40
+ * same place in another session; join it exactly like a door's
41
+ * (`Multiplayer.join({ mode: "hub", handoff })`), which commits it. The way
42
+ * back is unchanged. `not_found` when the friend is not in another layer.
43
+ */
44
+ jump(userId: string): Promise<HubJumpResult>;
45
+ /**
46
+ * Report another player in this hub session to Castle's moderators, with an
47
+ * optional short reason. Hub chrome only: the host fills in the place and
48
+ * session. `unavailable` on hosts without hub support.
49
+ */
50
+ report(userId: string, reason?: string): Promise<HubReportResult>;
51
+ /**
52
+ * Read this hub session's chat: the messages this player may read, whether
53
+ * they may chat at all, and the user ids who would be sent what they say.
54
+ * Who may chat is ghost's rule (players who follow each other, both over
55
+ * 13), never the deck's. Poll it, passing the last answer's `cursor` as
56
+ * `after` for only what is newer. `unavailable` on hosts without hub support
57
+ * or outside a hub session.
58
+ */
59
+ chat(after?: string): Promise<HubChatResult>;
60
+ /**
61
+ * Say something in this hub session's chat (at most 200 characters). The
62
+ * deck cannot choose who is sent it: ghost sends it to the players here who
63
+ * may chat with this one. `none` when there is nobody, `refused` when ghost
64
+ * will not take it, `limited` when said too fast.
65
+ */
66
+ say(text: string): Promise<HubSayResult>;
67
+ /**
68
+ * Report a chat message this player was returned to Castle's moderators, by
69
+ * its id. Hub chrome only: the host fills in the place and session.
70
+ * `unavailable` on hosts without hub support or outside a hub session.
71
+ */
72
+ reportChat(messageId: string): Promise<HubReportResult>;
73
+ /**
74
+ * Abandon an acquisition the deck will not join (its content failed to load,
75
+ * the player walked away). Optional: an unjoined one expires on its own.
76
+ */
77
+ cancel(): Promise<HubCancelResult>;
78
+ /**
79
+ * The player pressed the host's Back control, which a game cannot hide.
80
+ * Answer it with `back()` and the door animation; a host that hears nothing
81
+ * takes the player home itself. Returns an unsubscribe function.
82
+ */
83
+ onBack(listener: () => void): () => void;
84
+ }
85
+ export declare const Hub: CastleHubApi;