@bounded-sh/core 0.0.14 → 0.0.16

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/index.js CHANGED
@@ -3800,24 +3800,19 @@ async function buildSetDocumentsTransaction(connection, idl, anchorProvider, pay
3800
3800
  /* ------------------------------------------------------------------ */
3801
3801
  /* ENV helpers */
3802
3802
  /* ------------------------------------------------------------------ */
3803
- // Canonical `BOUNDED_PRIVATE_KEY` (matches the CLI); legacy
3804
- // `BOUNDED_SOLANA_KEYPAIR` still honored. Only consulted when no explicit
3805
- // keypair was provided (createWalletClient passes one).
3806
- const ENV_KEYPAIRS = ["BOUNDED_PRIVATE_KEY", "BOUNDED_SOLANA_KEYPAIR"];
3803
+ // Canonical `BOUNDED_PRIVATE_KEY` (matches the CLI). Only consulted when no
3804
+ // explicit keypair was provided (createWalletClient passes one).
3805
+ const ENV_KEYPAIR = "BOUNDED_PRIVATE_KEY";
3806
+ const LEGACY_ENV_KEYPAIR = "BOUNDED_SOLANA_KEYPAIR";
3807
3807
  function loadKeypairFromEnv() {
3808
- let secret;
3809
- let found;
3810
- for (const name of ENV_KEYPAIRS) {
3811
- const v = process.env[name];
3812
- if (v) {
3813
- secret = v;
3814
- found = name;
3815
- break;
3816
- }
3808
+ if (process.env[LEGACY_ENV_KEYPAIR]) {
3809
+ throw new Error(`${LEGACY_ENV_KEYPAIR} is no longer supported. Set ${ENV_KEYPAIR} instead, ` +
3810
+ `or pass an explicit keypair via createWalletClient({ keypair }).`);
3817
3811
  }
3812
+ const secret = process.env[ENV_KEYPAIR];
3818
3813
  if (!secret) {
3819
3814
  throw new Error(`No server keypair for this top-level call. The top-level get/set/subscribe/etc. use an ` +
3820
- `AMBIENT session — set ${ENV_KEYPAIRS[0]} to a base-58 secret key (or JSON array) to provide one. ` +
3815
+ `AMBIENT session — set ${ENV_KEYPAIR} to a base-58 secret key (or JSON array) to provide one. ` +
3821
3816
  `If you already created a wallet with createWalletClient({ keypair }), call ITS methods instead ` +
3822
3817
  `(client.subscribe / client.set / client.get): that client is self-contained and deliberately does ` +
3823
3818
  `not set the ambient session, so the top-level functions can't see it.`);
@@ -3829,7 +3824,7 @@ function loadKeypairFromEnv() {
3829
3824
  return web3_js.Keypair.fromSecretKey(secretKey);
3830
3825
  }
3831
3826
  catch (err) {
3832
- throw new Error(`Unable to parse ${found}. Ensure it is valid base-58 or JSON.`);
3827
+ throw new Error(`Unable to parse ${ENV_KEYPAIR}. Ensure it is valid base-58 or JSON.`);
3833
3828
  }
3834
3829
  }
3835
3830
  /* ------------------------------------------------------------------ */
@@ -3896,7 +3891,7 @@ class ServerSessionManager {
3896
3891
  this.session = session;
3897
3892
  }
3898
3893
  /* ---------------------------------------------- *
3899
- * CLEAR (e.g. after logout or 401 retry)
3894
+ * CLEAR (e.g. after explicit logout)
3900
3895
  * ---------------------------------------------- */
3901
3896
  clearSession() {
3902
3897
  this.session = null;
@@ -3920,11 +3915,6 @@ class ServerSessionManager {
3920
3915
  /* The default singleton instance (reads keypair from env) */
3921
3916
  ServerSessionManager.instance = new ServerSessionManager();
3922
3917
 
3923
- var serverSessionManager = /*#__PURE__*/Object.freeze({
3924
- __proto__: null,
3925
- ServerSessionManager: ServerSessionManager
3926
- });
3927
-
3928
3918
  /**
3929
3919
  * Safe base64 helpers for bounded-core.
3930
3920
  *
@@ -4149,13 +4139,6 @@ async function refreshAuthSessionOnce(appId, isServer) {
4149
4139
  async function makeApiRequest(method, urlPath, data, _overrides) {
4150
4140
  var _a, _b, _c, _d, _e, _f, _g, _h;
4151
4141
  const config = await getConfig();
4152
- let hasRetriedAfterServerSessionReset = false;
4153
- const clearServerSession = async () => {
4154
- if (!config.isServer)
4155
- return;
4156
- const { ServerSessionManager } = await Promise.resolve().then(function () { return serverSessionManager; });
4157
- ServerSessionManager.instance.clearSession();
4158
- };
4159
4142
  async function executeRequest() {
4160
4143
  var _a;
4161
4144
  // When _getAuthHeaders is provided (wallet client), use it as the sole auth source.
@@ -4205,13 +4188,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4205
4188
  }
4206
4189
  return await executeRequest();
4207
4190
  }
4208
- catch (_refreshError) {
4209
- // Server-side fallback: clear global session and retry once
4210
- if (config.isServer && !hasRetriedAfterServerSessionReset) {
4211
- hasRetriedAfterServerSessionReset = true;
4212
- await clearServerSession();
4213
- return await executeRequest();
4214
- }
4191
+ catch (_j) {
4215
4192
  throw error;
4216
4193
  }
4217
4194
  }
@@ -4358,99 +4335,54 @@ function normalizeReadResult(responseData, pathIsDocument) {
4358
4335
  }
4359
4336
  return responseData;
4360
4337
  }
4361
- function hashForKey$1(value) {
4338
+ function hashForKey$2(value) {
4362
4339
  let h = 5381;
4363
4340
  for (let i = 0; i < value.length; i++) {
4364
4341
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
4365
4342
  }
4366
4343
  return h.toString(36);
4367
4344
  }
4368
- /**
4369
- * Derive a stable identity string for the JWT a request will actually present.
4370
- * Prefers the subject-ish claims (sub / custom:walletAddress / address) so the
4371
- * same principal across token refreshes maps to the same fingerprint; falls back
4372
- * to a hash of the raw token if the payload can't be decoded.
4373
- */
4345
+ function authValueFromHeaders(headers) {
4346
+ return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
4347
+ }
4348
+ function principalFromAuthValue(authValue) {
4349
+ return authValue ? `h${hashForKey$2(authValue)}` : 'anon';
4350
+ }
4374
4351
  function principalFromIdToken$1(idToken) {
4375
- if (!idToken)
4376
- return 'anon';
4377
- try {
4378
- const parts = idToken.split('.');
4379
- if (parts.length < 2)
4380
- return `t${hashForKey$1(idToken)}`;
4381
- const payload = JSON.parse(decodeBase64Url(parts[1]));
4382
- const subject =
4383
- // Prefer the universal identity (@user.id) — it is the stable principal a
4384
- // request presents (equals the wallet for wallet logins; the account id for
4385
- // email/social logins, which carry no walletAddress). Keying the read cache
4386
- // on it keeps an email user's private snapshot scoped to that user.
4387
- payload['custom:userId'] ||
4388
- payload['custom:walletAddress'] ||
4389
- payload.walletAddress ||
4390
- payload.sub ||
4391
- payload.address;
4392
- if (subject)
4393
- return `s${hashForKey$1(String(subject))}`;
4394
- return `t${hashForKey$1(idToken)}`;
4395
- }
4396
- catch (_a) {
4397
- return `t${hashForKey$1(idToken)}`;
4398
- }
4352
+ return idToken ? `t${hashForKey$2(idToken)}` : 'anon';
4399
4353
  }
4400
4354
  /**
4401
4355
  * SECURITY (H1): Read caches must be keyed by the caller's principal, not just
4402
4356
  * by path/filter/shape. In a shared process / SSR worker / browser login-switch,
4403
4357
  * keying by path alone lets User B receive User A's cached private read before
4404
4358
  * any server read rule runs. This returns `appId:<principal>` for the identity a
4405
- * given read will actually authenticate as:
4406
- * - a per-request `_walletAddress` override that override's wallet
4407
- * - a per-request `_getAuthHeaders` override a hash of the auth header it
4408
- * produces (so a cross-principal cached entry is never served to it)
4409
- * - otherwise the ambient logged-in session's idToken subject (or `anon`)
4359
+ * given read will actually authenticate as. This intentionally treats JWTs as
4360
+ * opaque unverified bearer material never decoded claims and never caller
4361
+ * identity hints such as `_walletAddress`. A forged token that merely claims
4362
+ * another user's `sub` must miss that user's cache and go to the server.
4410
4363
  */
4411
4364
  async function getReadPrincipalKey(overrides) {
4412
- var _a, _b;
4413
4365
  const config = await getConfig();
4414
4366
  const appId = config.appId || '';
4415
- // Explicit per-request wallet override key by that identity.
4416
- if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
4417
- return `${appId}:w${overrides._walletAddress}`;
4367
+ // makeApiRequest applies overrides.headers AFTER its computed auth header, so
4368
+ // caller-supplied Authorization is the real request auth when present.
4369
+ const directAuth = authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers);
4370
+ if (directAuth) {
4371
+ return `${appId}:${principalFromAuthValue(directAuth)}`;
4418
4372
  }
4419
- // Per-request auth-header override (wallet client). Key by the actual header
4420
- // it produces so an entry cached under one override is never served to another.
4373
+ // Per-request auth-header override (wallet client). Key by the exact opaque
4374
+ // header it produces, not decoded claims or the unverified _walletAddress hint.
4421
4375
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
4422
4376
  try {
4423
4377
  const headers = await overrides._getAuthHeaders();
4424
- const authValue = (headers === null || headers === void 0 ? void 0 : headers.Authorization) ||
4425
- (headers === null || headers === void 0 ? void 0 : headers.authorization) ||
4426
- '';
4427
- // Decode the bearer token's subject when present for a stable key across
4428
- // refreshes; otherwise hash whatever header material we were given.
4429
- const bearer = authValue.startsWith('Bearer ')
4430
- ? authValue.slice('Bearer '.length)
4431
- : authValue;
4432
- const principal = bearer ? principalFromIdToken$1(bearer) : 'anon';
4433
- return `${appId}:o${principal}`;
4434
- }
4435
- catch (_c) {
4378
+ return `${appId}:o${principalFromAuthValue(authValueFromHeaders(headers))}`;
4379
+ }
4380
+ catch (_a) {
4436
4381
  // If we can't resolve the override identity, use a unique-ish key so we
4437
4382
  // never collide with (and serve) another principal's cached entry.
4438
- return `${appId}:o${hashForKey$1(String(Date.now()) + Math.random())}`;
4383
+ return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
4439
4384
  }
4440
4385
  }
4441
- // Direct per-request header override. makeApiRequest applies overrides.headers
4442
- // AFTER its computed auth header (api.ts), so a caller-supplied Authorization
4443
- // is the REAL request auth — key the cache by it so an entry is never served
4444
- // to a different principal under the ambient key. (Hardening: no in-repo read
4445
- // caller passes this today, but the cache must never trail the actual auth.)
4446
- const directAuth = ((_a = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _a === void 0 ? void 0 : _a.Authorization) ||
4447
- ((_b = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _b === void 0 ? void 0 : _b.authorization);
4448
- if (directAuth) {
4449
- const bearer = directAuth.startsWith('Bearer ')
4450
- ? directAuth.slice('Bearer '.length)
4451
- : directAuth;
4452
- return `${appId}:h${bearer ? principalFromIdToken$1(bearer) : hashForKey$1(directAuth)}`;
4453
- }
4454
4386
  // Ambient session principal.
4455
4387
  const idToken = await getIdToken(config.isServer);
4456
4388
  return `${appId}:${principalFromIdToken$1(idToken)}`;
@@ -4750,9 +4682,9 @@ async function get(path, opts = {}) {
4750
4682
  const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
4751
4683
  const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
4752
4684
  const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
4753
- const cursorKey = opts.cursor ? `:c${hashForKey$1(opts.cursor)}` : '';
4754
- const filterKey = opts.filter ? `:f${hashForKey$1(JSON.stringify(opts.filter))}` : '';
4755
- const sortKey = opts.sort ? `:s${hashForKey$1(JSON.stringify(opts.sort))}` : '';
4685
+ const cursorKey = opts.cursor ? `:c${hashForKey$2(opts.cursor)}` : '';
4686
+ const filterKey = opts.filter ? `:f${hashForKey$2(JSON.stringify(opts.filter))}` : '';
4687
+ const sortKey = opts.sort ? `:s${hashForKey$2(JSON.stringify(opts.sort))}` : '';
4756
4688
  const principalKey = await getReadPrincipalKey(opts._overrides);
4757
4689
  const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
4758
4690
  const now = Date.now();
@@ -5502,7 +5434,7 @@ let reconnectInProgress = null;
5502
5434
  function generateSubscriptionId() {
5503
5435
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
5504
5436
  }
5505
- function hashForKey(value) {
5437
+ function hashForKey$1(value) {
5506
5438
  let h = 5381;
5507
5439
  for (let i = 0; i < value.length; i++) {
5508
5440
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
@@ -5521,49 +5453,23 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5521
5453
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
5522
5454
  const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
5523
5455
  const limitKey = limit !== undefined ? `:l${limit}` : '';
5524
- const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
5525
- const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey(JSON.stringify(filter))}` : '';
5456
+ const cursorKey = cursor ? `:c${hashForKey$1(cursor)}` : '';
5457
+ const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey$1(JSON.stringify(filter))}` : '';
5526
5458
  const identityKey = identity || 'anon';
5527
5459
  return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
5528
5460
  }
5529
5461
  /**
5530
- * Derive a stable identity string for the principal a subscription authenticates
5531
- * as: `<appId>:<principalFingerprint>`. Mirrors the operations.ts logic so HTTP
5532
- * and WS caches scope reads to the same identity.
5462
+ * Derive an opaque identity string for the bearer material a subscription sends.
5463
+ * JWT payloads are deliberately not trusted here: cache isolation must happen
5464
+ * before the server has verified any claims.
5533
5465
  */
5534
5466
  function principalFromIdToken(idToken) {
5535
- if (!idToken)
5536
- return 'anon';
5537
- try {
5538
- const parts = idToken.split('.');
5539
- if (parts.length < 2)
5540
- return `t${hashForKey(idToken)}`;
5541
- const payload = JSON.parse(decodeBase64Url(parts[1]));
5542
- const subject =
5543
- // Universal identity (@user.id) first — the stable principal a request
5544
- // presents (the account id for email/social logins, which carry no
5545
- // walletAddress). Keeps the H1 response-cache scoping correct for them.
5546
- payload['custom:userId'] ||
5547
- payload['custom:walletAddress'] ||
5548
- payload.walletAddress ||
5549
- payload.sub ||
5550
- payload.address;
5551
- if (subject)
5552
- return `s${hashForKey(String(subject))}`;
5553
- return `t${hashForKey(idToken)}`;
5554
- }
5555
- catch (_a) {
5556
- return `t${hashForKey(idToken)}`;
5557
- }
5467
+ return idToken ? `t${hashForKey$1(idToken)}` : 'anon';
5558
5468
  }
5559
5469
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5560
- // Per-subscription wallet override (server WalletClient.subscribe): key by the
5561
- // wallet's own token, mirroring getReadPrincipalKey, so a wallet client never
5562
- // consults and never crashes on — the absent ambient env keypair, and its
5563
- // cached snapshots are never crossed with another principal's.
5564
- if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
5565
- return `${effectiveAppId}:w${overrides._walletAddress}`;
5566
- }
5470
+ // Per-subscription wallet override (server WalletClient.subscribe): key by
5471
+ // the wallet's own opaque token material, mirroring getReadPrincipalKey. Do
5472
+ // not trust decoded claims or the unverified _walletAddress hint.
5567
5473
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
5568
5474
  try {
5569
5475
  const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
@@ -5934,7 +5840,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5934
5840
  return connection;
5935
5841
  }
5936
5842
  function handleServerMessage(connection, message) {
5937
- var _a, _b;
5843
+ var _a;
5938
5844
  switch (message.type) {
5939
5845
  case 'authenticated': {
5940
5846
  connection.isAuthenticating = false;
@@ -6028,28 +5934,27 @@ function handleServerMessage(connection, message) {
6028
5934
  err.subscriptionId = message.subscriptionId;
6029
5935
  return err;
6030
5936
  };
5937
+ const wsError = buildWsError();
6031
5938
  // Handle CRUD request errors (requestId present)
6032
5939
  if (message.requestId) {
6033
5940
  const pendingReq = connection.pendingRequests.get(message.requestId);
6034
5941
  if (pendingReq) {
6035
5942
  connection.pendingRequests.delete(message.requestId);
6036
5943
  clearTimeout(pendingReq.timer);
6037
- pendingReq.reject(buildWsError());
5944
+ pendingReq.reject(wsError);
6038
5945
  }
6039
5946
  }
6040
5947
  if (message.subscriptionId) {
6041
5948
  // Reject pending subscription if this is a subscription error
6042
5949
  const pending = connection.pendingSubscriptions.get(message.subscriptionId);
6043
5950
  if (pending) {
6044
- pending.reject(buildWsError());
5951
+ pending.reject(wsError);
6045
5952
  connection.pendingSubscriptions.delete(message.subscriptionId);
6046
5953
  }
6047
5954
  // Notify error callbacks for this subscription
6048
5955
  const subscription = connection.subscriptions.get(message.subscriptionId);
6049
5956
  if (subscription) {
6050
- for (const callback of subscription.callbacks) {
6051
- (_b = callback.onError) === null || _b === void 0 ? void 0 : _b.call(callback, buildWsError());
6052
- }
5957
+ notifyErrorCallbacks(subscription, wsError);
6053
5958
  }
6054
5959
  }
6055
5960
  break;
@@ -6102,6 +6007,25 @@ function notifyCallbacks(subscription, data) {
6102
6007
  }
6103
6008
  }
6104
6009
  }
6010
+ function notifyErrorCallbacks(subscription, error) {
6011
+ let delivered = false;
6012
+ const callbacks = subscription.callbacks.slice();
6013
+ for (const callback of callbacks) {
6014
+ if (!callback.onError)
6015
+ continue;
6016
+ delivered = true;
6017
+ try {
6018
+ callback.onError(error);
6019
+ }
6020
+ catch (callbackError) {
6021
+ console.error('[WS v2] Error in subscription error callback:', callbackError);
6022
+ }
6023
+ }
6024
+ if (delivered) {
6025
+ error.__boundedDeliveredToOnError = true;
6026
+ }
6027
+ return delivered;
6028
+ }
6105
6029
  // WebSocket readyState constants
6106
6030
  const WS_READY_STATE_OPEN = 1;
6107
6031
  const WS_READY_STATE_CLOSED = 3;
@@ -6176,10 +6100,8 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6176
6100
  const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
6177
6101
  ? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
6178
6102
  : undefined;
6179
- const principalKey = (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress)
6180
- ? `w${overrides._walletAddress}`
6181
- : (authTokenProvider ? `o${principalFromIdToken(await authTokenProvider())}` : undefined);
6182
6103
  const identity = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6104
+ const principalKey = authTokenProvider ? identity : undefined;
6183
6105
  const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity);
6184
6106
  // Deliver cached data immediately if available
6185
6107
  const cachedEntry = responseCache.get(cacheKey);
@@ -6258,7 +6180,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6258
6180
  await subscriptionPromise;
6259
6181
  }
6260
6182
  catch (error) {
6261
- console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
6183
+ const err = error instanceof Error ? error : new Error(String(error));
6184
+ if (!err.__boundedDeliveredToOnError) {
6185
+ notifyErrorCallbacks(subscription, err);
6186
+ }
6187
+ if (!subscriptionOptions.onError) {
6188
+ connection.pendingSubscriptions.delete(subscriptionId);
6189
+ connection.subscriptions.delete(subscriptionId);
6190
+ throw err;
6191
+ }
6192
+ if (!err.__boundedDeliveredToOnError) {
6193
+ console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
6194
+ }
6262
6195
  }
6263
6196
  }
6264
6197
  // Return unsubscribe function
@@ -6421,6 +6354,13 @@ async function doReconnectWithNewAuth() {
6421
6354
  catch (error) {
6422
6355
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6423
6356
  }
6357
+ try {
6358
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return realtimeStore; });
6359
+ await reconnectRealtimeStoreWithNewAuth();
6360
+ }
6361
+ catch (error) {
6362
+ console.warn('[WS v2] Failed to reset legacy realtime store on auth change:', error);
6363
+ }
6424
6364
  for (const [appId, connection] of connections) {
6425
6365
  if (!connection.ws) {
6426
6366
  continue;
@@ -6888,6 +6828,16 @@ async function idbSet(key, value) {
6888
6828
  // RealtimeStore
6889
6829
  // ---------------------------------------------------------------------------
6890
6830
  let nextRequestId = 1;
6831
+ function hashForKey(value) {
6832
+ let h = 5381;
6833
+ for (let i = 0; i < value.length; i++) {
6834
+ h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
6835
+ }
6836
+ return h.toString(36);
6837
+ }
6838
+ function principalFromToken(token) {
6839
+ return token ? `t${hashForKey(token)}` : 'anon';
6840
+ }
6891
6841
  class RealtimeStore {
6892
6842
  constructor() {
6893
6843
  this.ws = null;
@@ -6903,7 +6853,9 @@ class RealtimeStore {
6903
6853
  this.idbDirtyKeys = new Set();
6904
6854
  this.closed = false;
6905
6855
  this.authToken = null;
6856
+ this.authPrincipalKey = 'anon';
6906
6857
  this.authenticating = false;
6858
+ this.suppressNextReconnect = false;
6907
6859
  this.isServer = false;
6908
6860
  this.tokenRefreshTimer = null;
6909
6861
  // -----------------------------------------------------------------------
@@ -6923,34 +6875,98 @@ class RealtimeStore {
6923
6875
  this.startTokenRefresh();
6924
6876
  }
6925
6877
  async refreshToken() {
6878
+ let token = null;
6926
6879
  try {
6927
6880
  const { getIdToken } = await Promise.resolve().then(function () { return utils; });
6928
- const token = await getIdToken(this.isServer);
6929
- if (token)
6930
- this.authToken = token;
6881
+ token = await getIdToken(this.isServer);
6931
6882
  }
6932
6883
  catch ( /* no auth available */_a) { /* no auth available */ }
6884
+ this.authToken = token !== null && token !== void 0 ? token : null;
6885
+ this.authPrincipalKey = principalFromToken(this.authToken);
6933
6886
  }
6934
6887
  startTokenRefresh() {
6935
6888
  if (this.tokenRefreshTimer)
6936
6889
  return;
6937
6890
  this.tokenRefreshTimer = setInterval(async () => {
6938
- var _a;
6939
- const prevToken = this.authToken;
6891
+ const prevPrincipal = this.authPrincipalKey;
6940
6892
  await this.refreshToken();
6941
- if (this.authToken && this.authToken !== prevToken && ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
6942
- // Token changed — reconnect with fresh token
6943
- this.ws.close(1000, 'Token refreshed');
6893
+ if (this.authPrincipalKey !== prevPrincipal) {
6894
+ await this.applyAuthPrincipalChange();
6895
+ if (this.subscriptions.size > 0) {
6896
+ await this.ensureConnected().catch(() => {
6897
+ this.setAllSubscriptionStatus('error');
6898
+ });
6899
+ }
6944
6900
  }
6945
6901
  }, 5 * 60 * 1000); // Check every 5 minutes
6946
6902
  }
6903
+ async ensureInitialized() {
6904
+ if (this.appId)
6905
+ return;
6906
+ if (!this.initPromise)
6907
+ this.initPromise = this.init();
6908
+ await this.initPromise;
6909
+ }
6910
+ async ensureCurrentAuth() {
6911
+ await this.ensureInitialized();
6912
+ const prevPrincipal = this.authPrincipalKey;
6913
+ await this.refreshToken();
6914
+ if (this.authPrincipalKey !== prevPrincipal) {
6915
+ await this.applyAuthPrincipalChange();
6916
+ }
6917
+ }
6918
+ rekeySubscriptionsForPrincipal() {
6919
+ const subs = Array.from(this.subscriptions.values());
6920
+ this.subscriptions.clear();
6921
+ for (const sub of subs) {
6922
+ this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);
6923
+ }
6924
+ }
6925
+ async applyAuthPrincipalChange() {
6926
+ if (this.idbFlushTimer) {
6927
+ clearTimeout(this.idbFlushTimer);
6928
+ this.idbFlushTimer = null;
6929
+ }
6930
+ this.idbDirtyKeys.clear();
6931
+ this.rekeySubscriptionsForPrincipal();
6932
+ for (const sub of this.subscriptions.values()) {
6933
+ sub.docs.clear();
6934
+ sub.ref.current = sub.docs;
6935
+ sub.error = null;
6936
+ sub.isStale = false;
6937
+ let loaded = false;
6938
+ if (sub.tier !== 'ephemeral') {
6939
+ const cached = await idbGet(this.idbKey(sub.path));
6940
+ if (cached && Array.isArray(cached)) {
6941
+ for (const doc of cached) {
6942
+ if (doc && doc._id)
6943
+ sub.docs.set(doc._id, doc);
6944
+ }
6945
+ sub.ref.current = sub.docs;
6946
+ loaded = sub.docs.size > 0;
6947
+ }
6948
+ }
6949
+ sub.status = loaded ? 'cached' : 'loading';
6950
+ sub.isStale = loaded;
6951
+ if (loaded)
6952
+ this.notifySubscription(sub);
6953
+ else
6954
+ this.notifyState(sub);
6955
+ }
6956
+ if (this.ws) {
6957
+ const ws = this.ws;
6958
+ this.ws = null;
6959
+ this.connectPromise = null;
6960
+ this.suppressNextReconnect = true;
6961
+ try {
6962
+ ws.close(1000, 'Auth changed');
6963
+ }
6964
+ catch ( /* ignore */_a) { /* ignore */ }
6965
+ }
6966
+ }
6947
6967
  async ensureConnected() {
6948
6968
  var _a;
6949
- if (!this.appId) {
6950
- if (!this.initPromise)
6951
- this.initPromise = this.init();
6952
- await this.initPromise;
6953
- }
6969
+ await this.ensureCurrentAuth();
6954
6970
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
6955
6971
  return;
6956
6972
  if (this.connectPromise)
@@ -7034,11 +7050,20 @@ class RealtimeStore {
7034
7050
  ws.addEventListener('close', () => {
7035
7051
  if (authTimer)
7036
7052
  clearTimeout(authTimer);
7053
+ if (this.ws !== ws) {
7054
+ if (this.suppressNextReconnect)
7055
+ this.suppressNextReconnect = false;
7056
+ return;
7057
+ }
7037
7058
  this.authenticating = false;
7038
7059
  this.ws = null;
7039
7060
  this.connectPromise = null;
7040
7061
  this.rejectAllPending('WebSocket closed');
7041
7062
  this.setAllSubscriptionStatus('reconnecting');
7063
+ if (this.suppressNextReconnect) {
7064
+ this.suppressNextReconnect = false;
7065
+ return;
7066
+ }
7042
7067
  this.scheduleReconnect();
7043
7068
  });
7044
7069
  });
@@ -7214,14 +7239,34 @@ class RealtimeStore {
7214
7239
  }
7215
7240
  }
7216
7241
  handleError(msg) {
7217
- var _a;
7242
+ var _a, _b, _c;
7243
+ const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));
7244
+ if (msg.code)
7245
+ error.code = msg.code;
7246
+ if (msg.subscriptionId || msg.id)
7247
+ error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;
7218
7248
  const requestId = msg.requestId;
7219
7249
  if (requestId) {
7220
7250
  const pending = this.pendingRequests.get(requestId);
7221
7251
  if (pending) {
7222
7252
  this.pendingRequests.delete(requestId);
7223
7253
  clearTimeout(pending.timeout);
7224
- pending.reject(new Error((_a = msg.message) !== null && _a !== void 0 ? _a : 'Server error'));
7254
+ pending.reject(error);
7255
+ }
7256
+ }
7257
+ const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;
7258
+ if (subId) {
7259
+ const sub = this.findSubscriptionById(subId);
7260
+ if (sub) {
7261
+ sub.status = 'error';
7262
+ sub.error = error;
7263
+ this.notifyState(sub);
7264
+ for (const callback of Array.from(sub.errorCallbacks)) {
7265
+ try {
7266
+ callback(error);
7267
+ }
7268
+ catch ( /* swallow */_d) { /* swallow */ }
7269
+ }
7225
7270
  }
7226
7271
  }
7227
7272
  }
@@ -7230,6 +7275,7 @@ class RealtimeStore {
7230
7275
  // -----------------------------------------------------------------------
7231
7276
  async subscribe(path, opts = {}) {
7232
7277
  var _a;
7278
+ await this.ensureCurrentAuth();
7233
7279
  const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
7234
7280
  const subKey = this.getSubKey(path, opts);
7235
7281
  let sub = this.subscriptions.get(subKey);
@@ -7239,6 +7285,8 @@ class RealtimeStore {
7239
7285
  sub.callbacks.add(opts.onData);
7240
7286
  if (opts.onState)
7241
7287
  sub.stateCallbacks.add(opts.onState);
7288
+ if (opts.onError)
7289
+ sub.errorCallbacks.add(opts.onError);
7242
7290
  // Immediately deliver current state
7243
7291
  if (opts.onData && sub.docs.size > 0) {
7244
7292
  opts.onData(this.docsToArray(sub));
@@ -7246,7 +7294,7 @@ class RealtimeStore {
7246
7294
  if (opts.onState) {
7247
7295
  opts.onState(this.getState(sub));
7248
7296
  }
7249
- return this.createUnsubscribe(subKey, opts.onData, opts.onState);
7297
+ return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7250
7298
  }
7251
7299
  // New subscription
7252
7300
  const subId = `sub_${nextRequestId++}`;
@@ -7261,6 +7309,7 @@ class RealtimeStore {
7261
7309
  error: null,
7262
7310
  callbacks: new Set(opts.onData ? [opts.onData] : []),
7263
7311
  stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
7312
+ errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
7264
7313
  ref: { current: new Map() },
7265
7314
  };
7266
7315
  this.subscriptions.set(subKey, sub);
@@ -7290,7 +7339,7 @@ class RealtimeStore {
7290
7339
  sub.error = new Error('Connection failed');
7291
7340
  this.notifyState(sub);
7292
7341
  }
7293
- return this.createUnsubscribe(subKey, opts.onData, opts.onState);
7342
+ return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7294
7343
  }
7295
7344
  getRef(path, opts = {}) {
7296
7345
  var _a;
@@ -7364,6 +7413,7 @@ class RealtimeStore {
7364
7413
  }
7365
7414
  }
7366
7415
  async get(path) {
7416
+ await this.ensureCurrentAuth();
7367
7417
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7368
7418
  // Check local subscriptions first
7369
7419
  const collectionPath = this.getCollectionPath(normalizedPath);
@@ -7544,7 +7594,7 @@ class RealtimeStore {
7544
7594
  return docPath;
7545
7595
  }
7546
7596
  getSubKey(path, opts) {
7547
- const parts = [path];
7597
+ const parts = [this.appId, this.authPrincipalKey, path];
7548
7598
  if (opts.filter)
7549
7599
  parts.push(JSON.stringify(opts.filter));
7550
7600
  if (opts.prompt)
@@ -7554,7 +7604,7 @@ class RealtimeStore {
7554
7604
  return parts.join('::');
7555
7605
  }
7556
7606
  idbKey(path) {
7557
- return `${this.appId}:${path}`;
7607
+ return `${this.appId}:${this.authPrincipalKey}:${path}`;
7558
7608
  }
7559
7609
  markIdbDirty(path) {
7560
7610
  const sub = this.findSubscriptionByPath(path);
@@ -7579,18 +7629,23 @@ class RealtimeStore {
7579
7629
  }
7580
7630
  }
7581
7631
  }
7582
- createUnsubscribe(subKey, onData, onState) {
7632
+ createUnsubscribe(subKey, subId, onData, onState, onError) {
7583
7633
  return async () => {
7584
- const sub = this.subscriptions.get(subKey);
7634
+ var _a;
7635
+ const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);
7585
7636
  if (!sub)
7586
7637
  return;
7638
+ const currentSubKey = this.getSubKey(sub.path, sub.options);
7587
7639
  if (onData)
7588
7640
  sub.callbacks.delete(onData);
7589
7641
  if (onState)
7590
7642
  sub.stateCallbacks.delete(onState);
7643
+ if (onError)
7644
+ sub.errorCallbacks.delete(onError);
7591
7645
  // If no more callbacks, unsubscribe entirely
7592
- if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0) {
7646
+ if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
7593
7647
  this.subscriptions.delete(subKey);
7648
+ this.subscriptions.delete(currentSubKey);
7594
7649
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
7595
7650
  this.ws.send(JSON.stringify({
7596
7651
  type: 'unsubscribe',
@@ -7662,6 +7717,22 @@ class RealtimeStore {
7662
7717
  this.rejectAllPending('Store closed');
7663
7718
  this.subscriptions.clear();
7664
7719
  }
7720
+ async reconnectWithNewAuth() {
7721
+ if (this.closed)
7722
+ return;
7723
+ await this.ensureInitialized();
7724
+ await this.refreshToken();
7725
+ await this.applyAuthPrincipalChange();
7726
+ if (this.subscriptions.size > 0) {
7727
+ await this.ensureConnected().catch((error) => {
7728
+ this.setAllSubscriptionStatus('error');
7729
+ for (const sub of this.subscriptions.values()) {
7730
+ sub.error = error instanceof Error ? error : new Error(String(error));
7731
+ this.notifyState(sub);
7732
+ }
7733
+ });
7734
+ }
7735
+ }
7665
7736
  }
7666
7737
  // ---------------------------------------------------------------------------
7667
7738
  // Singleton instance
@@ -7679,6 +7750,19 @@ function resetRealtimeStore() {
7679
7750
  storeInstance = null;
7680
7751
  }
7681
7752
  }
7753
+ async function reconnectRealtimeStoreWithNewAuth() {
7754
+ if (storeInstance) {
7755
+ await storeInstance.reconnectWithNewAuth();
7756
+ }
7757
+ }
7758
+
7759
+ var realtimeStore = /*#__PURE__*/Object.freeze({
7760
+ __proto__: null,
7761
+ RealtimeStore: RealtimeStore,
7762
+ getRealtimeStore: getRealtimeStore,
7763
+ reconnectRealtimeStoreWithNewAuth: reconnectRealtimeStoreWithNewAuth,
7764
+ resetRealtimeStore: resetRealtimeStore
7765
+ });
7682
7766
 
7683
7767
  // ---------------------------------------------------------------------------
7684
7768
  // functions.ts -- Bounded Functions client (the imperative escape hatch).
@@ -7834,6 +7918,23 @@ function realtimeHttpBase(wsApiUrl) {
7834
7918
  // Strip trailing slash from the resulting origin+path.
7835
7919
  return url.toString().replace(/\/$/, '');
7836
7920
  }
7921
+ function withoutAuthorization(headers) {
7922
+ if (!headers)
7923
+ return undefined;
7924
+ const clean = {};
7925
+ for (const [key, value] of Object.entries(headers)) {
7926
+ if (key.toLowerCase() === 'authorization')
7927
+ continue;
7928
+ clean[key] = value;
7929
+ }
7930
+ return Object.keys(clean).length > 0 ? clean : undefined;
7931
+ }
7932
+ async function liveAuthHeader(configIsServer, overrides) {
7933
+ if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
7934
+ return overrides._getAuthHeaders();
7935
+ }
7936
+ return createAuthHeader(configIsServer);
7937
+ }
7837
7938
  /**
7838
7939
  * Send a player intent to a running live room. Returns `{ ok: true }`.
7839
7940
  *
@@ -7848,7 +7949,7 @@ function realtimeHttpBase(wsApiUrl) {
7848
7949
  * transport error / timeout.
7849
7950
  */
7850
7951
  async function intent(roomPath, intent, opts = {}) {
7851
- var _a, _b, _c, _d, _e;
7952
+ var _a, _b, _c, _d, _e, _f, _g;
7852
7953
  if (!roomPath || typeof roomPath !== 'string') {
7853
7954
  throw new LiveIntentError('A room path is required');
7854
7955
  }
@@ -7869,37 +7970,48 @@ async function intent(roomPath, intent, opts = {}) {
7869
7970
  // (e.g. the first join before subscribeView's WS connects) — HTTP also throws
7870
7971
  // on a non-2xx, so errors are surfaced there too.
7871
7972
  const normalizedRoomPath = roomPath.replace(/\/$/, '');
7872
- if (opts.fireAndForget) {
7873
- if (wsIntent(config.appId, normalizedRoomPath, intent))
7874
- return { ok: true };
7875
- }
7876
- else {
7877
- const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
7878
- if (ack)
7879
- return await ack;
7973
+ const hasAuthOverride = !!((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders);
7974
+ if (!hasAuthOverride) {
7975
+ if (opts.fireAndForget) {
7976
+ if (wsIntent(config.appId, normalizedRoomPath, intent))
7977
+ return { ok: true };
7978
+ }
7979
+ else {
7980
+ const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
7981
+ if (ack)
7982
+ return await ack;
7983
+ }
7880
7984
  }
7881
7985
  const base = realtimeHttpBase(config.wsApiUrl);
7882
- // Attach the caller's session token automatically (same token as data calls).
7883
- const authHeader = await createAuthHeader(config.isServer);
7884
- const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, (authHeader !== null && authHeader !== void 0 ? authHeader : {})), ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
7986
+ const extraHeaders = withoutAuthorization(opts.headers);
7987
+ const overrideHeaders = withoutAuthorization((_b = opts._overrides) === null || _b === void 0 ? void 0 : _b.headers);
7988
+ const buildHeaders = async () => {
7989
+ const authHeader = await liveAuthHeader(config.isServer, opts._overrides);
7990
+ return Object.assign(Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, (overrideHeaders !== null && overrideHeaders !== void 0 ? overrideHeaders : {})), (extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
7991
+ };
7885
7992
  const controller = new AbortController();
7886
- const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 60000;
7993
+ const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
7887
7994
  const timer = setTimeout(() => controller.abort(), timeoutMs);
7888
7995
  let res;
7889
7996
  try {
7890
- res = await fetch(`${base}/live/intent`, {
7997
+ const send = async () => fetch(`${base}/live/intent`, {
7891
7998
  method: 'POST',
7892
- headers,
7999
+ headers: await buildHeaders(),
7893
8000
  body: JSON.stringify({ path: normalizedRoomPath, intent }),
7894
8001
  signal: controller.signal,
7895
8002
  });
8003
+ res = await send();
8004
+ if (res.status === 401 && ((_d = opts._overrides) === null || _d === void 0 ? void 0 : _d._clearAuth)) {
8005
+ await opts._overrides._clearAuth();
8006
+ res = await send();
8007
+ }
7896
8008
  }
7897
8009
  catch (err) {
7898
8010
  clearTimeout(timer);
7899
8011
  if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError') {
7900
8012
  throw new LiveIntentError(`Live intent to "${roomPath}" timed out after ${timeoutMs}ms`);
7901
8013
  }
7902
- throw new LiveIntentError(`Failed to reach the realtime worker: ${(_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : String(err)}`);
8014
+ throw new LiveIntentError(`Failed to reach the realtime worker: ${(_e = err === null || err === void 0 ? void 0 : err.message) !== null && _e !== void 0 ? _e : String(err)}`);
7903
8015
  }
7904
8016
  clearTimeout(timer);
7905
8017
  let body = null;
@@ -7908,12 +8020,12 @@ async function intent(roomPath, intent, opts = {}) {
7908
8020
  try {
7909
8021
  body = JSON.parse(text);
7910
8022
  }
7911
- catch (_f) {
8023
+ catch (_h) {
7912
8024
  body = { raw: text };
7913
8025
  }
7914
8026
  }
7915
8027
  if (!res.ok) {
7916
- const message = (_e = (_d = body === null || body === void 0 ? void 0 : body.error) !== null && _d !== void 0 ? _d : body === null || body === void 0 ? void 0 : body.message) !== null && _e !== void 0 ? _e : `Live intent failed with HTTP ${res.status}`;
8028
+ const message = (_g = (_f = body === null || body === void 0 ? void 0 : body.error) !== null && _f !== void 0 ? _f : body === null || body === void 0 ? void 0 : body.message) !== null && _g !== void 0 ? _g : `Live intent failed with HTTP ${res.status}`;
7917
8029
  throw new LiveIntentError(message, res.status, body);
7918
8030
  }
7919
8031
  return (body !== null && body !== void 0 ? body : { ok: true });