@bounded-sh/core 0.0.15 → 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
@@ -4315,99 +4315,54 @@ function normalizeReadResult(responseData, pathIsDocument) {
4315
4315
  }
4316
4316
  return responseData;
4317
4317
  }
4318
- function hashForKey$1(value) {
4318
+ function hashForKey$2(value) {
4319
4319
  let h = 5381;
4320
4320
  for (let i = 0; i < value.length; i++) {
4321
4321
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
4322
4322
  }
4323
4323
  return h.toString(36);
4324
4324
  }
4325
- /**
4326
- * Derive a stable identity string for the JWT a request will actually present.
4327
- * Prefers the subject-ish claims (sub / custom:walletAddress / address) so the
4328
- * same principal across token refreshes maps to the same fingerprint; falls back
4329
- * to a hash of the raw token if the payload can't be decoded.
4330
- */
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
+ }
4331
4331
  function principalFromIdToken$1(idToken) {
4332
- if (!idToken)
4333
- return 'anon';
4334
- try {
4335
- const parts = idToken.split('.');
4336
- if (parts.length < 2)
4337
- return `t${hashForKey$1(idToken)}`;
4338
- const payload = JSON.parse(decodeBase64Url(parts[1]));
4339
- const subject =
4340
- // Prefer the universal identity (@user.id) — it is the stable principal a
4341
- // request presents (equals the wallet for wallet logins; the account id for
4342
- // email/social logins, which carry no walletAddress). Keying the read cache
4343
- // on it keeps an email user's private snapshot scoped to that user.
4344
- payload['custom:userId'] ||
4345
- payload['custom:walletAddress'] ||
4346
- payload.walletAddress ||
4347
- payload.sub ||
4348
- payload.address;
4349
- if (subject)
4350
- return `s${hashForKey$1(String(subject))}`;
4351
- return `t${hashForKey$1(idToken)}`;
4352
- }
4353
- catch (_a) {
4354
- return `t${hashForKey$1(idToken)}`;
4355
- }
4332
+ return idToken ? `t${hashForKey$2(idToken)}` : 'anon';
4356
4333
  }
4357
4334
  /**
4358
4335
  * SECURITY (H1): Read caches must be keyed by the caller's principal, not just
4359
4336
  * by path/filter/shape. In a shared process / SSR worker / browser login-switch,
4360
4337
  * keying by path alone lets User B receive User A's cached private read before
4361
4338
  * any server read rule runs. This returns `appId:<principal>` for the identity a
4362
- * given read will actually authenticate as:
4363
- * - a per-request `_walletAddress` override that override's wallet
4364
- * - a per-request `_getAuthHeaders` override a hash of the auth header it
4365
- * produces (so a cross-principal cached entry is never served to it)
4366
- * - 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.
4367
4343
  */
4368
4344
  async function getReadPrincipalKey(overrides) {
4369
- var _a, _b;
4370
4345
  const config = await getConfig();
4371
4346
  const appId = config.appId || '';
4372
- // Explicit per-request wallet override key by that identity.
4373
- if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
4374
- 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)}`;
4375
4352
  }
4376
- // Per-request auth-header override (wallet client). Key by the actual header
4377
- // 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.
4378
4355
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
4379
4356
  try {
4380
4357
  const headers = await overrides._getAuthHeaders();
4381
- const authValue = (headers === null || headers === void 0 ? void 0 : headers.Authorization) ||
4382
- (headers === null || headers === void 0 ? void 0 : headers.authorization) ||
4383
- '';
4384
- // Decode the bearer token's subject when present for a stable key across
4385
- // refreshes; otherwise hash whatever header material we were given.
4386
- const bearer = authValue.startsWith('Bearer ')
4387
- ? authValue.slice('Bearer '.length)
4388
- : authValue;
4389
- const principal = bearer ? principalFromIdToken$1(bearer) : 'anon';
4390
- return `${appId}:o${principal}`;
4391
- }
4392
- catch (_c) {
4358
+ return `${appId}:o${principalFromAuthValue(authValueFromHeaders(headers))}`;
4359
+ }
4360
+ catch (_a) {
4393
4361
  // If we can't resolve the override identity, use a unique-ish key so we
4394
4362
  // never collide with (and serve) another principal's cached entry.
4395
- return `${appId}:o${hashForKey$1(String(Date.now()) + Math.random())}`;
4363
+ return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
4396
4364
  }
4397
4365
  }
4398
- // Direct per-request header override. makeApiRequest applies overrides.headers
4399
- // AFTER its computed auth header (api.ts), so a caller-supplied Authorization
4400
- // is the REAL request auth — key the cache by it so an entry is never served
4401
- // to a different principal under the ambient key. (Hardening: no in-repo read
4402
- // caller passes this today, but the cache must never trail the actual auth.)
4403
- const directAuth = ((_a = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _a === void 0 ? void 0 : _a.Authorization) ||
4404
- ((_b = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _b === void 0 ? void 0 : _b.authorization);
4405
- if (directAuth) {
4406
- const bearer = directAuth.startsWith('Bearer ')
4407
- ? directAuth.slice('Bearer '.length)
4408
- : directAuth;
4409
- return `${appId}:h${bearer ? principalFromIdToken$1(bearer) : hashForKey$1(directAuth)}`;
4410
- }
4411
4366
  // Ambient session principal.
4412
4367
  const idToken = await getIdToken(config.isServer);
4413
4368
  return `${appId}:${principalFromIdToken$1(idToken)}`;
@@ -4707,9 +4662,9 @@ async function get(path, opts = {}) {
4707
4662
  const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
4708
4663
  const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
4709
4664
  const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
4710
- const cursorKey = opts.cursor ? `:c${hashForKey$1(opts.cursor)}` : '';
4711
- const filterKey = opts.filter ? `:f${hashForKey$1(JSON.stringify(opts.filter))}` : '';
4712
- 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))}` : '';
4713
4668
  const principalKey = await getReadPrincipalKey(opts._overrides);
4714
4669
  const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
4715
4670
  const now = Date.now();
@@ -5459,7 +5414,7 @@ let reconnectInProgress = null;
5459
5414
  function generateSubscriptionId() {
5460
5415
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
5461
5416
  }
5462
- function hashForKey(value) {
5417
+ function hashForKey$1(value) {
5463
5418
  let h = 5381;
5464
5419
  for (let i = 0; i < value.length; i++) {
5465
5420
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
@@ -5478,49 +5433,23 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5478
5433
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
5479
5434
  const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
5480
5435
  const limitKey = limit !== undefined ? `:l${limit}` : '';
5481
- const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
5482
- 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))}` : '';
5483
5438
  const identityKey = identity || 'anon';
5484
5439
  return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
5485
5440
  }
5486
5441
  /**
5487
- * Derive a stable identity string for the principal a subscription authenticates
5488
- * as: `<appId>:<principalFingerprint>`. Mirrors the operations.ts logic so HTTP
5489
- * 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.
5490
5445
  */
5491
5446
  function principalFromIdToken(idToken) {
5492
- if (!idToken)
5493
- return 'anon';
5494
- try {
5495
- const parts = idToken.split('.');
5496
- if (parts.length < 2)
5497
- return `t${hashForKey(idToken)}`;
5498
- const payload = JSON.parse(decodeBase64Url(parts[1]));
5499
- const subject =
5500
- // Universal identity (@user.id) first — the stable principal a request
5501
- // presents (the account id for email/social logins, which carry no
5502
- // walletAddress). Keeps the H1 response-cache scoping correct for them.
5503
- payload['custom:userId'] ||
5504
- payload['custom:walletAddress'] ||
5505
- payload.walletAddress ||
5506
- payload.sub ||
5507
- payload.address;
5508
- if (subject)
5509
- return `s${hashForKey(String(subject))}`;
5510
- return `t${hashForKey(idToken)}`;
5511
- }
5512
- catch (_a) {
5513
- return `t${hashForKey(idToken)}`;
5514
- }
5447
+ return idToken ? `t${hashForKey$1(idToken)}` : 'anon';
5515
5448
  }
5516
5449
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5517
- // Per-subscription wallet override (server WalletClient.subscribe): key by the
5518
- // wallet's own token, mirroring getReadPrincipalKey, so a wallet client never
5519
- // consults and never crashes on — the absent ambient env keypair, and its
5520
- // cached snapshots are never crossed with another principal's.
5521
- if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
5522
- return `${effectiveAppId}:w${overrides._walletAddress}`;
5523
- }
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.
5524
5453
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
5525
5454
  try {
5526
5455
  const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
@@ -5891,7 +5820,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5891
5820
  return connection;
5892
5821
  }
5893
5822
  function handleServerMessage(connection, message) {
5894
- var _a, _b;
5823
+ var _a;
5895
5824
  switch (message.type) {
5896
5825
  case 'authenticated': {
5897
5826
  connection.isAuthenticating = false;
@@ -5985,28 +5914,27 @@ function handleServerMessage(connection, message) {
5985
5914
  err.subscriptionId = message.subscriptionId;
5986
5915
  return err;
5987
5916
  };
5917
+ const wsError = buildWsError();
5988
5918
  // Handle CRUD request errors (requestId present)
5989
5919
  if (message.requestId) {
5990
5920
  const pendingReq = connection.pendingRequests.get(message.requestId);
5991
5921
  if (pendingReq) {
5992
5922
  connection.pendingRequests.delete(message.requestId);
5993
5923
  clearTimeout(pendingReq.timer);
5994
- pendingReq.reject(buildWsError());
5924
+ pendingReq.reject(wsError);
5995
5925
  }
5996
5926
  }
5997
5927
  if (message.subscriptionId) {
5998
5928
  // Reject pending subscription if this is a subscription error
5999
5929
  const pending = connection.pendingSubscriptions.get(message.subscriptionId);
6000
5930
  if (pending) {
6001
- pending.reject(buildWsError());
5931
+ pending.reject(wsError);
6002
5932
  connection.pendingSubscriptions.delete(message.subscriptionId);
6003
5933
  }
6004
5934
  // Notify error callbacks for this subscription
6005
5935
  const subscription = connection.subscriptions.get(message.subscriptionId);
6006
5936
  if (subscription) {
6007
- for (const callback of subscription.callbacks) {
6008
- (_b = callback.onError) === null || _b === void 0 ? void 0 : _b.call(callback, buildWsError());
6009
- }
5937
+ notifyErrorCallbacks(subscription, wsError);
6010
5938
  }
6011
5939
  }
6012
5940
  break;
@@ -6059,6 +5987,25 @@ function notifyCallbacks(subscription, data) {
6059
5987
  }
6060
5988
  }
6061
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
+ }
6062
6009
  // WebSocket readyState constants
6063
6010
  const WS_READY_STATE_OPEN = 1;
6064
6011
  const WS_READY_STATE_CLOSED = 3;
@@ -6133,10 +6080,8 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6133
6080
  const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
6134
6081
  ? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
6135
6082
  : undefined;
6136
- const principalKey = (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress)
6137
- ? `w${overrides._walletAddress}`
6138
- : (authTokenProvider ? `o${principalFromIdToken(await authTokenProvider())}` : undefined);
6139
6083
  const identity = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6084
+ const principalKey = authTokenProvider ? identity : undefined;
6140
6085
  const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity);
6141
6086
  // Deliver cached data immediately if available
6142
6087
  const cachedEntry = responseCache.get(cacheKey);
@@ -6215,7 +6160,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6215
6160
  await subscriptionPromise;
6216
6161
  }
6217
6162
  catch (error) {
6218
- 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
+ }
6219
6175
  }
6220
6176
  }
6221
6177
  // Return unsubscribe function
@@ -6378,6 +6334,13 @@ async function doReconnectWithNewAuth() {
6378
6334
  catch (error) {
6379
6335
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6380
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
+ }
6381
6344
  for (const [appId, connection] of connections) {
6382
6345
  if (!connection.ws) {
6383
6346
  continue;
@@ -6845,6 +6808,16 @@ async function idbSet(key, value) {
6845
6808
  // RealtimeStore
6846
6809
  // ---------------------------------------------------------------------------
6847
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
+ }
6848
6821
  class RealtimeStore {
6849
6822
  constructor() {
6850
6823
  this.ws = null;
@@ -6860,7 +6833,9 @@ class RealtimeStore {
6860
6833
  this.idbDirtyKeys = new Set();
6861
6834
  this.closed = false;
6862
6835
  this.authToken = null;
6836
+ this.authPrincipalKey = 'anon';
6863
6837
  this.authenticating = false;
6838
+ this.suppressNextReconnect = false;
6864
6839
  this.isServer = false;
6865
6840
  this.tokenRefreshTimer = null;
6866
6841
  // -----------------------------------------------------------------------
@@ -6880,34 +6855,98 @@ class RealtimeStore {
6880
6855
  this.startTokenRefresh();
6881
6856
  }
6882
6857
  async refreshToken() {
6858
+ let token = null;
6883
6859
  try {
6884
6860
  const { getIdToken } = await Promise.resolve().then(function () { return utils; });
6885
- const token = await getIdToken(this.isServer);
6886
- if (token)
6887
- this.authToken = token;
6861
+ token = await getIdToken(this.isServer);
6888
6862
  }
6889
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);
6890
6866
  }
6891
6867
  startTokenRefresh() {
6892
6868
  if (this.tokenRefreshTimer)
6893
6869
  return;
6894
6870
  this.tokenRefreshTimer = setInterval(async () => {
6895
- var _a;
6896
- const prevToken = this.authToken;
6871
+ const prevPrincipal = this.authPrincipalKey;
6897
6872
  await this.refreshToken();
6898
- if (this.authToken && this.authToken !== prevToken && ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
6899
- // Token changed — reconnect with fresh token
6900
- 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
+ }
6901
6880
  }
6902
6881
  }, 5 * 60 * 1000); // Check every 5 minutes
6903
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
+ }
6904
6947
  async ensureConnected() {
6905
6948
  var _a;
6906
- if (!this.appId) {
6907
- if (!this.initPromise)
6908
- this.initPromise = this.init();
6909
- await this.initPromise;
6910
- }
6949
+ await this.ensureCurrentAuth();
6911
6950
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
6912
6951
  return;
6913
6952
  if (this.connectPromise)
@@ -6991,11 +7030,20 @@ class RealtimeStore {
6991
7030
  ws.addEventListener('close', () => {
6992
7031
  if (authTimer)
6993
7032
  clearTimeout(authTimer);
7033
+ if (this.ws !== ws) {
7034
+ if (this.suppressNextReconnect)
7035
+ this.suppressNextReconnect = false;
7036
+ return;
7037
+ }
6994
7038
  this.authenticating = false;
6995
7039
  this.ws = null;
6996
7040
  this.connectPromise = null;
6997
7041
  this.rejectAllPending('WebSocket closed');
6998
7042
  this.setAllSubscriptionStatus('reconnecting');
7043
+ if (this.suppressNextReconnect) {
7044
+ this.suppressNextReconnect = false;
7045
+ return;
7046
+ }
6999
7047
  this.scheduleReconnect();
7000
7048
  });
7001
7049
  });
@@ -7171,14 +7219,34 @@ class RealtimeStore {
7171
7219
  }
7172
7220
  }
7173
7221
  handleError(msg) {
7174
- 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;
7175
7228
  const requestId = msg.requestId;
7176
7229
  if (requestId) {
7177
7230
  const pending = this.pendingRequests.get(requestId);
7178
7231
  if (pending) {
7179
7232
  this.pendingRequests.delete(requestId);
7180
7233
  clearTimeout(pending.timeout);
7181
- 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
+ }
7182
7250
  }
7183
7251
  }
7184
7252
  }
@@ -7187,6 +7255,7 @@ class RealtimeStore {
7187
7255
  // -----------------------------------------------------------------------
7188
7256
  async subscribe(path, opts = {}) {
7189
7257
  var _a;
7258
+ await this.ensureCurrentAuth();
7190
7259
  const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
7191
7260
  const subKey = this.getSubKey(path, opts);
7192
7261
  let sub = this.subscriptions.get(subKey);
@@ -7196,6 +7265,8 @@ class RealtimeStore {
7196
7265
  sub.callbacks.add(opts.onData);
7197
7266
  if (opts.onState)
7198
7267
  sub.stateCallbacks.add(opts.onState);
7268
+ if (opts.onError)
7269
+ sub.errorCallbacks.add(opts.onError);
7199
7270
  // Immediately deliver current state
7200
7271
  if (opts.onData && sub.docs.size > 0) {
7201
7272
  opts.onData(this.docsToArray(sub));
@@ -7203,7 +7274,7 @@ class RealtimeStore {
7203
7274
  if (opts.onState) {
7204
7275
  opts.onState(this.getState(sub));
7205
7276
  }
7206
- return this.createUnsubscribe(subKey, opts.onData, opts.onState);
7277
+ return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7207
7278
  }
7208
7279
  // New subscription
7209
7280
  const subId = `sub_${nextRequestId++}`;
@@ -7218,6 +7289,7 @@ class RealtimeStore {
7218
7289
  error: null,
7219
7290
  callbacks: new Set(opts.onData ? [opts.onData] : []),
7220
7291
  stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
7292
+ errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
7221
7293
  ref: { current: new Map() },
7222
7294
  };
7223
7295
  this.subscriptions.set(subKey, sub);
@@ -7247,7 +7319,7 @@ class RealtimeStore {
7247
7319
  sub.error = new Error('Connection failed');
7248
7320
  this.notifyState(sub);
7249
7321
  }
7250
- return this.createUnsubscribe(subKey, opts.onData, opts.onState);
7322
+ return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7251
7323
  }
7252
7324
  getRef(path, opts = {}) {
7253
7325
  var _a;
@@ -7321,6 +7393,7 @@ class RealtimeStore {
7321
7393
  }
7322
7394
  }
7323
7395
  async get(path) {
7396
+ await this.ensureCurrentAuth();
7324
7397
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7325
7398
  // Check local subscriptions first
7326
7399
  const collectionPath = this.getCollectionPath(normalizedPath);
@@ -7501,7 +7574,7 @@ class RealtimeStore {
7501
7574
  return docPath;
7502
7575
  }
7503
7576
  getSubKey(path, opts) {
7504
- const parts = [path];
7577
+ const parts = [this.appId, this.authPrincipalKey, path];
7505
7578
  if (opts.filter)
7506
7579
  parts.push(JSON.stringify(opts.filter));
7507
7580
  if (opts.prompt)
@@ -7511,7 +7584,7 @@ class RealtimeStore {
7511
7584
  return parts.join('::');
7512
7585
  }
7513
7586
  idbKey(path) {
7514
- return `${this.appId}:${path}`;
7587
+ return `${this.appId}:${this.authPrincipalKey}:${path}`;
7515
7588
  }
7516
7589
  markIdbDirty(path) {
7517
7590
  const sub = this.findSubscriptionByPath(path);
@@ -7536,18 +7609,23 @@ class RealtimeStore {
7536
7609
  }
7537
7610
  }
7538
7611
  }
7539
- createUnsubscribe(subKey, onData, onState) {
7612
+ createUnsubscribe(subKey, subId, onData, onState, onError) {
7540
7613
  return async () => {
7541
- 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);
7542
7616
  if (!sub)
7543
7617
  return;
7618
+ const currentSubKey = this.getSubKey(sub.path, sub.options);
7544
7619
  if (onData)
7545
7620
  sub.callbacks.delete(onData);
7546
7621
  if (onState)
7547
7622
  sub.stateCallbacks.delete(onState);
7623
+ if (onError)
7624
+ sub.errorCallbacks.delete(onError);
7548
7625
  // If no more callbacks, unsubscribe entirely
7549
- if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0) {
7626
+ if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
7550
7627
  this.subscriptions.delete(subKey);
7628
+ this.subscriptions.delete(currentSubKey);
7551
7629
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
7552
7630
  this.ws.send(JSON.stringify({
7553
7631
  type: 'unsubscribe',
@@ -7619,6 +7697,22 @@ class RealtimeStore {
7619
7697
  this.rejectAllPending('Store closed');
7620
7698
  this.subscriptions.clear();
7621
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
+ }
7622
7716
  }
7623
7717
  // ---------------------------------------------------------------------------
7624
7718
  // Singleton instance
@@ -7636,6 +7730,19 @@ function resetRealtimeStore() {
7636
7730
  storeInstance = null;
7637
7731
  }
7638
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
+ });
7639
7746
 
7640
7747
  // ---------------------------------------------------------------------------
7641
7748
  // functions.ts -- Bounded Functions client (the imperative escape hatch).
@@ -7791,6 +7898,23 @@ function realtimeHttpBase(wsApiUrl) {
7791
7898
  // Strip trailing slash from the resulting origin+path.
7792
7899
  return url.toString().replace(/\/$/, '');
7793
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
+ }
7794
7918
  /**
7795
7919
  * Send a player intent to a running live room. Returns `{ ok: true }`.
7796
7920
  *
@@ -7805,7 +7929,7 @@ function realtimeHttpBase(wsApiUrl) {
7805
7929
  * transport error / timeout.
7806
7930
  */
7807
7931
  async function intent(roomPath, intent, opts = {}) {
7808
- var _a, _b, _c, _d, _e;
7932
+ var _a, _b, _c, _d, _e, _f, _g;
7809
7933
  if (!roomPath || typeof roomPath !== 'string') {
7810
7934
  throw new LiveIntentError('A room path is required');
7811
7935
  }
@@ -7826,37 +7950,48 @@ async function intent(roomPath, intent, opts = {}) {
7826
7950
  // (e.g. the first join before subscribeView's WS connects) — HTTP also throws
7827
7951
  // on a non-2xx, so errors are surfaced there too.
7828
7952
  const normalizedRoomPath = roomPath.replace(/\/$/, '');
7829
- if (opts.fireAndForget) {
7830
- if (wsIntent(config.appId, normalizedRoomPath, intent))
7831
- return { ok: true };
7832
- }
7833
- else {
7834
- const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
7835
- if (ack)
7836
- 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
+ }
7837
7964
  }
7838
7965
  const base = realtimeHttpBase(config.wsApiUrl);
7839
- // Attach the caller's session token automatically (same token as data calls).
7840
- const authHeader = await createAuthHeader(config.isServer);
7841
- 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
+ };
7842
7972
  const controller = new AbortController();
7843
- const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 60000;
7973
+ const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
7844
7974
  const timer = setTimeout(() => controller.abort(), timeoutMs);
7845
7975
  let res;
7846
7976
  try {
7847
- res = await fetch(`${base}/live/intent`, {
7977
+ const send = async () => fetch(`${base}/live/intent`, {
7848
7978
  method: 'POST',
7849
- headers,
7979
+ headers: await buildHeaders(),
7850
7980
  body: JSON.stringify({ path: normalizedRoomPath, intent }),
7851
7981
  signal: controller.signal,
7852
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
+ }
7853
7988
  }
7854
7989
  catch (err) {
7855
7990
  clearTimeout(timer);
7856
7991
  if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError') {
7857
7992
  throw new LiveIntentError(`Live intent to "${roomPath}" timed out after ${timeoutMs}ms`);
7858
7993
  }
7859
- 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)}`);
7860
7995
  }
7861
7996
  clearTimeout(timer);
7862
7997
  let body = null;
@@ -7865,12 +8000,12 @@ async function intent(roomPath, intent, opts = {}) {
7865
8000
  try {
7866
8001
  body = JSON.parse(text);
7867
8002
  }
7868
- catch (_f) {
8003
+ catch (_h) {
7869
8004
  body = { raw: text };
7870
8005
  }
7871
8006
  }
7872
8007
  if (!res.ok) {
7873
- 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}`;
7874
8009
  throw new LiveIntentError(message, res.status, body);
7875
8010
  }
7876
8011
  return (body !== null && body !== void 0 ? body : { ok: true });