castle-web-sdk 0.4.4 → 0.4.6

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/storage.js CHANGED
@@ -1,22 +1,22 @@
1
- import { getAuth, requireAuthToken } from "./auth";
2
- import { getDeckContext, requireDeckId } from "./context";
3
1
  import { CastleError } from "./errors";
4
- import { graphqlRequest } from "./graphql";
2
+ import { hostRequest } from "./transport";
5
3
  const FLUSH_INTERVAL_MS = 2000;
4
+ // Per-player private storage. The host stamps deckId/sessionId, so one deck
5
+ // session = one stable blob: load once, then serve reads from cache and batch
6
+ // writes on a 2s flush. Dirty writes overlay the server blob so an in-flight
7
+ // write isn't clobbered by the flush response.
6
8
  class PrivateStorageImpl {
7
9
  cache = new Map();
8
10
  dirty = new Map();
9
11
  loadPromise = null;
10
- loadedContextKey = null;
11
12
  flushTimer = null;
12
13
  async get(key) {
13
- await this.ensureLoaded("Storage.get");
14
+ await this.ensureLoaded();
14
15
  return (this.cache.get(key) ?? null);
15
16
  }
16
17
  set(key, value) {
17
- const encoded = encodeStorageValue(value, "Storage.set");
18
18
  this.cache.set(key, value);
19
- this.dirty.set(key, encoded);
19
+ this.dirty.set(key, encodeStorageValue(value, "Storage.set"));
20
20
  this.scheduleFlush();
21
21
  }
22
22
  remove(key) {
@@ -24,51 +24,37 @@ class PrivateStorageImpl {
24
24
  this.dirty.set(key, null);
25
25
  this.scheduleFlush();
26
26
  }
27
- async flush() {
28
- if (this.dirty.size === 0)
29
- return;
30
- const context = await currentContext("Storage.flush");
31
- const snapshot = new Map(this.dirty);
32
- const data = await storageGraphql(UPDATE_DECK_STORAGE_MUTATION, {
33
- deckId: context.deckId,
34
- sessionId: context.sessionId,
35
- updates: updatesFromDirty(snapshot),
36
- }, "updateDeckStorage");
37
- this.applyServerBlob(data.updateDeckStorage, contextKey(context.deckId, context.sessionId));
38
- clearAcknowledgedDirty(this.dirty, snapshot);
39
- this.overlayDirty();
40
- }
41
- ensureLoaded(operation) {
27
+ ensureLoaded() {
42
28
  if (!this.loadPromise) {
43
- this.loadPromise = this.load(operation).catch((error) => {
29
+ this.loadPromise = this.load().catch((error) => {
44
30
  this.loadPromise = null;
45
31
  throw error;
46
32
  });
47
33
  }
48
34
  return this.loadPromise;
49
35
  }
50
- async load(operation) {
51
- const context = await currentContext(operation);
52
- const key = contextKey(context.deckId, context.sessionId);
53
- if (this.loadedContextKey === key)
54
- return;
55
- const data = await storageGraphql(DECK_STORAGE_QUERY, { deckId: context.deckId, sessionId: context.sessionId }, "deckStorage");
56
- this.cache = decodeStorageBlob(data.deckStorage, operation);
57
- this.loadedContextKey = key;
36
+ async load() {
37
+ const { blob } = await hostRequest("deckStorage.load", {});
38
+ this.cache = decodeStorageBlob(blob, "Storage.get");
58
39
  this.overlayDirty();
59
40
  }
60
- applyServerBlob(blob, loadedContextKey) {
41
+ async flush() {
42
+ if (this.dirty.size === 0)
43
+ return;
44
+ const snapshot = new Map(this.dirty);
45
+ const { blob } = await hostRequest("deckStorage.update", {
46
+ updates: updatesFromDirty(snapshot),
47
+ });
61
48
  this.cache = decodeStorageBlob(blob, "Storage.flush");
62
- this.loadedContextKey = loadedContextKey;
49
+ clearAcknowledgedDirty(this.dirty, snapshot);
50
+ this.overlayDirty();
63
51
  }
64
52
  overlayDirty() {
65
53
  for (const [key, encoded] of this.dirty) {
66
- if (encoded === null) {
54
+ if (encoded === null)
67
55
  this.cache.delete(key);
68
- }
69
- else {
56
+ else
70
57
  this.cache.set(key, decodeStorageValue(encoded, "Storage.dirty"));
71
- }
72
58
  }
73
59
  }
74
60
  scheduleFlush() {
@@ -80,71 +66,50 @@ class PrivateStorageImpl {
80
66
  }, FLUSH_INTERVAL_MS);
81
67
  }
82
68
  }
69
+ // Cross-player storage. Writes always target the current player (the host
70
+ // forces 'user'-scope writes to whoever is signed in), so dirty writes bucket
71
+ // under a fixed key; reads of another player's 'user' bucket take an explicit
72
+ // userId and never have pending writes. Reads coalesce per microtask into one
73
+ // command with a keys[] array.
83
74
  class SharedStorageImpl {
84
75
  dirtyBuckets = new Map();
85
- buckets = new Map();
86
76
  readBatches = new Map();
87
- pendingWrites = new Set();
88
- pendingWriteErrors = [];
89
77
  flushTimer = null;
90
78
  async get(scope, userOrKey, maybeKey) {
91
- const bucket = await readBucket(scope, userOrKey, maybeKey, "SharedStorage.get");
79
+ assertScope(scope, "SharedStorage.get");
80
+ const userId = scope === "user" && maybeKey ? userOrKey : undefined;
92
81
  const key = maybeKey ?? userOrKey;
93
- await this.awaitPendingWrites("SharedStorage.get");
94
- const dirty = this.peekDirty(bucket, key);
82
+ const dirty = this.peekDirty(scope, userId, key);
95
83
  if (dirty !== undefined) {
96
84
  return (dirty === null ? null : decodeStorageValue(dirty, "SharedStorage.get"));
97
85
  }
98
- return this.queueRead(bucket, key);
86
+ return this.queueRead(scope, userId, key);
99
87
  }
100
88
  set(scope, key, value) {
101
- this.enqueueWrite(scope, key, encodeStorageValue(value, "SharedStorage.set"), "SharedStorage.set");
89
+ assertScope(scope, "SharedStorage.set");
90
+ this.write(scope, key, encodeStorageValue(value, "SharedStorage.set"));
102
91
  }
103
92
  remove(scope, key) {
104
- this.enqueueWrite(scope, key, null, "SharedStorage.remove");
105
- }
106
- async flush() {
107
- await this.awaitPendingWrites("SharedStorage.flush");
108
- if (this.dirtyBuckets.size === 0)
109
- return;
110
- const tasks = Array.from(this.dirtyBuckets, ([bucketKey, dirty]) => this.flushBucket(bucketKey, new Map(dirty)));
111
- await Promise.all(tasks);
93
+ assertScope(scope, "SharedStorage.remove");
94
+ this.write(scope, key, null);
112
95
  }
113
- async write(scope, key, encoded, operation) {
114
- const bucket = await writeBucket(scope, operation);
115
- const bucketKey = makeBucketKey(bucket);
116
- this.buckets.set(bucketKey, bucket);
96
+ write(scope, key, encoded) {
97
+ const bucketKey = writeBucketKey(scope);
117
98
  const dirty = this.dirtyBuckets.get(bucketKey) ?? new Map();
118
99
  dirty.set(key, encoded);
119
100
  this.dirtyBuckets.set(bucketKey, dirty);
120
101
  this.scheduleFlush();
121
102
  }
122
- enqueueWrite(scope, key, encoded, operation) {
123
- const pending = this.write(scope, key, encoded, operation)
124
- .catch((error) => {
125
- this.pendingWriteErrors.push(error);
126
- })
127
- .finally(() => {
128
- this.pendingWrites.delete(pending);
129
- });
130
- this.pendingWrites.add(pending);
103
+ peekDirty(scope, userId, key) {
104
+ if (scope === "user" && userId)
105
+ return undefined;
106
+ return this.dirtyBuckets.get(writeBucketKey(scope))?.get(key);
131
107
  }
132
- async awaitPendingWrites(operation) {
133
- if (this.pendingWrites.size > 0) {
134
- await Promise.all(this.pendingWrites);
135
- }
136
- const error = this.pendingWriteErrors.shift();
137
- if (error instanceof Error)
138
- throw error;
139
- if (error) {
140
- throw storageError("CASTLE_STORAGE_WRITE_FAILED", "Queued shared storage write failed.", operation);
141
- }
142
- }
143
- async queueRead(bucket, key) {
144
- const bucketKey = makeBucketKey(bucket);
108
+ queueRead(scope, userId, key) {
109
+ const bucketKey = readBucketKey(scope, userId);
145
110
  let batch = this.readBatches.get(bucketKey);
146
111
  if (!batch) {
147
- batch = { bucket, reads: new Map(), scheduled: false };
112
+ batch = { bucketKey, scope, userId, reads: new Map(), scheduled: false };
148
113
  this.readBatches.set(bucketKey, batch);
149
114
  }
150
115
  const reads = batch.reads.get(key) ?? [];
@@ -154,11 +119,9 @@ class SharedStorageImpl {
154
119
  });
155
120
  if (!batch.scheduled) {
156
121
  batch.scheduled = true;
157
- queueMicrotask(() => {
158
- void this.flushReadBatch(bucketKey);
159
- });
122
+ queueMicrotask(() => void this.flushReadBatch(bucketKey));
160
123
  }
161
- return (await promise);
124
+ return promise;
162
125
  }
163
126
  async flushReadBatch(bucketKey) {
164
127
  const batch = this.readBatches.get(bucketKey);
@@ -167,9 +130,12 @@ class SharedStorageImpl {
167
130
  this.readBatches.delete(bucketKey);
168
131
  const keys = Array.from(batch.reads.keys());
169
132
  try {
170
- const context = await currentContext("SharedStorage.get");
171
- const data = await storageGraphql(SHARED_DECK_STORAGE_QUERY, sharedVariables(context.deckId, batch.bucket, { keys }), "sharedDeckStorage");
172
- this.resolveReadBatch(batch, data.sharedDeckStorage);
133
+ const { blob } = await hostRequest("sharedDeckStorage.load", {
134
+ scope: batch.scope,
135
+ userId: batch.userId ?? null,
136
+ keys,
137
+ });
138
+ this.resolveReadBatch(batch, blob);
173
139
  }
174
140
  catch (error) {
175
141
  rejectReadBatch(batch, error);
@@ -177,7 +143,7 @@ class SharedStorageImpl {
177
143
  }
178
144
  resolveReadBatch(batch, blob) {
179
145
  for (const [key, reads] of batch.reads) {
180
- const encoded = this.peekDirty(batch.bucket, key) ?? blob[key] ?? null;
146
+ const encoded = this.peekDirty(batch.scope, batch.userId, key) ?? blob[key] ?? null;
181
147
  const value = encoded === null
182
148
  ? null
183
149
  : decodeStorageValue(encoded, "SharedStorage.get");
@@ -185,16 +151,19 @@ class SharedStorageImpl {
185
151
  read.resolve(value);
186
152
  }
187
153
  }
154
+ async flush() {
155
+ if (this.dirtyBuckets.size === 0)
156
+ return;
157
+ const tasks = Array.from(this.dirtyBuckets, ([bucketKey, dirty]) => this.flushBucket(bucketKey, new Map(dirty)));
158
+ await Promise.all(tasks);
159
+ }
188
160
  async flushBucket(bucketKey, snapshot) {
189
161
  if (snapshot.size === 0)
190
162
  return;
191
- const bucket = this.buckets.get(bucketKey);
192
- if (!bucket)
193
- return;
194
- const context = await currentContext("SharedStorage.flush");
195
- await storageGraphql(UPDATE_SHARED_DECK_STORAGE_MUTATION, sharedVariables(context.deckId, bucket, {
163
+ await hostRequest("sharedDeckStorage.update", {
164
+ scope: scopeFromWriteKey(bucketKey),
196
165
  updates: updatesFromDirty(snapshot),
197
- }), "updateSharedDeckStorage");
166
+ });
198
167
  const dirty = this.dirtyBuckets.get(bucketKey);
199
168
  if (!dirty)
200
169
  return;
@@ -202,112 +171,29 @@ class SharedStorageImpl {
202
171
  if (dirty.size === 0)
203
172
  this.dirtyBuckets.delete(bucketKey);
204
173
  }
205
- peekDirty(bucket, key) {
206
- return this.dirtyBuckets.get(makeBucketKey(bucket))?.get(key);
207
- }
208
174
  scheduleFlush() {
209
175
  if (this.flushTimer)
210
176
  return;
211
177
  this.flushTimer = setTimeout(() => {
212
178
  this.flushTimer = null;
213
- void this.flush();
179
+ void this.flush().catch(reportSharedStorageError);
214
180
  }, FLUSH_INTERVAL_MS);
215
181
  }
216
182
  }
217
183
  export const Storage = new PrivateStorageImpl();
218
184
  export const SharedStorage = new SharedStorageImpl();
219
- const DECK_STORAGE_QUERY = `
220
- query CastleDeckStorage($deckId: ID!, $sessionId: ID) {
221
- deckStorage(deckId: $deckId, sessionId: $sessionId)
222
- }
223
- `;
224
- const UPDATE_DECK_STORAGE_MUTATION = `
225
- mutation CastleUpdateDeckStorage(
226
- $deckId: ID!,
227
- $updates: [DeckStorageUpdateInput!]!,
228
- $sessionId: ID
229
- ) {
230
- updateDeckStorage(deckId: $deckId, updates: $updates, sessionId: $sessionId)
231
- }
232
- `;
233
- const SHARED_DECK_STORAGE_QUERY = `
234
- query CastleSharedDeckStorage(
235
- $deckId: ID!,
236
- $keys: [String!]!,
237
- $sessionId: ID,
238
- $userId: ID
239
- ) {
240
- sharedDeckStorage(deckId: $deckId, keys: $keys, sessionId: $sessionId, userId: $userId)
241
- }
242
- `;
243
- const UPDATE_SHARED_DECK_STORAGE_MUTATION = `
244
- mutation CastleUpdateSharedDeckStorage(
245
- $deckId: ID!,
246
- $updates: [SharedDeckStorageUpdateInput!]!,
247
- $sessionId: ID,
248
- $userId: ID
249
- ) {
250
- updateSharedDeckStorage(
251
- deckId: $deckId,
252
- updates: $updates,
253
- sessionId: $sessionId,
254
- userId: $userId
255
- )
256
- }
257
- `;
258
- async function currentContext(operation) {
259
- const deckId = await storageDeckId(operation);
260
- const context = await getDeckContext();
261
- return { deckId, sessionId: context.sessionId ?? undefined };
262
- }
263
- async function storageDeckId(operation) {
264
- try {
265
- return await requireDeckId(operation);
266
- }
267
- catch (error) {
268
- if (error instanceof CastleError && error.code === "MISSING_DECK_ID") {
269
- throw storageError("CASTLE_STORAGE_MISSING_DECK_ID", "Save this deck before using Castle storage.", operation);
270
- }
271
- throw error;
272
- }
185
+ // 'user'-scope writes/self-reads share one bucket (the host resolves the
186
+ // current player); a 'user' read of someone else keys by their id.
187
+ function writeBucketKey(scope) {
188
+ return scope === "deck" ? "deck" : "user:self";
273
189
  }
274
- async function storageGraphql(query, variables, operation) {
275
- const token = await requireAuthToken(operation);
276
- return graphqlRequest(query, variables, {
277
- operation,
278
- requireAuth: true,
279
- token,
280
- });
281
- }
282
- async function readBucket(scope, userOrKey, maybeKey, operation) {
283
- assertScope(scope, operation);
284
- if (scope === "deck")
285
- return withSession({ scope });
286
- if (maybeKey)
287
- return withSession({ scope, userId: userOrKey });
288
- return writeBucket(scope, operation);
289
- }
290
- async function writeBucket(scope, operation) {
291
- assertScope(scope, operation);
190
+ function readBucketKey(scope, userId) {
292
191
  if (scope === "deck")
293
- return withSession({ scope });
294
- const auth = await getAuth();
295
- if (!auth.userId) {
296
- throw storageError("CASTLE_STORAGE_MISSING_USER_ID", "Castle user id is required for user-scoped shared storage.", operation);
297
- }
298
- return withSession({ scope, userId: auth.userId });
192
+ return "deck";
193
+ return userId ? `user:other:${userId}` : "user:self";
299
194
  }
300
- async function withSession(bucket) {
301
- const context = await getDeckContext();
302
- return { ...bucket, sessionId: context.sessionId ?? undefined };
303
- }
304
- function sharedVariables(deckId, bucket, extra) {
305
- return {
306
- deckId,
307
- sessionId: bucket.sessionId,
308
- userId: bucket.userId,
309
- ...extra,
310
- };
195
+ function scopeFromWriteKey(bucketKey) {
196
+ return bucketKey === "deck" ? "deck" : "user";
311
197
  }
312
198
  function updatesFromDirty(dirty) {
313
199
  return Array.from(dirty, ([key, value]) => ({ key, value }));
@@ -362,18 +248,14 @@ function assertJsonValue(value, seen, operation) {
362
248
  throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values must be JSON.", operation);
363
249
  }
364
250
  function assertJsonArray(values, seen, operation) {
365
- if (seen.has(values)) {
366
- throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values cannot be cyclic.", operation);
367
- }
251
+ assertNotCyclic(values, seen, operation);
368
252
  seen.add(values);
369
253
  for (const value of values)
370
254
  assertJsonValue(value, seen, operation);
371
255
  seen.delete(values);
372
256
  }
373
257
  function assertJsonObject(value, seen, operation) {
374
- if (seen.has(value)) {
375
- throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values cannot be cyclic.", operation);
376
- }
258
+ assertNotCyclic(value, seen, operation);
377
259
  const prototype = Object.getPrototypeOf(value);
378
260
  if (prototype !== Object.prototype && prototype !== null) {
379
261
  throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage objects must be plain JSON.", operation);
@@ -383,11 +265,10 @@ function assertJsonObject(value, seen, operation) {
383
265
  assertJsonValue(child, seen, operation);
384
266
  seen.delete(value);
385
267
  }
386
- function makeBucketKey(bucket) {
387
- return `${bucket.scope}:${bucket.userId ?? ""}:${bucket.sessionId ?? ""}`;
388
- }
389
- function contextKey(deckId, sessionId) {
390
- return `${deckId}:${sessionId ?? ""}`;
268
+ function assertNotCyclic(value, seen, operation) {
269
+ if (seen.has(value)) {
270
+ throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values cannot be cyclic.", operation);
271
+ }
391
272
  }
392
273
  function assertScope(scope, operation) {
393
274
  if (scope !== "deck" && scope !== "user") {
@@ -400,6 +281,9 @@ function rejectReadBatch(batch, error) {
400
281
  read.reject(error);
401
282
  }
402
283
  }
284
+ function reportSharedStorageError(error) {
285
+ console.warn("Castle shared storage write failed", error);
286
+ }
403
287
  function storageError(code, message, operation) {
404
288
  return new CastleError({ code, message, operation });
405
289
  }
package/dist/time.js CHANGED
@@ -1,14 +1,5 @@
1
1
  import { CastleError } from "./errors";
2
- import { graphqlRequest } from "./graphql";
3
- const SERVER_TIME_QUERY = `
4
- query CastleServerTime {
5
- serverTime {
6
- timestamp
7
- timezoneOffset
8
- castleEpochData
9
- }
10
- }
11
- `;
2
+ import { hostRequest } from "./transport";
12
3
  let serverTimeSnapshot = null;
13
4
  let serverTimePromise = null;
14
5
  export const Time = {
@@ -38,12 +29,10 @@ async function syncServerTime() {
38
29
  }
39
30
  async function fetchServerTime() {
40
31
  const operation = "Time.getServerTime";
41
- const data = await graphqlRequest(SERVER_TIME_QUERY, undefined, {
42
- operation,
43
- });
44
- const timestamp = numberField(data.serverTime.timestamp, "serverTime.timestamp", operation);
45
- const timezoneOffset = numberField(data.serverTime.timezoneOffset, "serverTime.timezoneOffset", operation);
46
- const epochData = jsonObject(data.serverTime.castleEpochData, "serverTime.castleEpochData", operation);
32
+ const data = await hostRequest("time.getServerTime", {});
33
+ const timestamp = numberField(data.timestamp, "serverTime.timestamp", operation);
34
+ const timezoneOffset = numberField(data.timezoneOffset, "serverTime.timezoneOffset", operation);
35
+ const epochData = jsonObject(data.castleEpochData, "serverTime.castleEpochData", operation);
47
36
  return {
48
37
  offsetSeconds: snappedOffset(timestamp - clientUnixSeconds()),
49
38
  castleTimezoneOffsetMinutes: timezoneOffset,
@@ -0,0 +1,16 @@
1
+ import { type CommandName, type CommandParams, type CommandResult } from "./commands";
2
+ type PostChannel = "mobile" | "web";
3
+ interface ReactNativeWebViewBridge {
4
+ postMessage: (message: string) => void;
5
+ }
6
+ declare global {
7
+ interface Window {
8
+ ReactNativeWebView?: ReactNativeWebViewBridge;
9
+ __castleSdkHost?: {
10
+ receive: (message: unknown) => void;
11
+ };
12
+ }
13
+ }
14
+ export declare function hostRequest<C extends CommandName>(command: C, params: CommandParams[C]): Promise<CommandResult[C]>;
15
+ export declare function getCommandChannel(): PostChannel | "local";
16
+ export {};
@@ -0,0 +1,126 @@
1
+ // Deck-side command-poster. The SDK never makes API calls or holds the auth
2
+ // token; every privileged operation becomes a `command` posted to the outer
3
+ // runtime (host), which validates it, stamps trusted context, runs the call
4
+ // with its own auth, and replies. Three host channels:
5
+ // - mobile → window.ReactNativeWebView.postMessage; host pushes responses
6
+ // back by calling window.__castleSdkHost.receive(...)
7
+ // - web → window.parent.postMessage; responses via 'message' events
8
+ // - local → the castle-web serve dev server, over runtime.ts's websocket
9
+ // Error reconstruction is uniform here so callers always get a CastleError.
10
+ import { CASTLE_SDK_PROTOCOL, isResponseEnvelope, } from "./commands";
11
+ import { getCastleEmbed } from "./context";
12
+ import { CastleError } from "./errors";
13
+ import { sendLocalCommand } from "./runtime";
14
+ const REQUEST_TIMEOUT_MS = 15000;
15
+ // Interactive platform commands hold the screen while the player interacts with
16
+ // a host-native sheet (e.g. a pass purchase), so the flat data-command timeout
17
+ // is wrong for them: they get NO timeout and resolve only when the host replies.
18
+ const INTERACTIVE_COMMANDS = new Set([
19
+ "pass.offer",
20
+ ]);
21
+ let nextRequestId = 1;
22
+ const pending = new Map();
23
+ let listenersInstalled = false;
24
+ export async function hostRequest(command, params) {
25
+ try {
26
+ const channel = resolveChannel();
27
+ const env = channel === "local"
28
+ ? await sendLocalCommand(command, params)
29
+ : await postCommand(channel, command, params);
30
+ return interpretResponse(command, env);
31
+ }
32
+ catch (error) {
33
+ // Honor the SDK contract that every thrown error is a CastleError. Errors
34
+ // surfaced by the host (interpretResponse) are already CastleErrors; this
35
+ // wraps transport-level failures (host timeout / unreachable / dev server
36
+ // disconnected) that would otherwise be plain Errors.
37
+ if (error instanceof CastleError)
38
+ throw error;
39
+ throw new CastleError({
40
+ code: "CASTLE_HOST_UNAVAILABLE",
41
+ message: error instanceof Error
42
+ ? error.message
43
+ : `Castle host did not handle ${command}.`,
44
+ operation: command,
45
+ });
46
+ }
47
+ }
48
+ // Exposed so capability modules (e.g. passes) can tell whether the current host
49
+ // has its own UI surface over the deck. "mobile"/"web" hosts render their own
50
+ // purchase/upsell UI; the "local" dev server has none, so the SDK itself shows
51
+ // a minimal in-page notice there.
52
+ export function getCommandChannel() {
53
+ return resolveChannel();
54
+ }
55
+ function resolveChannel() {
56
+ if (typeof window === "undefined")
57
+ return "local";
58
+ if (window.ReactNativeWebView)
59
+ return "mobile";
60
+ const host = getCastleEmbed()?.host;
61
+ if (host === "web")
62
+ return "web";
63
+ if (host === "dev")
64
+ return "local";
65
+ // Fallback: an iframe with a parent is the web player; otherwise assume the
66
+ // local dev server (top-level page served by `castle-web serve`).
67
+ return window.parent && window.parent !== window ? "web" : "local";
68
+ }
69
+ function postCommand(channel, command, params) {
70
+ installResponseListener();
71
+ const requestId = `csdk_${nextRequestId++}`;
72
+ return new Promise((resolve, reject) => {
73
+ const timeout = INTERACTIVE_COMMANDS.has(command)
74
+ ? undefined
75
+ : setTimeout(() => {
76
+ pending.delete(requestId);
77
+ reject(new Error(`Castle host did not respond to ${command}.`));
78
+ }, REQUEST_TIMEOUT_MS);
79
+ pending.set(requestId, { resolve, reject, timeout });
80
+ sendEnvelope(channel, { castleSdk: CASTLE_SDK_PROTOCOL, requestId, command, params });
81
+ });
82
+ }
83
+ function sendEnvelope(channel, envelope) {
84
+ const json = JSON.stringify(envelope);
85
+ if (channel === "mobile") {
86
+ window.ReactNativeWebView?.postMessage(json);
87
+ }
88
+ else {
89
+ window.parent.postMessage(envelope, "*");
90
+ }
91
+ }
92
+ // The mobile host can't dispatch a DOM 'message' event, so it calls this global
93
+ // directly with the parsed envelope. The web host posts a 'message' event.
94
+ function installResponseListener() {
95
+ if (listenersInstalled || typeof window === "undefined")
96
+ return;
97
+ listenersInstalled = true;
98
+ window.__castleSdkHost = { receive: (message) => settle(message) };
99
+ window.addEventListener("message", (event) => {
100
+ settle(event.data);
101
+ });
102
+ }
103
+ function settle(message) {
104
+ if (!isResponseEnvelope(message))
105
+ return;
106
+ const entry = pending.get(message.requestId);
107
+ if (!entry)
108
+ return;
109
+ if (entry.timeout)
110
+ clearTimeout(entry.timeout);
111
+ pending.delete(message.requestId);
112
+ entry.resolve(message);
113
+ }
114
+ function interpretResponse(command, env) {
115
+ if (env.ok)
116
+ return env.data;
117
+ throw fromSerializedError(env.error, command);
118
+ }
119
+ function fromSerializedError(error, command) {
120
+ return new CastleError({
121
+ code: error?.code ?? "CASTLE_HOST_ERROR",
122
+ message: error?.message ?? `Castle command ${command} failed.`,
123
+ operation: error?.command ?? command,
124
+ extensions: error?.extensions,
125
+ });
126
+ }
package/dist/user.js CHANGED
@@ -1,14 +1,5 @@
1
- import { requireAuthToken } from "./auth";
2
1
  import { CastleError } from "./errors";
3
- import { graphqlRequest } from "./graphql";
4
- const CURRENT_USER_QUERY = `
5
- query CastleCurrentUser {
6
- me {
7
- userId
8
- username
9
- }
10
- }
11
- `;
2
+ import { hostRequest } from "./transport";
12
3
  let currentUser = null;
13
4
  let currentUserPromise = null;
14
5
  export const User = {
@@ -25,25 +16,17 @@ async function getCurrent() {
25
16
  }
26
17
  async function fetchCurrentUser() {
27
18
  const operation = "User.getCurrent";
28
- const token = await requireAuthToken(operation);
29
- const data = await graphqlRequest(CURRENT_USER_QUERY, undefined, {
30
- operation,
31
- requireAuth: true,
32
- token,
33
- });
34
- if (!data.me) {
19
+ const { user } = await hostRequest("user.getCurrent", {});
20
+ if (!user) {
35
21
  throw new CastleError({
36
22
  code: "LOGIN_REQUIRED",
37
23
  message: "Log in to Castle before using User.getCurrent().",
38
24
  operation,
39
25
  });
40
26
  }
41
- return normalizeCurrentUser(data.me, operation);
42
- }
43
- function normalizeCurrentUser(user, operation) {
44
27
  return {
45
- userId: requiredString(user.userId, "me.userId", operation),
46
- username: requiredString(user.username, "me.username", operation),
28
+ userId: requiredString(user.userId, "user.userId", operation),
29
+ username: requiredString(user.username, "user.username", operation),
47
30
  isActive: true,
48
31
  };
49
32
  }
@@ -52,7 +35,7 @@ function requiredString(value, field, operation) {
52
35
  return value;
53
36
  throw new CastleError({
54
37
  code: "GRAPHQL_BAD_DATA",
55
- message: `Castle GraphQL response did not include ${field}.`,
38
+ message: `Castle response did not include ${field}.`,
56
39
  operation,
57
40
  });
58
41
  }