castle-web-cli 0.4.59 → 0.4.60

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/api.d.ts CHANGED
@@ -1,3 +1,11 @@
1
+ interface GraphqlResponse {
2
+ data?: Record<string, unknown>;
3
+ errors?: Array<{
4
+ message?: string;
5
+ extensions?: Record<string, unknown>;
6
+ }>;
7
+ }
8
+ export declare function graphql(query: string, variables?: Record<string, unknown>): Promise<GraphqlResponse>;
1
9
  export declare function startCLILogin(): Promise<{
2
10
  pollToken: string;
3
11
  url: string;
@@ -71,3 +79,4 @@ export interface WebDeckSource {
71
79
  }
72
80
  export declare function webDeckSource(deckId: string): Promise<WebDeckSource | null>;
73
81
  export declare function saveWebDeckSource(deckId: string, uploadId: string): Promise<WebDeckSource>;
82
+ export {};
package/dist/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as config from './config.js';
2
2
  const API_HOST = 'https://api.castle.xyz/graphql';
3
- async function graphql(query, variables) {
3
+ export async function graphql(query, variables) {
4
4
  const token = config.getToken();
5
5
  const headers = {
6
6
  'X-OS': 'cli',
@@ -0,0 +1,47 @@
1
+ // Hand-written declaration for the vendored, self-contained host.js (runtime is
2
+ // copied here by scripts/copy-host-module.mjs). Kept tiny and import-free on
3
+ // purpose — the SDK's emitted .d.ts use extensionless imports that the CLI's
4
+ // Node16 resolution rejects. Mirror the public surface of
5
+ // castle-experimental-web/sdk/src/host.ts; update if that signature changes.
6
+
7
+ export interface HostContext {
8
+ deckId: string | null;
9
+ sessionId: string | null;
10
+ userId: string | null;
11
+ username: string | null;
12
+ }
13
+
14
+ export interface GraphqlError {
15
+ message?: string;
16
+ extensions?: Record<string, unknown>;
17
+ }
18
+
19
+ export interface GraphqlResponse {
20
+ data?: unknown;
21
+ errors?: GraphqlError[];
22
+ }
23
+
24
+ export type GraphqlFetch = (
25
+ query: string,
26
+ variables: Record<string, unknown>,
27
+ ) => Promise<GraphqlResponse>;
28
+
29
+ export interface SerializedCommandError {
30
+ code: string;
31
+ message: string;
32
+ command?: string;
33
+ extensions?: Record<string, unknown>;
34
+ }
35
+
36
+ export interface HostResult {
37
+ ok: boolean;
38
+ data?: unknown;
39
+ error?: SerializedCommandError;
40
+ }
41
+
42
+ export function executeCommand(
43
+ ctx: HostContext,
44
+ command: unknown,
45
+ params: unknown,
46
+ graphqlFetch: GraphqlFetch,
47
+ ): Promise<HostResult>;
@@ -0,0 +1,373 @@
1
+ // GENERATED — do not edit. Vendored from castle-experimental-web/sdk/dist/host.js
2
+ // Re-run `npm run copy-host` in castle-experimental-web after an SDK change.
3
+
4
+ // Host-side command executor — the shared module run by every trusted host
5
+ // (web DeckPlayer, mobile React Native, the castle-web dev server). It takes a
6
+ // command + params from an untrusted deck, validates it against the allowlist,
7
+ // stamps trusted context (deckId/sessionId/userId — never taken from the deck),
8
+ // builds the GraphQL query, runs it via the host's own authed fetch, and
9
+ // returns a serializable result/error envelope. The deck never sees a token or
10
+ // builds a query. This file is NOT imported by deck-side code.
11
+ //
12
+ // Usage from a host:
13
+ // import { executeCommand } from "castle-web-sdk/host";
14
+ // const result = await executeCommand(ctx, command, params, graphqlFetch);
15
+ // // reply { castleSdk: 1, requestId, ...result } back to the deck.
16
+ // Runtime command allowlist — kept here (not in commands.ts) so the import
17
+ // above stays type-only. Must stay in sync with CommandParams in commands.ts.
18
+ const COMMAND_NAMES = [
19
+ "deckStorage.load",
20
+ "deckStorage.update",
21
+ "sharedDeckStorage.load",
22
+ "sharedDeckStorage.update",
23
+ "leaderboard.fetch",
24
+ "leaderboard.save",
25
+ "user.getCurrent",
26
+ "time.getServerTime",
27
+ ];
28
+ function isCommandName(value) {
29
+ return (typeof value === "string" &&
30
+ COMMAND_NAMES.includes(value));
31
+ }
32
+ class HostCommandError extends Error {
33
+ code;
34
+ command;
35
+ extensions;
36
+ constructor(code, message, command, extensions) {
37
+ super(message);
38
+ this.name = "HostCommandError";
39
+ this.code = code;
40
+ this.command = command;
41
+ this.extensions = extensions;
42
+ }
43
+ }
44
+ export async function executeCommand(ctx, command, params, graphqlFetch) {
45
+ if (!isCommandName(command)) {
46
+ return {
47
+ ok: false,
48
+ error: {
49
+ code: "UNKNOWN_COMMAND",
50
+ message: "Unknown Castle command.",
51
+ command: typeof command === "string" ? command : undefined,
52
+ },
53
+ };
54
+ }
55
+ try {
56
+ const data = await runCommand(ctx, command, (params ?? {}), graphqlFetch);
57
+ return { ok: true, data };
58
+ }
59
+ catch (error) {
60
+ return { ok: false, error: toSerializedError(error, command) };
61
+ }
62
+ }
63
+ function runCommand(ctx, command, params, gql) {
64
+ switch (command) {
65
+ case "deckStorage.load":
66
+ return deckStorageLoad(ctx, gql);
67
+ case "deckStorage.update":
68
+ return deckStorageUpdate(ctx, params, gql);
69
+ case "sharedDeckStorage.load":
70
+ return sharedDeckStorageLoad(ctx, params, gql);
71
+ case "sharedDeckStorage.update":
72
+ return sharedDeckStorageUpdate(ctx, params, gql);
73
+ case "leaderboard.fetch":
74
+ return leaderboardFetch(ctx, params, gql);
75
+ case "leaderboard.save":
76
+ return leaderboardSave(ctx, params, gql);
77
+ case "user.getCurrent":
78
+ return Promise.resolve(userGetCurrent(ctx));
79
+ case "time.getServerTime":
80
+ return timeGetServerTime(gql);
81
+ }
82
+ }
83
+ async function deckStorageLoad(ctx, gql) {
84
+ const deckId = requireDeckId(ctx, "deckStorage.load");
85
+ const data = await graphql(gql, DECK_STORAGE_QUERY, { deckId, sessionId: ctx.sessionId }, "deckStorage.load");
86
+ return { blob: data.deckStorage ?? {} };
87
+ }
88
+ async function deckStorageUpdate(ctx, params, gql) {
89
+ const deckId = requireDeckId(ctx, "deckStorage.update");
90
+ const data = await graphql(gql, UPDATE_DECK_STORAGE_MUTATION, {
91
+ deckId,
92
+ sessionId: ctx.sessionId,
93
+ updates: asUpdates(params.updates),
94
+ }, "deckStorage.update");
95
+ return { blob: data.updateDeckStorage ?? {} };
96
+ }
97
+ async function sharedDeckStorageLoad(ctx, params, gql) {
98
+ const deckId = requireDeckId(ctx, "sharedDeckStorage.load");
99
+ const scope = asScope(params.scope, "sharedDeckStorage.load");
100
+ // 'user' read: an explicit userId targets another player's public bucket;
101
+ // otherwise the current player's. 'deck' scope ignores userId.
102
+ const userId = scope === "user" ? asOptionalString(params.userId) ?? ctx.userId : null;
103
+ const data = await graphql(gql, SHARED_DECK_STORAGE_QUERY, { deckId, sessionId: ctx.sessionId, userId, keys: asKeys(params.keys) }, "sharedDeckStorage.load");
104
+ return { blob: data.sharedDeckStorage ?? {} };
105
+ }
106
+ async function sharedDeckStorageUpdate(ctx, params, gql) {
107
+ const deckId = requireDeckId(ctx, "sharedDeckStorage.update");
108
+ const scope = asScope(params.scope, "sharedDeckStorage.update");
109
+ // 'user'-scope writes are forced to the current player — the deck cannot
110
+ // write another player's bucket.
111
+ const userId = scope === "user" ? requireUserId(ctx, "sharedDeckStorage.update") : null;
112
+ await graphql(gql, UPDATE_SHARED_DECK_STORAGE_MUTATION, { deckId, sessionId: ctx.sessionId, userId, updates: asUpdates(params.updates) }, "sharedDeckStorage.update");
113
+ return { ok: true };
114
+ }
115
+ async function leaderboardFetch(ctx, params, gql) {
116
+ const deckId = requireDeckId(ctx, "leaderboard.fetch");
117
+ const variables = {
118
+ deckId,
119
+ variable: asString(params.variable, "variable", "leaderboard.fetch"),
120
+ type: asLeaderboardType(params.type),
121
+ filter: "dedupUsers",
122
+ includeFollowList: false,
123
+ includeParties: false,
124
+ scope: asOptionalString(params.scope) ?? null,
125
+ };
126
+ // A non-null score means the deck just wrote this value and wants its own
127
+ // score reflected immediately: write-and-read atomically via leaderboardV2
128
+ // (mirrors the engine's getLeaderboard path in core/src/leaderboards.cpp).
129
+ // No score → plain read of the settled leaderboard.
130
+ const score = asOptionalNumber(params.score);
131
+ if (score !== null) {
132
+ const data = await graphql(gql, LEADERBOARD_V2_MUTATION, { ...variables, score }, "leaderboard.fetch");
133
+ return { leaderboard: data.leaderboardV2, currentUserId: ctx.userId };
134
+ }
135
+ const data = await graphql(gql, LEADERBOARD_QUERY, variables, "leaderboard.fetch");
136
+ return { leaderboard: data.leaderboard, currentUserId: ctx.userId };
137
+ }
138
+ async function leaderboardSave(ctx, params, gql) {
139
+ const deckId = requireDeckId(ctx, "leaderboard.save");
140
+ await graphql(gql, SAVE_LEADERBOARD_MUTATION, {
141
+ deckId,
142
+ variable: asString(params.variable, "variable", "leaderboard.save"),
143
+ score: asNumber(params.score, "score", "leaderboard.save"),
144
+ scope: asOptionalString(params.scope) ?? null,
145
+ }, "leaderboard.save");
146
+ return { ok: true };
147
+ }
148
+ function userGetCurrent(ctx) {
149
+ if (!ctx.userId || !ctx.username)
150
+ return { user: null };
151
+ return { user: { userId: ctx.userId, username: ctx.username } };
152
+ }
153
+ async function timeGetServerTime(gql) {
154
+ const data = await graphql(gql, SERVER_TIME_QUERY, {}, "time.getServerTime");
155
+ return {
156
+ timestamp: data.serverTime.timestamp,
157
+ timezoneOffset: data.serverTime.timezoneOffset,
158
+ castleEpochData: data.serverTime.castleEpochData,
159
+ };
160
+ }
161
+ async function graphql(gql, query, variables, command) {
162
+ const result = await gql(query, variables);
163
+ if (result.errors && result.errors.length > 0) {
164
+ const first = result.errors[0];
165
+ const code = first?.extensions?.code;
166
+ throw new HostCommandError(typeof code === "string" ? code : "GRAPHQL_ERROR", first?.message ?? "Castle GraphQL request failed.", command, first?.extensions);
167
+ }
168
+ if (result.data === null || result.data === undefined) {
169
+ throw new HostCommandError("GRAPHQL_NO_DATA", "Castle GraphQL response did not include data.", command);
170
+ }
171
+ return result.data;
172
+ }
173
+ function requireDeckId(ctx, command) {
174
+ if (ctx.deckId)
175
+ return ctx.deckId;
176
+ throw new HostCommandError("MISSING_DECK_ID", "Save this deck to Castle before using this API.", command);
177
+ }
178
+ function requireUserId(ctx, command) {
179
+ if (ctx.userId)
180
+ return ctx.userId;
181
+ throw new HostCommandError("LOGIN_REQUIRED", "Log in to Castle before using this API.", command);
182
+ }
183
+ function toSerializedError(error, command) {
184
+ if (error instanceof HostCommandError) {
185
+ return {
186
+ code: error.code,
187
+ message: error.message,
188
+ command: error.command ?? command,
189
+ extensions: error.extensions,
190
+ };
191
+ }
192
+ return {
193
+ code: "CASTLE_HOST_ERROR",
194
+ message: error instanceof Error ? error.message : "Castle command failed.",
195
+ command,
196
+ };
197
+ }
198
+ function asUpdates(value) {
199
+ if (!Array.isArray(value))
200
+ return [];
201
+ return value.flatMap((entry) => {
202
+ if (typeof entry !== "object" || entry === null)
203
+ return [];
204
+ const record = entry;
205
+ if (typeof record.key !== "string")
206
+ return [];
207
+ const raw = record.value;
208
+ return [{ key: record.key, value: typeof raw === "string" ? raw : null }];
209
+ });
210
+ }
211
+ function asKeys(value) {
212
+ if (!Array.isArray(value))
213
+ return [];
214
+ return value.filter((key) => typeof key === "string");
215
+ }
216
+ function asScope(value, command) {
217
+ if (value === "deck" || value === "user")
218
+ return value;
219
+ throw new HostCommandError("CASTLE_STORAGE_INVALID_SCOPE", 'SharedStorage scope must be "deck" or "user".', command);
220
+ }
221
+ function asLeaderboardType(value) {
222
+ if (value === "high" || value === "low")
223
+ return value;
224
+ throw new HostCommandError("INVALID_LEADERBOARD_TYPE", "Leaderboard type must be high or low.", "leaderboard.fetch");
225
+ }
226
+ function asString(value, field, command) {
227
+ if (typeof value === "string" && value.length > 0)
228
+ return value;
229
+ throw new HostCommandError("INVALID_ARGUMENT", `Castle command ${command} requires ${field}.`, command);
230
+ }
231
+ function asNumber(value, field, command) {
232
+ if (typeof value === "number" && Number.isFinite(value))
233
+ return value;
234
+ throw new HostCommandError("INVALID_ARGUMENT", `Castle command ${command} requires a numeric ${field}.`, command);
235
+ }
236
+ function asOptionalString(value) {
237
+ return typeof value === "string" && value.length > 0 ? value : null;
238
+ }
239
+ function asOptionalNumber(value) {
240
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
241
+ }
242
+ const DECK_STORAGE_QUERY = `
243
+ query CastleDeckStorage($deckId: ID!, $sessionId: ID) {
244
+ deckStorage(deckId: $deckId, sessionId: $sessionId)
245
+ }
246
+ `;
247
+ const UPDATE_DECK_STORAGE_MUTATION = `
248
+ mutation CastleUpdateDeckStorage(
249
+ $deckId: ID!,
250
+ $updates: [DeckStorageUpdateInput!]!,
251
+ $sessionId: ID
252
+ ) {
253
+ updateDeckStorage(deckId: $deckId, updates: $updates, sessionId: $sessionId)
254
+ }
255
+ `;
256
+ const SHARED_DECK_STORAGE_QUERY = `
257
+ query CastleSharedDeckStorage(
258
+ $deckId: ID!,
259
+ $keys: [String!]!,
260
+ $sessionId: ID,
261
+ $userId: ID
262
+ ) {
263
+ sharedDeckStorage(deckId: $deckId, keys: $keys, sessionId: $sessionId, userId: $userId)
264
+ }
265
+ `;
266
+ const UPDATE_SHARED_DECK_STORAGE_MUTATION = `
267
+ mutation CastleUpdateSharedDeckStorage(
268
+ $deckId: ID!,
269
+ $updates: [SharedDeckStorageUpdateInput!]!,
270
+ $sessionId: ID,
271
+ $userId: ID
272
+ ) {
273
+ updateSharedDeckStorage(
274
+ deckId: $deckId,
275
+ updates: $updates,
276
+ sessionId: $sessionId,
277
+ userId: $userId
278
+ )
279
+ }
280
+ `;
281
+ const LEADERBOARD_QUERY = `
282
+ query CastleLeaderboard(
283
+ $deckId: ID!
284
+ $variable: String!
285
+ $type: LeaderboardType!
286
+ $filter: LeaderboardFilter!
287
+ $includeFollowList: Boolean
288
+ $includeParties: Boolean
289
+ $scope: String
290
+ ) {
291
+ leaderboard(
292
+ deckId: $deckId
293
+ variable: $variable
294
+ type: $type
295
+ filter: $filter
296
+ includeFollowList: $includeFollowList
297
+ includeParties: $includeParties
298
+ scope: $scope
299
+ ) {
300
+ yourScore { score }
301
+ list {
302
+ place
303
+ score
304
+ user {
305
+ userId
306
+ username
307
+ }
308
+ }
309
+ }
310
+ }
311
+ `;
312
+ // Same args + selection as LEADERBOARD_QUERY, plus the required `score`. Unlike
313
+ // saveVariableToLeaderboard, this writes the score AND returns the post-write
314
+ // leaderboard in one round trip, so the deck can show its own fresh score
315
+ // without waiting for server-side settling. Mirrors the engine's leaderboardV2
316
+ // use in core/src/leaderboards.cpp.
317
+ const LEADERBOARD_V2_MUTATION = `
318
+ mutation CastleLeaderboardV2(
319
+ $deckId: ID!
320
+ $variable: String!
321
+ $type: LeaderboardType!
322
+ $filter: LeaderboardFilter!
323
+ $score: Float!
324
+ $includeFollowList: Boolean
325
+ $includeParties: Boolean
326
+ $scope: String
327
+ ) {
328
+ leaderboardV2(
329
+ deckId: $deckId
330
+ variable: $variable
331
+ type: $type
332
+ filter: $filter
333
+ score: $score
334
+ includeFollowList: $includeFollowList
335
+ includeParties: $includeParties
336
+ scope: $scope
337
+ ) {
338
+ yourScore { score }
339
+ list {
340
+ place
341
+ score
342
+ user {
343
+ userId
344
+ username
345
+ }
346
+ }
347
+ }
348
+ }
349
+ `;
350
+ const SAVE_LEADERBOARD_MUTATION = `
351
+ mutation CastleSaveVariableToLeaderboard(
352
+ $deckId: ID!
353
+ $variable: String!
354
+ $score: Float!
355
+ $scope: String
356
+ ) {
357
+ saveVariableToLeaderboard(
358
+ deckId: $deckId
359
+ variable: $variable
360
+ score: $score
361
+ scope: $scope
362
+ )
363
+ }
364
+ `;
365
+ const SERVER_TIME_QUERY = `
366
+ query CastleServerTime {
367
+ serverTime {
368
+ timestamp
369
+ timezoneOffset
370
+ castleEpochData
371
+ }
372
+ }
373
+ `;