@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.mjs CHANGED
@@ -3780,24 +3780,19 @@ async function buildSetDocumentsTransaction(connection, idl, anchorProvider, pay
3780
3780
  /* ------------------------------------------------------------------ */
3781
3781
  /* ENV helpers */
3782
3782
  /* ------------------------------------------------------------------ */
3783
- // Canonical `BOUNDED_PRIVATE_KEY` (matches the CLI); legacy
3784
- // `BOUNDED_SOLANA_KEYPAIR` still honored. Only consulted when no explicit
3785
- // keypair was provided (createWalletClient passes one).
3786
- const ENV_KEYPAIRS = ["BOUNDED_PRIVATE_KEY", "BOUNDED_SOLANA_KEYPAIR"];
3783
+ // Canonical `BOUNDED_PRIVATE_KEY` (matches the CLI). Only consulted when no
3784
+ // explicit keypair was provided (createWalletClient passes one).
3785
+ const ENV_KEYPAIR = "BOUNDED_PRIVATE_KEY";
3786
+ const LEGACY_ENV_KEYPAIR = "BOUNDED_SOLANA_KEYPAIR";
3787
3787
  function loadKeypairFromEnv() {
3788
- let secret;
3789
- let found;
3790
- for (const name of ENV_KEYPAIRS) {
3791
- const v = process.env[name];
3792
- if (v) {
3793
- secret = v;
3794
- found = name;
3795
- break;
3796
- }
3788
+ if (process.env[LEGACY_ENV_KEYPAIR]) {
3789
+ throw new Error(`${LEGACY_ENV_KEYPAIR} is no longer supported. Set ${ENV_KEYPAIR} instead, ` +
3790
+ `or pass an explicit keypair via createWalletClient({ keypair }).`);
3797
3791
  }
3792
+ const secret = process.env[ENV_KEYPAIR];
3798
3793
  if (!secret) {
3799
3794
  throw new Error(`No server keypair for this top-level call. The top-level get/set/subscribe/etc. use an ` +
3800
- `AMBIENT session — set ${ENV_KEYPAIRS[0]} to a base-58 secret key (or JSON array) to provide one. ` +
3795
+ `AMBIENT session — set ${ENV_KEYPAIR} to a base-58 secret key (or JSON array) to provide one. ` +
3801
3796
  `If you already created a wallet with createWalletClient({ keypair }), call ITS methods instead ` +
3802
3797
  `(client.subscribe / client.set / client.get): that client is self-contained and deliberately does ` +
3803
3798
  `not set the ambient session, so the top-level functions can't see it.`);
@@ -3809,7 +3804,7 @@ function loadKeypairFromEnv() {
3809
3804
  return Keypair.fromSecretKey(secretKey);
3810
3805
  }
3811
3806
  catch (err) {
3812
- throw new Error(`Unable to parse ${found}. Ensure it is valid base-58 or JSON.`);
3807
+ throw new Error(`Unable to parse ${ENV_KEYPAIR}. Ensure it is valid base-58 or JSON.`);
3813
3808
  }
3814
3809
  }
3815
3810
  /* ------------------------------------------------------------------ */
@@ -3876,7 +3871,7 @@ class ServerSessionManager {
3876
3871
  this.session = session;
3877
3872
  }
3878
3873
  /* ---------------------------------------------- *
3879
- * CLEAR (e.g. after logout or 401 retry)
3874
+ * CLEAR (e.g. after explicit logout)
3880
3875
  * ---------------------------------------------- */
3881
3876
  clearSession() {
3882
3877
  this.session = null;
@@ -3900,11 +3895,6 @@ class ServerSessionManager {
3900
3895
  /* The default singleton instance (reads keypair from env) */
3901
3896
  ServerSessionManager.instance = new ServerSessionManager();
3902
3897
 
3903
- var serverSessionManager = /*#__PURE__*/Object.freeze({
3904
- __proto__: null,
3905
- ServerSessionManager: ServerSessionManager
3906
- });
3907
-
3908
3898
  /**
3909
3899
  * Safe base64 helpers for bounded-core.
3910
3900
  *
@@ -4129,13 +4119,6 @@ async function refreshAuthSessionOnce(appId, isServer) {
4129
4119
  async function makeApiRequest(method, urlPath, data, _overrides) {
4130
4120
  var _a, _b, _c, _d, _e, _f, _g, _h;
4131
4121
  const config = await getConfig();
4132
- let hasRetriedAfterServerSessionReset = false;
4133
- const clearServerSession = async () => {
4134
- if (!config.isServer)
4135
- return;
4136
- const { ServerSessionManager } = await Promise.resolve().then(function () { return serverSessionManager; });
4137
- ServerSessionManager.instance.clearSession();
4138
- };
4139
4122
  async function executeRequest() {
4140
4123
  var _a;
4141
4124
  // When _getAuthHeaders is provided (wallet client), use it as the sole auth source.
@@ -4185,13 +4168,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4185
4168
  }
4186
4169
  return await executeRequest();
4187
4170
  }
4188
- catch (_refreshError) {
4189
- // Server-side fallback: clear global session and retry once
4190
- if (config.isServer && !hasRetriedAfterServerSessionReset) {
4191
- hasRetriedAfterServerSessionReset = true;
4192
- await clearServerSession();
4193
- return await executeRequest();
4194
- }
4171
+ catch (_j) {
4195
4172
  throw error;
4196
4173
  }
4197
4174
  }
@@ -4338,99 +4315,54 @@ function normalizeReadResult(responseData, pathIsDocument) {
4338
4315
  }
4339
4316
  return responseData;
4340
4317
  }
4341
- function hashForKey$1(value) {
4318
+ function hashForKey$2(value) {
4342
4319
  let h = 5381;
4343
4320
  for (let i = 0; i < value.length; i++) {
4344
4321
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
4345
4322
  }
4346
4323
  return h.toString(36);
4347
4324
  }
4348
- /**
4349
- * Derive a stable identity string for the JWT a request will actually present.
4350
- * Prefers the subject-ish claims (sub / custom:walletAddress / address) so the
4351
- * same principal across token refreshes maps to the same fingerprint; falls back
4352
- * to a hash of the raw token if the payload can't be decoded.
4353
- */
4325
+ function authValueFromHeaders(headers) {
4326
+ return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
4327
+ }
4328
+ function principalFromAuthValue(authValue) {
4329
+ return authValue ? `h${hashForKey$2(authValue)}` : 'anon';
4330
+ }
4354
4331
  function principalFromIdToken$1(idToken) {
4355
- if (!idToken)
4356
- return 'anon';
4357
- try {
4358
- const parts = idToken.split('.');
4359
- if (parts.length < 2)
4360
- return `t${hashForKey$1(idToken)}`;
4361
- const payload = JSON.parse(decodeBase64Url(parts[1]));
4362
- const subject =
4363
- // Prefer the universal identity (@user.id) — it is the stable principal a
4364
- // request presents (equals the wallet for wallet logins; the account id for
4365
- // email/social logins, which carry no walletAddress). Keying the read cache
4366
- // on it keeps an email user's private snapshot scoped to that user.
4367
- payload['custom:userId'] ||
4368
- payload['custom:walletAddress'] ||
4369
- payload.walletAddress ||
4370
- payload.sub ||
4371
- payload.address;
4372
- if (subject)
4373
- return `s${hashForKey$1(String(subject))}`;
4374
- return `t${hashForKey$1(idToken)}`;
4375
- }
4376
- catch (_a) {
4377
- return `t${hashForKey$1(idToken)}`;
4378
- }
4332
+ return idToken ? `t${hashForKey$2(idToken)}` : 'anon';
4379
4333
  }
4380
4334
  /**
4381
4335
  * SECURITY (H1): Read caches must be keyed by the caller's principal, not just
4382
4336
  * by path/filter/shape. In a shared process / SSR worker / browser login-switch,
4383
4337
  * keying by path alone lets User B receive User A's cached private read before
4384
4338
  * any server read rule runs. This returns `appId:<principal>` for the identity a
4385
- * given read will actually authenticate as:
4386
- * - a per-request `_walletAddress` override that override's wallet
4387
- * - a per-request `_getAuthHeaders` override a hash of the auth header it
4388
- * produces (so a cross-principal cached entry is never served to it)
4389
- * - otherwise the ambient logged-in session's idToken subject (or `anon`)
4339
+ * given read will actually authenticate as. This intentionally treats JWTs as
4340
+ * opaque unverified bearer material never decoded claims and never caller
4341
+ * identity hints such as `_walletAddress`. A forged token that merely claims
4342
+ * another user's `sub` must miss that user's cache and go to the server.
4390
4343
  */
4391
4344
  async function getReadPrincipalKey(overrides) {
4392
- var _a, _b;
4393
4345
  const config = await getConfig();
4394
4346
  const appId = config.appId || '';
4395
- // Explicit per-request wallet override key by that identity.
4396
- if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
4397
- return `${appId}:w${overrides._walletAddress}`;
4347
+ // makeApiRequest applies overrides.headers AFTER its computed auth header, so
4348
+ // caller-supplied Authorization is the real request auth when present.
4349
+ const directAuth = authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers);
4350
+ if (directAuth) {
4351
+ return `${appId}:${principalFromAuthValue(directAuth)}`;
4398
4352
  }
4399
- // Per-request auth-header override (wallet client). Key by the actual header
4400
- // it produces so an entry cached under one override is never served to another.
4353
+ // Per-request auth-header override (wallet client). Key by the exact opaque
4354
+ // header it produces, not decoded claims or the unverified _walletAddress hint.
4401
4355
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
4402
4356
  try {
4403
4357
  const headers = await overrides._getAuthHeaders();
4404
- const authValue = (headers === null || headers === void 0 ? void 0 : headers.Authorization) ||
4405
- (headers === null || headers === void 0 ? void 0 : headers.authorization) ||
4406
- '';
4407
- // Decode the bearer token's subject when present for a stable key across
4408
- // refreshes; otherwise hash whatever header material we were given.
4409
- const bearer = authValue.startsWith('Bearer ')
4410
- ? authValue.slice('Bearer '.length)
4411
- : authValue;
4412
- const principal = bearer ? principalFromIdToken$1(bearer) : 'anon';
4413
- return `${appId}:o${principal}`;
4414
- }
4415
- catch (_c) {
4358
+ return `${appId}:o${principalFromAuthValue(authValueFromHeaders(headers))}`;
4359
+ }
4360
+ catch (_a) {
4416
4361
  // If we can't resolve the override identity, use a unique-ish key so we
4417
4362
  // never collide with (and serve) another principal's cached entry.
4418
- return `${appId}:o${hashForKey$1(String(Date.now()) + Math.random())}`;
4363
+ return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
4419
4364
  }
4420
4365
  }
4421
- // Direct per-request header override. makeApiRequest applies overrides.headers
4422
- // AFTER its computed auth header (api.ts), so a caller-supplied Authorization
4423
- // is the REAL request auth — key the cache by it so an entry is never served
4424
- // to a different principal under the ambient key. (Hardening: no in-repo read
4425
- // caller passes this today, but the cache must never trail the actual auth.)
4426
- const directAuth = ((_a = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _a === void 0 ? void 0 : _a.Authorization) ||
4427
- ((_b = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _b === void 0 ? void 0 : _b.authorization);
4428
- if (directAuth) {
4429
- const bearer = directAuth.startsWith('Bearer ')
4430
- ? directAuth.slice('Bearer '.length)
4431
- : directAuth;
4432
- return `${appId}:h${bearer ? principalFromIdToken$1(bearer) : hashForKey$1(directAuth)}`;
4433
- }
4434
4366
  // Ambient session principal.
4435
4367
  const idToken = await getIdToken(config.isServer);
4436
4368
  return `${appId}:${principalFromIdToken$1(idToken)}`;
@@ -4730,9 +4662,9 @@ async function get(path, opts = {}) {
4730
4662
  const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
4731
4663
  const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
4732
4664
  const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
4733
- const cursorKey = opts.cursor ? `:c${hashForKey$1(opts.cursor)}` : '';
4734
- const filterKey = opts.filter ? `:f${hashForKey$1(JSON.stringify(opts.filter))}` : '';
4735
- const sortKey = opts.sort ? `:s${hashForKey$1(JSON.stringify(opts.sort))}` : '';
4665
+ const cursorKey = opts.cursor ? `:c${hashForKey$2(opts.cursor)}` : '';
4666
+ const filterKey = opts.filter ? `:f${hashForKey$2(JSON.stringify(opts.filter))}` : '';
4667
+ const sortKey = opts.sort ? `:s${hashForKey$2(JSON.stringify(opts.sort))}` : '';
4736
4668
  const principalKey = await getReadPrincipalKey(opts._overrides);
4737
4669
  const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
4738
4670
  const now = Date.now();
@@ -5482,7 +5414,7 @@ let reconnectInProgress = null;
5482
5414
  function generateSubscriptionId() {
5483
5415
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
5484
5416
  }
5485
- function hashForKey(value) {
5417
+ function hashForKey$1(value) {
5486
5418
  let h = 5381;
5487
5419
  for (let i = 0; i < value.length; i++) {
5488
5420
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
@@ -5501,49 +5433,23 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5501
5433
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
5502
5434
  const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
5503
5435
  const limitKey = limit !== undefined ? `:l${limit}` : '';
5504
- const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
5505
- const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey(JSON.stringify(filter))}` : '';
5436
+ const cursorKey = cursor ? `:c${hashForKey$1(cursor)}` : '';
5437
+ const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey$1(JSON.stringify(filter))}` : '';
5506
5438
  const identityKey = identity || 'anon';
5507
5439
  return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
5508
5440
  }
5509
5441
  /**
5510
- * Derive a stable identity string for the principal a subscription authenticates
5511
- * as: `<appId>:<principalFingerprint>`. Mirrors the operations.ts logic so HTTP
5512
- * and WS caches scope reads to the same identity.
5442
+ * Derive an opaque identity string for the bearer material a subscription sends.
5443
+ * JWT payloads are deliberately not trusted here: cache isolation must happen
5444
+ * before the server has verified any claims.
5513
5445
  */
5514
5446
  function principalFromIdToken(idToken) {
5515
- if (!idToken)
5516
- return 'anon';
5517
- try {
5518
- const parts = idToken.split('.');
5519
- if (parts.length < 2)
5520
- return `t${hashForKey(idToken)}`;
5521
- const payload = JSON.parse(decodeBase64Url(parts[1]));
5522
- const subject =
5523
- // Universal identity (@user.id) first — the stable principal a request
5524
- // presents (the account id for email/social logins, which carry no
5525
- // walletAddress). Keeps the H1 response-cache scoping correct for them.
5526
- payload['custom:userId'] ||
5527
- payload['custom:walletAddress'] ||
5528
- payload.walletAddress ||
5529
- payload.sub ||
5530
- payload.address;
5531
- if (subject)
5532
- return `s${hashForKey(String(subject))}`;
5533
- return `t${hashForKey(idToken)}`;
5534
- }
5535
- catch (_a) {
5536
- return `t${hashForKey(idToken)}`;
5537
- }
5447
+ return idToken ? `t${hashForKey$1(idToken)}` : 'anon';
5538
5448
  }
5539
5449
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5540
- // Per-subscription wallet override (server WalletClient.subscribe): key by the
5541
- // wallet's own token, mirroring getReadPrincipalKey, so a wallet client never
5542
- // consults and never crashes on — the absent ambient env keypair, and its
5543
- // cached snapshots are never crossed with another principal's.
5544
- if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
5545
- return `${effectiveAppId}:w${overrides._walletAddress}`;
5546
- }
5450
+ // Per-subscription wallet override (server WalletClient.subscribe): key by
5451
+ // the wallet's own opaque token material, mirroring getReadPrincipalKey. Do
5452
+ // not trust decoded claims or the unverified _walletAddress hint.
5547
5453
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
5548
5454
  try {
5549
5455
  const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
@@ -5914,7 +5820,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5914
5820
  return connection;
5915
5821
  }
5916
5822
  function handleServerMessage(connection, message) {
5917
- var _a, _b;
5823
+ var _a;
5918
5824
  switch (message.type) {
5919
5825
  case 'authenticated': {
5920
5826
  connection.isAuthenticating = false;
@@ -6008,28 +5914,27 @@ function handleServerMessage(connection, message) {
6008
5914
  err.subscriptionId = message.subscriptionId;
6009
5915
  return err;
6010
5916
  };
5917
+ const wsError = buildWsError();
6011
5918
  // Handle CRUD request errors (requestId present)
6012
5919
  if (message.requestId) {
6013
5920
  const pendingReq = connection.pendingRequests.get(message.requestId);
6014
5921
  if (pendingReq) {
6015
5922
  connection.pendingRequests.delete(message.requestId);
6016
5923
  clearTimeout(pendingReq.timer);
6017
- pendingReq.reject(buildWsError());
5924
+ pendingReq.reject(wsError);
6018
5925
  }
6019
5926
  }
6020
5927
  if (message.subscriptionId) {
6021
5928
  // Reject pending subscription if this is a subscription error
6022
5929
  const pending = connection.pendingSubscriptions.get(message.subscriptionId);
6023
5930
  if (pending) {
6024
- pending.reject(buildWsError());
5931
+ pending.reject(wsError);
6025
5932
  connection.pendingSubscriptions.delete(message.subscriptionId);
6026
5933
  }
6027
5934
  // Notify error callbacks for this subscription
6028
5935
  const subscription = connection.subscriptions.get(message.subscriptionId);
6029
5936
  if (subscription) {
6030
- for (const callback of subscription.callbacks) {
6031
- (_b = callback.onError) === null || _b === void 0 ? void 0 : _b.call(callback, buildWsError());
6032
- }
5937
+ notifyErrorCallbacks(subscription, wsError);
6033
5938
  }
6034
5939
  }
6035
5940
  break;
@@ -6082,6 +5987,25 @@ function notifyCallbacks(subscription, data) {
6082
5987
  }
6083
5988
  }
6084
5989
  }
5990
+ function notifyErrorCallbacks(subscription, error) {
5991
+ let delivered = false;
5992
+ const callbacks = subscription.callbacks.slice();
5993
+ for (const callback of callbacks) {
5994
+ if (!callback.onError)
5995
+ continue;
5996
+ delivered = true;
5997
+ try {
5998
+ callback.onError(error);
5999
+ }
6000
+ catch (callbackError) {
6001
+ console.error('[WS v2] Error in subscription error callback:', callbackError);
6002
+ }
6003
+ }
6004
+ if (delivered) {
6005
+ error.__boundedDeliveredToOnError = true;
6006
+ }
6007
+ return delivered;
6008
+ }
6085
6009
  // WebSocket readyState constants
6086
6010
  const WS_READY_STATE_OPEN = 1;
6087
6011
  const WS_READY_STATE_CLOSED = 3;
@@ -6156,10 +6080,8 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6156
6080
  const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
6157
6081
  ? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
6158
6082
  : undefined;
6159
- const principalKey = (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress)
6160
- ? `w${overrides._walletAddress}`
6161
- : (authTokenProvider ? `o${principalFromIdToken(await authTokenProvider())}` : undefined);
6162
6083
  const identity = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6084
+ const principalKey = authTokenProvider ? identity : undefined;
6163
6085
  const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity);
6164
6086
  // Deliver cached data immediately if available
6165
6087
  const cachedEntry = responseCache.get(cacheKey);
@@ -6238,7 +6160,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6238
6160
  await subscriptionPromise;
6239
6161
  }
6240
6162
  catch (error) {
6241
- console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
6163
+ const err = error instanceof Error ? error : new Error(String(error));
6164
+ if (!err.__boundedDeliveredToOnError) {
6165
+ notifyErrorCallbacks(subscription, err);
6166
+ }
6167
+ if (!subscriptionOptions.onError) {
6168
+ connection.pendingSubscriptions.delete(subscriptionId);
6169
+ connection.subscriptions.delete(subscriptionId);
6170
+ throw err;
6171
+ }
6172
+ if (!err.__boundedDeliveredToOnError) {
6173
+ console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
6174
+ }
6242
6175
  }
6243
6176
  }
6244
6177
  // Return unsubscribe function
@@ -6401,6 +6334,13 @@ async function doReconnectWithNewAuth() {
6401
6334
  catch (error) {
6402
6335
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6403
6336
  }
6337
+ try {
6338
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return realtimeStore; });
6339
+ await reconnectRealtimeStoreWithNewAuth();
6340
+ }
6341
+ catch (error) {
6342
+ console.warn('[WS v2] Failed to reset legacy realtime store on auth change:', error);
6343
+ }
6404
6344
  for (const [appId, connection] of connections) {
6405
6345
  if (!connection.ws) {
6406
6346
  continue;
@@ -6868,6 +6808,16 @@ async function idbSet(key, value) {
6868
6808
  // RealtimeStore
6869
6809
  // ---------------------------------------------------------------------------
6870
6810
  let nextRequestId = 1;
6811
+ function hashForKey(value) {
6812
+ let h = 5381;
6813
+ for (let i = 0; i < value.length; i++) {
6814
+ h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
6815
+ }
6816
+ return h.toString(36);
6817
+ }
6818
+ function principalFromToken(token) {
6819
+ return token ? `t${hashForKey(token)}` : 'anon';
6820
+ }
6871
6821
  class RealtimeStore {
6872
6822
  constructor() {
6873
6823
  this.ws = null;
@@ -6883,7 +6833,9 @@ class RealtimeStore {
6883
6833
  this.idbDirtyKeys = new Set();
6884
6834
  this.closed = false;
6885
6835
  this.authToken = null;
6836
+ this.authPrincipalKey = 'anon';
6886
6837
  this.authenticating = false;
6838
+ this.suppressNextReconnect = false;
6887
6839
  this.isServer = false;
6888
6840
  this.tokenRefreshTimer = null;
6889
6841
  // -----------------------------------------------------------------------
@@ -6903,34 +6855,98 @@ class RealtimeStore {
6903
6855
  this.startTokenRefresh();
6904
6856
  }
6905
6857
  async refreshToken() {
6858
+ let token = null;
6906
6859
  try {
6907
6860
  const { getIdToken } = await Promise.resolve().then(function () { return utils; });
6908
- const token = await getIdToken(this.isServer);
6909
- if (token)
6910
- this.authToken = token;
6861
+ token = await getIdToken(this.isServer);
6911
6862
  }
6912
6863
  catch ( /* no auth available */_a) { /* no auth available */ }
6864
+ this.authToken = token !== null && token !== void 0 ? token : null;
6865
+ this.authPrincipalKey = principalFromToken(this.authToken);
6913
6866
  }
6914
6867
  startTokenRefresh() {
6915
6868
  if (this.tokenRefreshTimer)
6916
6869
  return;
6917
6870
  this.tokenRefreshTimer = setInterval(async () => {
6918
- var _a;
6919
- const prevToken = this.authToken;
6871
+ const prevPrincipal = this.authPrincipalKey;
6920
6872
  await this.refreshToken();
6921
- if (this.authToken && this.authToken !== prevToken && ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
6922
- // Token changed — reconnect with fresh token
6923
- this.ws.close(1000, 'Token refreshed');
6873
+ if (this.authPrincipalKey !== prevPrincipal) {
6874
+ await this.applyAuthPrincipalChange();
6875
+ if (this.subscriptions.size > 0) {
6876
+ await this.ensureConnected().catch(() => {
6877
+ this.setAllSubscriptionStatus('error');
6878
+ });
6879
+ }
6924
6880
  }
6925
6881
  }, 5 * 60 * 1000); // Check every 5 minutes
6926
6882
  }
6883
+ async ensureInitialized() {
6884
+ if (this.appId)
6885
+ return;
6886
+ if (!this.initPromise)
6887
+ this.initPromise = this.init();
6888
+ await this.initPromise;
6889
+ }
6890
+ async ensureCurrentAuth() {
6891
+ await this.ensureInitialized();
6892
+ const prevPrincipal = this.authPrincipalKey;
6893
+ await this.refreshToken();
6894
+ if (this.authPrincipalKey !== prevPrincipal) {
6895
+ await this.applyAuthPrincipalChange();
6896
+ }
6897
+ }
6898
+ rekeySubscriptionsForPrincipal() {
6899
+ const subs = Array.from(this.subscriptions.values());
6900
+ this.subscriptions.clear();
6901
+ for (const sub of subs) {
6902
+ this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);
6903
+ }
6904
+ }
6905
+ async applyAuthPrincipalChange() {
6906
+ if (this.idbFlushTimer) {
6907
+ clearTimeout(this.idbFlushTimer);
6908
+ this.idbFlushTimer = null;
6909
+ }
6910
+ this.idbDirtyKeys.clear();
6911
+ this.rekeySubscriptionsForPrincipal();
6912
+ for (const sub of this.subscriptions.values()) {
6913
+ sub.docs.clear();
6914
+ sub.ref.current = sub.docs;
6915
+ sub.error = null;
6916
+ sub.isStale = false;
6917
+ let loaded = false;
6918
+ if (sub.tier !== 'ephemeral') {
6919
+ const cached = await idbGet(this.idbKey(sub.path));
6920
+ if (cached && Array.isArray(cached)) {
6921
+ for (const doc of cached) {
6922
+ if (doc && doc._id)
6923
+ sub.docs.set(doc._id, doc);
6924
+ }
6925
+ sub.ref.current = sub.docs;
6926
+ loaded = sub.docs.size > 0;
6927
+ }
6928
+ }
6929
+ sub.status = loaded ? 'cached' : 'loading';
6930
+ sub.isStale = loaded;
6931
+ if (loaded)
6932
+ this.notifySubscription(sub);
6933
+ else
6934
+ this.notifyState(sub);
6935
+ }
6936
+ if (this.ws) {
6937
+ const ws = this.ws;
6938
+ this.ws = null;
6939
+ this.connectPromise = null;
6940
+ this.suppressNextReconnect = true;
6941
+ try {
6942
+ ws.close(1000, 'Auth changed');
6943
+ }
6944
+ catch ( /* ignore */_a) { /* ignore */ }
6945
+ }
6946
+ }
6927
6947
  async ensureConnected() {
6928
6948
  var _a;
6929
- if (!this.appId) {
6930
- if (!this.initPromise)
6931
- this.initPromise = this.init();
6932
- await this.initPromise;
6933
- }
6949
+ await this.ensureCurrentAuth();
6934
6950
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
6935
6951
  return;
6936
6952
  if (this.connectPromise)
@@ -7014,11 +7030,20 @@ class RealtimeStore {
7014
7030
  ws.addEventListener('close', () => {
7015
7031
  if (authTimer)
7016
7032
  clearTimeout(authTimer);
7033
+ if (this.ws !== ws) {
7034
+ if (this.suppressNextReconnect)
7035
+ this.suppressNextReconnect = false;
7036
+ return;
7037
+ }
7017
7038
  this.authenticating = false;
7018
7039
  this.ws = null;
7019
7040
  this.connectPromise = null;
7020
7041
  this.rejectAllPending('WebSocket closed');
7021
7042
  this.setAllSubscriptionStatus('reconnecting');
7043
+ if (this.suppressNextReconnect) {
7044
+ this.suppressNextReconnect = false;
7045
+ return;
7046
+ }
7022
7047
  this.scheduleReconnect();
7023
7048
  });
7024
7049
  });
@@ -7194,14 +7219,34 @@ class RealtimeStore {
7194
7219
  }
7195
7220
  }
7196
7221
  handleError(msg) {
7197
- var _a;
7222
+ var _a, _b, _c;
7223
+ const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));
7224
+ if (msg.code)
7225
+ error.code = msg.code;
7226
+ if (msg.subscriptionId || msg.id)
7227
+ error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;
7198
7228
  const requestId = msg.requestId;
7199
7229
  if (requestId) {
7200
7230
  const pending = this.pendingRequests.get(requestId);
7201
7231
  if (pending) {
7202
7232
  this.pendingRequests.delete(requestId);
7203
7233
  clearTimeout(pending.timeout);
7204
- pending.reject(new Error((_a = msg.message) !== null && _a !== void 0 ? _a : 'Server error'));
7234
+ pending.reject(error);
7235
+ }
7236
+ }
7237
+ const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;
7238
+ if (subId) {
7239
+ const sub = this.findSubscriptionById(subId);
7240
+ if (sub) {
7241
+ sub.status = 'error';
7242
+ sub.error = error;
7243
+ this.notifyState(sub);
7244
+ for (const callback of Array.from(sub.errorCallbacks)) {
7245
+ try {
7246
+ callback(error);
7247
+ }
7248
+ catch ( /* swallow */_d) { /* swallow */ }
7249
+ }
7205
7250
  }
7206
7251
  }
7207
7252
  }
@@ -7210,6 +7255,7 @@ class RealtimeStore {
7210
7255
  // -----------------------------------------------------------------------
7211
7256
  async subscribe(path, opts = {}) {
7212
7257
  var _a;
7258
+ await this.ensureCurrentAuth();
7213
7259
  const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
7214
7260
  const subKey = this.getSubKey(path, opts);
7215
7261
  let sub = this.subscriptions.get(subKey);
@@ -7219,6 +7265,8 @@ class RealtimeStore {
7219
7265
  sub.callbacks.add(opts.onData);
7220
7266
  if (opts.onState)
7221
7267
  sub.stateCallbacks.add(opts.onState);
7268
+ if (opts.onError)
7269
+ sub.errorCallbacks.add(opts.onError);
7222
7270
  // Immediately deliver current state
7223
7271
  if (opts.onData && sub.docs.size > 0) {
7224
7272
  opts.onData(this.docsToArray(sub));
@@ -7226,7 +7274,7 @@ class RealtimeStore {
7226
7274
  if (opts.onState) {
7227
7275
  opts.onState(this.getState(sub));
7228
7276
  }
7229
- return this.createUnsubscribe(subKey, opts.onData, opts.onState);
7277
+ return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7230
7278
  }
7231
7279
  // New subscription
7232
7280
  const subId = `sub_${nextRequestId++}`;
@@ -7241,6 +7289,7 @@ class RealtimeStore {
7241
7289
  error: null,
7242
7290
  callbacks: new Set(opts.onData ? [opts.onData] : []),
7243
7291
  stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
7292
+ errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
7244
7293
  ref: { current: new Map() },
7245
7294
  };
7246
7295
  this.subscriptions.set(subKey, sub);
@@ -7270,7 +7319,7 @@ class RealtimeStore {
7270
7319
  sub.error = new Error('Connection failed');
7271
7320
  this.notifyState(sub);
7272
7321
  }
7273
- return this.createUnsubscribe(subKey, opts.onData, opts.onState);
7322
+ return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7274
7323
  }
7275
7324
  getRef(path, opts = {}) {
7276
7325
  var _a;
@@ -7344,6 +7393,7 @@ class RealtimeStore {
7344
7393
  }
7345
7394
  }
7346
7395
  async get(path) {
7396
+ await this.ensureCurrentAuth();
7347
7397
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7348
7398
  // Check local subscriptions first
7349
7399
  const collectionPath = this.getCollectionPath(normalizedPath);
@@ -7524,7 +7574,7 @@ class RealtimeStore {
7524
7574
  return docPath;
7525
7575
  }
7526
7576
  getSubKey(path, opts) {
7527
- const parts = [path];
7577
+ const parts = [this.appId, this.authPrincipalKey, path];
7528
7578
  if (opts.filter)
7529
7579
  parts.push(JSON.stringify(opts.filter));
7530
7580
  if (opts.prompt)
@@ -7534,7 +7584,7 @@ class RealtimeStore {
7534
7584
  return parts.join('::');
7535
7585
  }
7536
7586
  idbKey(path) {
7537
- return `${this.appId}:${path}`;
7587
+ return `${this.appId}:${this.authPrincipalKey}:${path}`;
7538
7588
  }
7539
7589
  markIdbDirty(path) {
7540
7590
  const sub = this.findSubscriptionByPath(path);
@@ -7559,18 +7609,23 @@ class RealtimeStore {
7559
7609
  }
7560
7610
  }
7561
7611
  }
7562
- createUnsubscribe(subKey, onData, onState) {
7612
+ createUnsubscribe(subKey, subId, onData, onState, onError) {
7563
7613
  return async () => {
7564
- const sub = this.subscriptions.get(subKey);
7614
+ var _a;
7615
+ const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);
7565
7616
  if (!sub)
7566
7617
  return;
7618
+ const currentSubKey = this.getSubKey(sub.path, sub.options);
7567
7619
  if (onData)
7568
7620
  sub.callbacks.delete(onData);
7569
7621
  if (onState)
7570
7622
  sub.stateCallbacks.delete(onState);
7623
+ if (onError)
7624
+ sub.errorCallbacks.delete(onError);
7571
7625
  // If no more callbacks, unsubscribe entirely
7572
- if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0) {
7626
+ if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
7573
7627
  this.subscriptions.delete(subKey);
7628
+ this.subscriptions.delete(currentSubKey);
7574
7629
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
7575
7630
  this.ws.send(JSON.stringify({
7576
7631
  type: 'unsubscribe',
@@ -7642,6 +7697,22 @@ class RealtimeStore {
7642
7697
  this.rejectAllPending('Store closed');
7643
7698
  this.subscriptions.clear();
7644
7699
  }
7700
+ async reconnectWithNewAuth() {
7701
+ if (this.closed)
7702
+ return;
7703
+ await this.ensureInitialized();
7704
+ await this.refreshToken();
7705
+ await this.applyAuthPrincipalChange();
7706
+ if (this.subscriptions.size > 0) {
7707
+ await this.ensureConnected().catch((error) => {
7708
+ this.setAllSubscriptionStatus('error');
7709
+ for (const sub of this.subscriptions.values()) {
7710
+ sub.error = error instanceof Error ? error : new Error(String(error));
7711
+ this.notifyState(sub);
7712
+ }
7713
+ });
7714
+ }
7715
+ }
7645
7716
  }
7646
7717
  // ---------------------------------------------------------------------------
7647
7718
  // Singleton instance
@@ -7659,6 +7730,19 @@ function resetRealtimeStore() {
7659
7730
  storeInstance = null;
7660
7731
  }
7661
7732
  }
7733
+ async function reconnectRealtimeStoreWithNewAuth() {
7734
+ if (storeInstance) {
7735
+ await storeInstance.reconnectWithNewAuth();
7736
+ }
7737
+ }
7738
+
7739
+ var realtimeStore = /*#__PURE__*/Object.freeze({
7740
+ __proto__: null,
7741
+ RealtimeStore: RealtimeStore,
7742
+ getRealtimeStore: getRealtimeStore,
7743
+ reconnectRealtimeStoreWithNewAuth: reconnectRealtimeStoreWithNewAuth,
7744
+ resetRealtimeStore: resetRealtimeStore
7745
+ });
7662
7746
 
7663
7747
  // ---------------------------------------------------------------------------
7664
7748
  // functions.ts -- Bounded Functions client (the imperative escape hatch).
@@ -7814,6 +7898,23 @@ function realtimeHttpBase(wsApiUrl) {
7814
7898
  // Strip trailing slash from the resulting origin+path.
7815
7899
  return url.toString().replace(/\/$/, '');
7816
7900
  }
7901
+ function withoutAuthorization(headers) {
7902
+ if (!headers)
7903
+ return undefined;
7904
+ const clean = {};
7905
+ for (const [key, value] of Object.entries(headers)) {
7906
+ if (key.toLowerCase() === 'authorization')
7907
+ continue;
7908
+ clean[key] = value;
7909
+ }
7910
+ return Object.keys(clean).length > 0 ? clean : undefined;
7911
+ }
7912
+ async function liveAuthHeader(configIsServer, overrides) {
7913
+ if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
7914
+ return overrides._getAuthHeaders();
7915
+ }
7916
+ return createAuthHeader(configIsServer);
7917
+ }
7817
7918
  /**
7818
7919
  * Send a player intent to a running live room. Returns `{ ok: true }`.
7819
7920
  *
@@ -7828,7 +7929,7 @@ function realtimeHttpBase(wsApiUrl) {
7828
7929
  * transport error / timeout.
7829
7930
  */
7830
7931
  async function intent(roomPath, intent, opts = {}) {
7831
- var _a, _b, _c, _d, _e;
7932
+ var _a, _b, _c, _d, _e, _f, _g;
7832
7933
  if (!roomPath || typeof roomPath !== 'string') {
7833
7934
  throw new LiveIntentError('A room path is required');
7834
7935
  }
@@ -7849,37 +7950,48 @@ async function intent(roomPath, intent, opts = {}) {
7849
7950
  // (e.g. the first join before subscribeView's WS connects) — HTTP also throws
7850
7951
  // on a non-2xx, so errors are surfaced there too.
7851
7952
  const normalizedRoomPath = roomPath.replace(/\/$/, '');
7852
- if (opts.fireAndForget) {
7853
- if (wsIntent(config.appId, normalizedRoomPath, intent))
7854
- return { ok: true };
7855
- }
7856
- else {
7857
- const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
7858
- if (ack)
7859
- return await ack;
7953
+ const hasAuthOverride = !!((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders);
7954
+ if (!hasAuthOverride) {
7955
+ if (opts.fireAndForget) {
7956
+ if (wsIntent(config.appId, normalizedRoomPath, intent))
7957
+ return { ok: true };
7958
+ }
7959
+ else {
7960
+ const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
7961
+ if (ack)
7962
+ return await ack;
7963
+ }
7860
7964
  }
7861
7965
  const base = realtimeHttpBase(config.wsApiUrl);
7862
- // Attach the caller's session token automatically (same token as data calls).
7863
- const authHeader = await createAuthHeader(config.isServer);
7864
- 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 : {}));
7966
+ const extraHeaders = withoutAuthorization(opts.headers);
7967
+ const overrideHeaders = withoutAuthorization((_b = opts._overrides) === null || _b === void 0 ? void 0 : _b.headers);
7968
+ const buildHeaders = async () => {
7969
+ const authHeader = await liveAuthHeader(config.isServer, opts._overrides);
7970
+ 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 : {}));
7971
+ };
7865
7972
  const controller = new AbortController();
7866
- const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 60000;
7973
+ const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
7867
7974
  const timer = setTimeout(() => controller.abort(), timeoutMs);
7868
7975
  let res;
7869
7976
  try {
7870
- res = await fetch(`${base}/live/intent`, {
7977
+ const send = async () => fetch(`${base}/live/intent`, {
7871
7978
  method: 'POST',
7872
- headers,
7979
+ headers: await buildHeaders(),
7873
7980
  body: JSON.stringify({ path: normalizedRoomPath, intent }),
7874
7981
  signal: controller.signal,
7875
7982
  });
7983
+ res = await send();
7984
+ if (res.status === 401 && ((_d = opts._overrides) === null || _d === void 0 ? void 0 : _d._clearAuth)) {
7985
+ await opts._overrides._clearAuth();
7986
+ res = await send();
7987
+ }
7876
7988
  }
7877
7989
  catch (err) {
7878
7990
  clearTimeout(timer);
7879
7991
  if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError') {
7880
7992
  throw new LiveIntentError(`Live intent to "${roomPath}" timed out after ${timeoutMs}ms`);
7881
7993
  }
7882
- 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)}`);
7994
+ 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)}`);
7883
7995
  }
7884
7996
  clearTimeout(timer);
7885
7997
  let body = null;
@@ -7888,12 +8000,12 @@ async function intent(roomPath, intent, opts = {}) {
7888
8000
  try {
7889
8001
  body = JSON.parse(text);
7890
8002
  }
7891
- catch (_f) {
8003
+ catch (_h) {
7892
8004
  body = { raw: text };
7893
8005
  }
7894
8006
  }
7895
8007
  if (!res.ok) {
7896
- 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}`;
8008
+ 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}`;
7897
8009
  throw new LiveIntentError(message, res.status, body);
7898
8010
  }
7899
8011
  return (body !== null && body !== void 0 ? body : { ok: true });