castle-web-sdk 0.4.5 → 0.4.7

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/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # Castle Web SDK Reference
2
+
3
+ `castle-web-sdk` lets a deck use services provided by the Castle
4
+ platform — for example, a deck can save per-player data, post and read
5
+ scores on a leaderboard, or get a server-synced time for daily content.
6
+
7
+ Comes with `castle-web init`. Import what you need from
8
+ `castle-web-sdk`:
9
+
10
+ ```js
11
+ import { setup, initCard, Storage, Leaderboard } from "castle-web-sdk";
12
+ ```
13
+
14
+ ## Contents
15
+
16
+ - [Storage](#storage)
17
+ - [SharedStorage](#sharedstorage)
18
+ - [Leaderboard](#leaderboard)
19
+ - [Time](#time)
20
+ - [User](#user)
21
+ - [Pass](#pass)
22
+ - [Lifecycle](#lifecycle)
23
+ - [Setup](#setup)
24
+ - [CastleError](#castleerror)
25
+
26
+ ## Storage
27
+
28
+ `Storage` saves data for the current player. Nobody else can read it.
29
+ Use it for save files, settings, progress.
30
+
31
+ ### `Storage.get<T>(key): Promise<T | null>`
32
+
33
+ Returns the value at `key`, or `null` if not set.
34
+
35
+ ```js
36
+ const level = (await Storage.get("level")) ?? 1;
37
+ ```
38
+
39
+ ### `Storage.set(key, value)`
40
+
41
+ Sets `key` to `value`. `value` must be something that can convert to
42
+ JSON (`null`, booleans, finite numbers, strings, arrays, plain
43
+ objects). The next `get(key)` returns the new value immediately. Writes
44
+ save in the background.
45
+
46
+ ```js
47
+ Storage.set("level", 7);
48
+ Storage.set("settings", { sound: true, music: false });
49
+ ```
50
+
51
+ ### `Storage.remove(key)`
52
+
53
+ Removes `key`.
54
+
55
+ ## SharedStorage
56
+
57
+ `SharedStorage` saves data that other players can read. Values must
58
+ be something that can convert to JSON, same as `Storage`.
59
+
60
+ Scopes:
61
+
62
+ - `'deck'` — one shared bucket for the whole deck. Any player can read
63
+ or write.
64
+ - `'user'` — a per-player public bucket. Any player can read; only the
65
+ owning player can write.
66
+
67
+ ### `SharedStorage.get(scope, key): Promise<T | null>`
68
+
69
+ Reads a shared value. For `'user'`, omit the user id to read the
70
+ current player's bucket, or pass one to read someone else's:
71
+
72
+ ```js
73
+ const worldHighScore = await SharedStorage.get("deck", "highScore");
74
+ const myColor = await SharedStorage.get("user", "color");
75
+ const theirColor = await SharedStorage.get("user", otherUserId, "color");
76
+ ```
77
+
78
+ ### `SharedStorage.set(scope, key, value)`
79
+
80
+ Writes a shared value. `'user'` writes always go to the current
81
+ player's bucket. Writes save in the background.
82
+
83
+ ```js
84
+ SharedStorage.set("deck", "highScore", 9001);
85
+ SharedStorage.set("user", "color", "red");
86
+ ```
87
+
88
+ ### `SharedStorage.remove(scope, key)`
89
+
90
+ Removes a shared value.
91
+
92
+ ## Leaderboard
93
+
94
+ `Leaderboard` ranks players by a numeric score, per deck and per
95
+ variable name. Pick a variable name for each leaderboard the deck has
96
+ (e.g. `'score'`, `'time'`).
97
+
98
+ ### `Leaderboard.write(variable, score, options?)`
99
+
100
+ Submits a score. Only the player's best score for that `variable` (and
101
+ scope) is kept. In the editor it does nothing — safe to call from
102
+ gameplay code unconditionally.
103
+
104
+ `options` is `{ scope?: string }`. By default the score goes to the
105
+ deck's global leaderboard for `variable`; pass `scope` to write to a
106
+ separate leaderboard (for example a daily one, or a custom id):
107
+
108
+ ```js
109
+ Leaderboard.write("score", 1200);
110
+
111
+ // A daily leaderboard: scope by the current Castle day so each day gets
112
+ // its own board (see Time.getServerDate).
113
+ const { daysSinceCastleEpoch } = await Time.getServerDate();
114
+ Leaderboard.write("score", 1200, { scope: `daily-${daysSinceCastleEpoch}` });
115
+ ```
116
+
117
+ ### `Leaderboard.fetch(variable, type, options?): Promise<LeaderboardData>`
118
+
119
+ Fetches the leaderboard for `variable`. `type` is `'high'` (highest
120
+ first) or `'low'` (lowest first). `options.scope` works the same as in
121
+ `write`.
122
+
123
+ If the player has written a score this session, a `fetch` reflects
124
+ **their own** new score right away — you can `write` then `fetch` and
125
+ show the result without waiting. Other players' recent scores still
126
+ appear on their own normal timing.
127
+
128
+ The returned `LeaderboardData` has:
129
+
130
+ - `list` — array of entries, each `{ place, value, username, userId? }`.
131
+ - `playerRank` — the current player's place on the board (if they have
132
+ a score).
133
+ - `playerValue` — the current player's score (if they have one).
134
+
135
+ ```js
136
+ const data = await Leaderboard.fetch("score", "high");
137
+ for (const entry of data.list) {
138
+ console.log(`${entry.place}. ${entry.username} — ${entry.value}`);
139
+ }
140
+ if (data.playerRank) {
141
+ console.log(`you are #${data.playerRank} with ${data.playerValue}`);
142
+ }
143
+ ```
144
+
145
+ ## Time
146
+
147
+ ### `Time.getServerTime(): Promise<number>`
148
+
149
+ Returns the current server time as a Unix timestamp in seconds.
150
+
151
+ ```js
152
+ const now = await Time.getServerTime();
153
+ ```
154
+
155
+ ### `Time.getServerDate(timezone?): Promise<CastleDateParts>`
156
+
157
+ Returns the current server time broken into date parts. `timezone` is
158
+ `'Castle'` (default; Castle's server timezone, same for every player)
159
+ or `'player'` (the player's local timezone).
160
+
161
+ The returned `CastleDateParts` has:
162
+
163
+ - `sec`, `min`, `hour` — time of day.
164
+ - `day` (1-31), `month` (1-12), `year` — date.
165
+ - `wday` — day of the week (1-7, Sunday = 1).
166
+ - `yday` — day of the year (1-366).
167
+ - `daysSinceCastleEpoch` — a day number that increments every day.
168
+ Use it for daily content.
169
+
170
+ ```js
171
+ const date = await Time.getServerDate("player");
172
+ const dailyPuzzle = (date.daysSinceCastleEpoch % 30) + 1;
173
+ ```
174
+
175
+ ## User
176
+
177
+ ### `User.getCurrent(): Promise<CastleUser>`
178
+
179
+ Returns the signed-in player. Throws `CastleError`
180
+ (`LOGIN_REQUIRED`) when nobody is signed in.
181
+
182
+ The returned `CastleUser` has `userId`, `username`, and `isActive`.
183
+
184
+ ```js
185
+ const me = await User.getCurrent();
186
+ greet(me.username);
187
+ ```
188
+
189
+ ## Pass
190
+
191
+ A pass is something a creator sells to players for Castle bricks (the
192
+ in-app currency): buy it once, own it for good. Use one to gate part of a
193
+ deck behind a purchase — bonus levels, a cosmetic, supporting the
194
+ creator. Set up the pass (name, art, price) on Castle; a deck refers to
195
+ it by id.
196
+
197
+ ### `Pass.has(passId): Promise<boolean>`
198
+
199
+ Returns `true` if the current player owns the pass. No UI, nothing
200
+ charged — use it to gate content.
201
+
202
+ ```js
203
+ if (await Pass.has(bonusLevelsPassId)) {
204
+ showBonusLevels();
205
+ }
206
+ ```
207
+
208
+ ### `Pass.offer(passId): Promise<PassOfferResult>`
209
+
210
+ Presents the pass for the player to buy; resolves when they're done.
211
+ Bricks cost real money, so this only works in the Castle mobile app —
212
+ elsewhere (the website, the dev server) it resolves `unavailable`.
213
+ `PassOfferResult` has a `status`:
214
+
215
+ - `'purchased'` — just bought it; grant access.
216
+ - `'alreadyOwned'` — already had it (not charged); grant access.
217
+ - `'cancelled'` — dismissed without buying.
218
+ - `'unavailable'` — can't buy here (e.g. the website).
219
+
220
+ ```js
221
+ const { status } = await Pass.offer(bonusLevelsPassId);
222
+ if (status === "purchased" || status === "alreadyOwned") {
223
+ showBonusLevels();
224
+ }
225
+ ```
226
+
227
+ ## Lifecycle
228
+
229
+ `Lifecycle` tells the host when the deck has painted its first frame, so
230
+ the Castle feed can reveal it right away instead of waiting out a fixed
231
+ delay.
232
+
233
+ ### `Lifecycle.ready()`
234
+
235
+ Signal that the deck has painted its first presentable frame. Idempotent
236
+ — only the first call has any effect.
237
+
238
+ Kits already wire this up to fire as soon as the first frame paints; if
239
+ you change how the deck first renders, make sure it still fires there. If
240
+ nothing calls this, the feed reveals the deck on its own after a brief
241
+ timeout.
242
+
243
+ If you call it right after kicking off your initial render, wait for the
244
+ next paint first so the host doesn't reveal a blank frame:
245
+
246
+ ```js
247
+ createRoot(root).render(<App />);
248
+ requestAnimationFrame(() => requestAnimationFrame(() => Lifecycle.ready()));
249
+ ```
250
+
251
+ ## Setup
252
+
253
+ Startup, editor-mode check, and a file-write call for editor UI.
254
+
255
+ ### `setup()`
256
+
257
+ Call this once at the start of the deck, before any other SDK call.
258
+ `setup()` initializes the SDK so the rest of the API is usable and
259
+ mounts the centered 5:7 card shell around whatever the deck renders
260
+ into `#root` (when the deck is being played standalone in a browser).
261
+ While running locally with `castle-web serve`, it also forwards
262
+ `console` output to the CLI and reloads the page when `castle-web
263
+ restart` runs.
264
+
265
+ ```js
266
+ import { setup } from "castle-web-sdk";
267
+
268
+ setup();
269
+ ```
270
+
271
+ ### `initCard(): HTMLDivElement`
272
+
273
+ Use this when the deck draws into a `<canvas>` (or anything else)
274
+ rather than into the React tree at `#root`. Returns a centered,
275
+ viewport-sized `<div>` with the standard Castle 5:7 card aspect ratio.
276
+ The div resizes itself when the window resizes.
277
+
278
+ ```js
279
+ import { setup, initCard } from "castle-web-sdk";
280
+
281
+ setup();
282
+ const card = initCard();
283
+
284
+ const canvas = document.createElement("canvas");
285
+ canvas.style.cssText = "width: 100%; height: 100%; display: block;";
286
+ card.appendChild(canvas);
287
+ ```
288
+
289
+ If the deck mounts a React tree into `#root` instead, you don't need
290
+ `initCard()` — `setup()` already wraps `#root`'s children in a card.
291
+
292
+ ### `CARD_RATIO`
293
+
294
+ The card aspect ratio (`5 / 7`). Use this if you need to size something
295
+ to match the card.
296
+
297
+ ### `isEdit(): boolean`
298
+
299
+ `true` when the deck is being edited, `false` when it's being played.
300
+ Use this to show editor UI only in edit mode.
301
+
302
+ ```js
303
+ import { isEdit, setup } from "castle-web-sdk";
304
+
305
+ setup();
306
+ if (isEdit()) {
307
+ mountEditor();
308
+ } else {
309
+ startGame();
310
+ }
311
+ ```
312
+
313
+ ### `writeFile(path, contents): Promise<void>`
314
+
315
+ Writes a file in the deck directory. `path` is relative to the deck
316
+ root, `contents` is a string. Use this from editor UI to save scenes,
317
+ drawings, or generated source.
318
+
319
+ ```js
320
+ import { writeFile } from "castle-web-sdk";
321
+
322
+ await writeFile("scenes/main.scene", JSON.stringify(scene, null, 2));
323
+ ```
324
+
325
+ Only works while editing locally with `castle-web serve`. Calls from a
326
+ published deck fail.
327
+
328
+ ## CastleError
329
+
330
+ Every error the SDK throws is a `CastleError`. Check `code` to tell
331
+ the kinds apart.
332
+
333
+ Common codes:
334
+
335
+ - `LOGIN_REQUIRED` — the player needs to be signed in.
336
+ - `MISSING_DECK_ID` — the deck hasn't been saved to Castle yet, so it
337
+ has no id.
338
+ - `CASTLE_STORAGE_SERIALIZE_FAILED` — value isn't plain JSON (e.g. a
339
+ class instance, a function, a non-finite number, or a cycle).
340
+ - `INVALID_LEADERBOARD_VARIABLE`, `INVALID_LEADERBOARD_SCORE`,
341
+ `INVALID_LEADERBOARD_TYPE` — bad argument to a `Leaderboard` call.
342
+ - `UNSUPPORTED_TIMEZONE` — `Time.getServerDate` got a zone other than
343
+ `'Castle'` or `'player'`.
344
+ - `CASTLE_HOST_UNAVAILABLE` — the Castle host (the app or website running
345
+ the deck) didn't handle the request — e.g. it timed out or wasn't
346
+ reachable. Usually transient; retry or surface a gentle error.
package/dist/castle.d.ts CHANGED
@@ -2,6 +2,8 @@ export { isEdit } from "./context";
2
2
  export { CastleError } from "./errors";
3
3
  export { Leaderboard } from "./leaderboard";
4
4
  export type { LeaderboardData, LeaderboardEntry, LeaderboardOptions, LeaderboardScope, LeaderboardSort, } from "./leaderboard";
5
+ export { Lifecycle } from "./lifecycle";
6
+ export type { CastleLifecycleApi } from "./lifecycle";
5
7
  export { Pass } from "./passes";
6
8
  export type { CastlePassApi, PassOfferResult, PassOfferStatus, } from "./passes";
7
9
  export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
package/dist/castle.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export { isEdit } from "./context";
3
3
  export { CastleError } from "./errors";
4
4
  export { Leaderboard } from "./leaderboard";
5
+ export { Lifecycle } from "./lifecycle";
5
6
  export { Pass } from "./passes";
6
7
  export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
7
8
  export { SharedStorage, Storage } from "./storage";
@@ -115,3 +115,8 @@ export interface CommandResponseEnvelope {
115
115
  error?: SerializedCommandError;
116
116
  }
117
117
  export declare function isResponseEnvelope(value: unknown): value is CommandResponseEnvelope;
118
+ export type LifecycleEvent = "ready";
119
+ export interface LifecycleEnvelope {
120
+ castleSdk: typeof CASTLE_SDK_PROTOCOL;
121
+ lifecycle: LifecycleEvent;
122
+ }
@@ -0,0 +1,4 @@
1
+ export interface CastleLifecycleApi {
2
+ ready(): void;
3
+ }
4
+ export declare const Lifecycle: CastleLifecycleApi;
@@ -0,0 +1,13 @@
1
+ // Deck-side lifecycle signals. The feed reveals the deck when it reports `ready`
2
+ // (its first presentable frame) instead of waiting out a fixed timer.
3
+ import { hostNotify } from "./transport";
4
+ let readySent = false;
5
+ function ready() {
6
+ if (readySent)
7
+ return;
8
+ readySent = true;
9
+ hostNotify("ready");
10
+ }
11
+ export const Lifecycle = {
12
+ ready,
13
+ };
package/dist/runtime.js CHANGED
@@ -250,26 +250,34 @@ async function captureWithHtml2Canvas(target) {
250
250
  return null;
251
251
  }
252
252
  }
253
+ function cropCanvasToCard(card, canvas) {
254
+ const cardRect = card.getBoundingClientRect();
255
+ const c = document.createElement("canvas");
256
+ c.width = cardRect.width * devicePixelRatio;
257
+ c.height = cardRect.height * devicePixelRatio;
258
+ const ctx = c.getContext("2d");
259
+ const canvasRect = canvas.getBoundingClientRect();
260
+ const dx = (canvasRect.left - cardRect.left) * devicePixelRatio;
261
+ const dy = (canvasRect.top - cardRect.top) * devicePixelRatio;
262
+ ctx.drawImage(canvas, dx, dy, canvasRect.width * devicePixelRatio, canvasRect.height * devicePixelRatio);
263
+ return c.toDataURL("image/png");
264
+ }
253
265
  async function captureScreenshot() {
254
- const card = document.getElementById("castle-card");
255
- const canvas = document.querySelector("canvas");
266
+ const card = document.querySelector("#castle-card, [data-castle-card]");
267
+ if (card) {
268
+ const cardCanvas = card.querySelector("canvas");
269
+ if (cardCanvas)
270
+ return cropCanvasToCard(card, cardCanvas);
271
+ const cropped = await captureWithHtml2Canvas(card);
272
+ if (cropped)
273
+ return cropped;
274
+ }
256
275
  if (document.body?.dataset.castleScreenshotTarget === "viewport") {
257
276
  const viewportCapture = await captureWithHtml2Canvas(document.body);
258
277
  if (viewportCapture)
259
278
  return viewportCapture;
260
279
  }
261
- if (card && canvas) {
262
- const cardRect = card.getBoundingClientRect();
263
- const c = document.createElement("canvas");
264
- c.width = cardRect.width * devicePixelRatio;
265
- c.height = cardRect.height * devicePixelRatio;
266
- const ctx = c.getContext("2d");
267
- const canvasRect = canvas.getBoundingClientRect();
268
- const dx = (canvasRect.left - cardRect.left) * devicePixelRatio;
269
- const dy = (canvasRect.top - cardRect.top) * devicePixelRatio;
270
- ctx.drawImage(canvas, dx, dy, canvasRect.width * devicePixelRatio, canvasRect.height * devicePixelRatio);
271
- return c.toDataURL("image/png");
272
- }
280
+ const canvas = document.querySelector("canvas");
273
281
  if (canvas)
274
282
  return canvas.toDataURL("image/png");
275
283
  return captureWithHtml2Canvas(card || document.body);
@@ -1,4 +1,4 @@
1
- import { type CommandName, type CommandParams, type CommandResult } from "./commands";
1
+ import { type CommandName, type CommandParams, type CommandResult, type LifecycleEvent } from "./commands";
2
2
  type PostChannel = "mobile" | "web";
3
3
  interface ReactNativeWebViewBridge {
4
4
  postMessage: (message: string) => void;
@@ -13,4 +13,5 @@ declare global {
13
13
  }
14
14
  export declare function hostRequest<C extends CommandName>(command: C, params: CommandParams[C]): Promise<CommandResult[C]>;
15
15
  export declare function getCommandChannel(): PostChannel | "local";
16
+ export declare function hostNotify(event: LifecycleEvent): void;
16
17
  export {};
package/dist/transport.js CHANGED
@@ -52,6 +52,20 @@ export async function hostRequest(command, params) {
52
52
  export function getCommandChannel() {
53
53
  return resolveChannel();
54
54
  }
55
+ // Fire-and-forget counterpart to hostRequest: no reply, no timeout. The local
56
+ // dev server has no surface to react to lifecycle events, so it's skipped.
57
+ export function hostNotify(event) {
58
+ if (typeof window === "undefined")
59
+ return;
60
+ const channel = resolveChannel();
61
+ if (channel === "local")
62
+ return;
63
+ const envelope = {
64
+ castleSdk: CASTLE_SDK_PROTOCOL,
65
+ lifecycle: event,
66
+ };
67
+ sendEnvelope(channel, envelope);
68
+ }
55
69
  function resolveChannel() {
56
70
  if (typeof window === "undefined")
57
71
  return "local";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "type": "module",
5
5
  "main": "dist/castle.js",
6
6
  "types": "dist/castle.d.ts",
@@ -13,10 +13,9 @@
13
13
  "//host": "host.{js,d.ts} is the host-side executor — deliberately NOT exported and NOT packaged. It is vendored into host repos via scripts/copy-host-module.mjs; decks must never receive it.",
14
14
  "files": [
15
15
  "dist",
16
- "src",
16
+ "README.md",
17
17
  "!dist/host.js",
18
- "!dist/host.d.ts",
19
- "!src/host.ts"
18
+ "!dist/host.d.ts"
20
19
  ],
21
20
  "scripts": {
22
21
  "build": "rm -rf dist && tsc",
package/src/castle.ts DELETED
@@ -1,25 +0,0 @@
1
- // Castle Web SDK
2
-
3
- export { isEdit } from "./context";
4
- export { CastleError } from "./errors";
5
- export { Leaderboard } from "./leaderboard";
6
- export type {
7
- LeaderboardData,
8
- LeaderboardEntry,
9
- LeaderboardOptions,
10
- LeaderboardScope,
11
- LeaderboardSort,
12
- } from "./leaderboard";
13
- export { Pass } from "./passes";
14
- export type {
15
- CastlePassApi,
16
- PassOfferResult,
17
- PassOfferStatus,
18
- } from "./passes";
19
- export { CARD_RATIO, initCard, onBeforeRestart, setup, writeFile } from "./runtime";
20
- export { SharedStorage, Storage } from "./storage";
21
- export { Time } from "./time";
22
- export type { CastleClockZone, CastleDateParts, CastleTimeApi } from "./time";
23
- export type { Json } from "./types";
24
- export { User } from "./user";
25
- export type { CastleUser, CastleUserApi } from "./user";
package/src/commands.ts DELETED
@@ -1,136 +0,0 @@
1
- // The SDK↔host command contract. This is the single source of truth for the
2
- // wire protocol shared by the deck-side command-poster (`transport.ts`) and the
3
- // host-side executor (`host.ts`). It carries ONLY names and types — no GraphQL
4
- // query strings and no auth — so importing it into a deck bundle reveals
5
- // nothing privileged.
6
-
7
- import type { Json } from "./types";
8
-
9
- // Marker + protocol version. Doubles as a discriminator so host messages can't
10
- // be confused with embed.js `castlexyz:` strings or RN console messages.
11
- export const CASTLE_SDK_PROTOCOL = 1;
12
-
13
- export type StorageBlob = Record<string, string>;
14
- export type SharedScope = "deck" | "user";
15
-
16
- export interface StorageUpdate {
17
- key: string;
18
- value: string | null;
19
- }
20
-
21
- // The raw GraphQL leaderboard shape the host returns; the SDK normalizes it
22
- // (and computes playerRank from `currentUserId`) into the public LeaderboardData.
23
- export interface RawLeaderboardEntry {
24
- place?: string | number | null;
25
- score?: string | number | null;
26
- user?: { userId?: string | null; username?: string | null } | null;
27
- }
28
-
29
- export interface RawLeaderboard {
30
- list?: RawLeaderboardEntry[] | null;
31
- yourScore?: { score?: string | number | null } | null;
32
- }
33
-
34
- export type PassOfferStatus =
35
- | "purchased"
36
- | "alreadyOwned"
37
- | "cancelled"
38
- | "unavailable";
39
-
40
- export interface PassOfferResult {
41
- status: PassOfferStatus;
42
- }
43
-
44
- export interface CommandParams {
45
- "deckStorage.load": Record<string, never>;
46
- "deckStorage.update": { updates: StorageUpdate[] };
47
- "sharedDeckStorage.load": {
48
- scope: SharedScope;
49
- userId?: string | null;
50
- keys: string[];
51
- };
52
- "sharedDeckStorage.update": { scope: SharedScope; updates: StorageUpdate[] };
53
- "leaderboard.fetch": {
54
- variable: string;
55
- type: "high" | "low";
56
- scope?: string | null;
57
- // When present, the deck has a freshly-written score for this
58
- // variable+scope that hasn't settled server-side yet. The host routes
59
- // through the leaderboardV2 mutation, which writes this score and returns
60
- // the post-write leaderboard atomically, so the player's own score shows
61
- // up immediately. Absent → a plain read of the settled leaderboard.
62
- score?: number | null;
63
- };
64
- "leaderboard.save": { variable: string; score: number; scope?: string | null };
65
- "user.getCurrent": Record<string, never>;
66
- "time.getServerTime": Record<string, never>;
67
- "pass.has": { passId: string };
68
- // Platform/interactive command — dispatched to the host's platformHandler,
69
- // not graphqlFetch. Unlike the data commands above, this one can stay open
70
- // for a long time (the player interacting with a native sheet), so the
71
- // deck-side transport gives it no timeout.
72
- "pass.offer": { passId: string };
73
- }
74
-
75
- export interface CommandResult {
76
- "deckStorage.load": { blob: StorageBlob };
77
- "deckStorage.update": { blob: StorageBlob };
78
- "sharedDeckStorage.load": { blob: StorageBlob };
79
- "sharedDeckStorage.update": { ok: true };
80
- "leaderboard.fetch": {
81
- leaderboard: RawLeaderboard;
82
- currentUserId: string | null;
83
- };
84
- "leaderboard.save": { ok: true };
85
- "user.getCurrent": { user: { userId: string; username: string } | null };
86
- "time.getServerTime": {
87
- timestamp: number;
88
- timezoneOffset: number;
89
- castleEpochData: Json;
90
- };
91
- "pass.has": { hasPass: boolean };
92
- "pass.offer": PassOfferResult;
93
- }
94
-
95
- export type CommandName = keyof CommandParams;
96
-
97
- // NB: the runtime command allowlist (COMMAND_NAMES / isCommandName) lives in
98
- // host.ts, not here, so that host.ts can keep ALL of its imports type-only and
99
- // compile to a single self-contained file (zero runtime imports) that vendors
100
- // cleanly into Node-ESM / webpack / Metro hosts in other repos.
101
-
102
- // Serializable error that survives the postMessage boundary. The deck-side
103
- // rebuilds a real CastleError from it, preserving `code` so decks can branch.
104
- export interface SerializedCommandError {
105
- code: string;
106
- message: string;
107
- command?: string;
108
- extensions?: Record<string, unknown>;
109
- }
110
-
111
- export interface CommandRequestEnvelope {
112
- castleSdk: typeof CASTLE_SDK_PROTOCOL;
113
- requestId: string;
114
- command: CommandName;
115
- params: unknown;
116
- }
117
-
118
- export interface CommandResponseEnvelope {
119
- castleSdk: typeof CASTLE_SDK_PROTOCOL;
120
- requestId: string;
121
- ok: boolean;
122
- data?: unknown;
123
- error?: SerializedCommandError;
124
- }
125
-
126
- export function isResponseEnvelope(
127
- value: unknown,
128
- ): value is CommandResponseEnvelope {
129
- if (typeof value !== "object" || value === null) return false;
130
- const record = value as Record<string, unknown>;
131
- return (
132
- record.castleSdk === CASTLE_SDK_PROTOCOL &&
133
- typeof record.requestId === "string" &&
134
- typeof record.ok === "boolean"
135
- );
136
- }
package/src/context.ts DELETED
@@ -1,30 +0,0 @@
1
- // Which outer runtime is hosting the deck. Set by each host alongside the
2
- // (now non-secret) CastleEmbed flags; used by transport.ts to pick a channel.
3
- export type CastleHost = "web" | "mobile" | "dev";
4
-
5
- export interface CastleEmbed {
6
- edit?: boolean;
7
- feed?: boolean;
8
- host?: CastleHost;
9
- }
10
-
11
- declare global {
12
- interface Window {
13
- CastleEmbed?: CastleEmbed;
14
- }
15
- }
16
-
17
- export function getCastleEmbed(): CastleEmbed | undefined {
18
- return typeof window === "undefined" ? undefined : window.CastleEmbed;
19
- }
20
-
21
- export function isEdit(): boolean {
22
- try {
23
- const params = new URLSearchParams(window.location.search);
24
- const override = params.get("edit");
25
- if (override === "0" || override === "false") return false;
26
- } catch {
27
- // ignore -- window.location may be unavailable
28
- }
29
- return !!getCastleEmbed()?.edit;
30
- }