@bounded-sh/core 0.0.15 → 0.0.17

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
@@ -4335,99 +4335,54 @@ function normalizeReadResult(responseData, pathIsDocument) {
4335
4335
  }
4336
4336
  return responseData;
4337
4337
  }
4338
- function hashForKey$1(value) {
4338
+ function hashForKey$2(value) {
4339
4339
  let h = 5381;
4340
4340
  for (let i = 0; i < value.length; i++) {
4341
4341
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
4342
4342
  }
4343
4343
  return h.toString(36);
4344
4344
  }
4345
- /**
4346
- * Derive a stable identity string for the JWT a request will actually present.
4347
- * Prefers the subject-ish claims (sub / custom:walletAddress / address) so the
4348
- * same principal across token refreshes maps to the same fingerprint; falls back
4349
- * to a hash of the raw token if the payload can't be decoded.
4350
- */
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
+ }
4351
4351
  function principalFromIdToken$1(idToken) {
4352
- if (!idToken)
4353
- return 'anon';
4354
- try {
4355
- const parts = idToken.split('.');
4356
- if (parts.length < 2)
4357
- return `t${hashForKey$1(idToken)}`;
4358
- const payload = JSON.parse(decodeBase64Url(parts[1]));
4359
- const subject =
4360
- // Prefer the universal identity (@user.id) — it is the stable principal a
4361
- // request presents (equals the wallet for wallet logins; the account id for
4362
- // email/social logins, which carry no walletAddress). Keying the read cache
4363
- // on it keeps an email user's private snapshot scoped to that user.
4364
- payload['custom:userId'] ||
4365
- payload['custom:walletAddress'] ||
4366
- payload.walletAddress ||
4367
- payload.sub ||
4368
- payload.address;
4369
- if (subject)
4370
- return `s${hashForKey$1(String(subject))}`;
4371
- return `t${hashForKey$1(idToken)}`;
4372
- }
4373
- catch (_a) {
4374
- return `t${hashForKey$1(idToken)}`;
4375
- }
4352
+ return idToken ? `t${hashForKey$2(idToken)}` : 'anon';
4376
4353
  }
4377
4354
  /**
4378
4355
  * SECURITY (H1): Read caches must be keyed by the caller's principal, not just
4379
4356
  * by path/filter/shape. In a shared process / SSR worker / browser login-switch,
4380
4357
  * keying by path alone lets User B receive User A's cached private read before
4381
4358
  * any server read rule runs. This returns `appId:<principal>` for the identity a
4382
- * given read will actually authenticate as:
4383
- * - a per-request `_walletAddress` override that override's wallet
4384
- * - a per-request `_getAuthHeaders` override a hash of the auth header it
4385
- * produces (so a cross-principal cached entry is never served to it)
4386
- * - 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.
4387
4363
  */
4388
4364
  async function getReadPrincipalKey(overrides) {
4389
- var _a, _b;
4390
4365
  const config = await getConfig();
4391
4366
  const appId = config.appId || '';
4392
- // Explicit per-request wallet override key by that identity.
4393
- if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
4394
- 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)}`;
4395
4372
  }
4396
- // Per-request auth-header override (wallet client). Key by the actual header
4397
- // 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.
4398
4375
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
4399
4376
  try {
4400
4377
  const headers = await overrides._getAuthHeaders();
4401
- const authValue = (headers === null || headers === void 0 ? void 0 : headers.Authorization) ||
4402
- (headers === null || headers === void 0 ? void 0 : headers.authorization) ||
4403
- '';
4404
- // Decode the bearer token's subject when present for a stable key across
4405
- // refreshes; otherwise hash whatever header material we were given.
4406
- const bearer = authValue.startsWith('Bearer ')
4407
- ? authValue.slice('Bearer '.length)
4408
- : authValue;
4409
- const principal = bearer ? principalFromIdToken$1(bearer) : 'anon';
4410
- return `${appId}:o${principal}`;
4411
- }
4412
- catch (_c) {
4378
+ return `${appId}:o${principalFromAuthValue(authValueFromHeaders(headers))}`;
4379
+ }
4380
+ catch (_a) {
4413
4381
  // If we can't resolve the override identity, use a unique-ish key so we
4414
4382
  // never collide with (and serve) another principal's cached entry.
4415
- return `${appId}:o${hashForKey$1(String(Date.now()) + Math.random())}`;
4383
+ return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
4416
4384
  }
4417
4385
  }
4418
- // Direct per-request header override. makeApiRequest applies overrides.headers
4419
- // AFTER its computed auth header (api.ts), so a caller-supplied Authorization
4420
- // is the REAL request auth — key the cache by it so an entry is never served
4421
- // to a different principal under the ambient key. (Hardening: no in-repo read
4422
- // caller passes this today, but the cache must never trail the actual auth.)
4423
- const directAuth = ((_a = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _a === void 0 ? void 0 : _a.Authorization) ||
4424
- ((_b = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _b === void 0 ? void 0 : _b.authorization);
4425
- if (directAuth) {
4426
- const bearer = directAuth.startsWith('Bearer ')
4427
- ? directAuth.slice('Bearer '.length)
4428
- : directAuth;
4429
- return `${appId}:h${bearer ? principalFromIdToken$1(bearer) : hashForKey$1(directAuth)}`;
4430
- }
4431
4386
  // Ambient session principal.
4432
4387
  const idToken = await getIdToken(config.isServer);
4433
4388
  return `${appId}:${principalFromIdToken$1(idToken)}`;
@@ -4727,9 +4682,9 @@ async function get(path, opts = {}) {
4727
4682
  const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
4728
4683
  const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
4729
4684
  const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
4730
- const cursorKey = opts.cursor ? `:c${hashForKey$1(opts.cursor)}` : '';
4731
- const filterKey = opts.filter ? `:f${hashForKey$1(JSON.stringify(opts.filter))}` : '';
4732
- 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))}` : '';
4733
4688
  const principalKey = await getReadPrincipalKey(opts._overrides);
4734
4689
  const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
4735
4690
  const now = Date.now();
@@ -5479,7 +5434,7 @@ let reconnectInProgress = null;
5479
5434
  function generateSubscriptionId() {
5480
5435
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
5481
5436
  }
5482
- function hashForKey(value) {
5437
+ function hashForKey$1(value) {
5483
5438
  let h = 5381;
5484
5439
  for (let i = 0; i < value.length; i++) {
5485
5440
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
@@ -5498,49 +5453,23 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5498
5453
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
5499
5454
  const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
5500
5455
  const limitKey = limit !== undefined ? `:l${limit}` : '';
5501
- const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
5502
- 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))}` : '';
5503
5458
  const identityKey = identity || 'anon';
5504
5459
  return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
5505
5460
  }
5506
5461
  /**
5507
- * Derive a stable identity string for the principal a subscription authenticates
5508
- * as: `<appId>:<principalFingerprint>`. Mirrors the operations.ts logic so HTTP
5509
- * 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.
5510
5465
  */
5511
5466
  function principalFromIdToken(idToken) {
5512
- if (!idToken)
5513
- return 'anon';
5514
- try {
5515
- const parts = idToken.split('.');
5516
- if (parts.length < 2)
5517
- return `t${hashForKey(idToken)}`;
5518
- const payload = JSON.parse(decodeBase64Url(parts[1]));
5519
- const subject =
5520
- // Universal identity (@user.id) first — the stable principal a request
5521
- // presents (the account id for email/social logins, which carry no
5522
- // walletAddress). Keeps the H1 response-cache scoping correct for them.
5523
- payload['custom:userId'] ||
5524
- payload['custom:walletAddress'] ||
5525
- payload.walletAddress ||
5526
- payload.sub ||
5527
- payload.address;
5528
- if (subject)
5529
- return `s${hashForKey(String(subject))}`;
5530
- return `t${hashForKey(idToken)}`;
5531
- }
5532
- catch (_a) {
5533
- return `t${hashForKey(idToken)}`;
5534
- }
5467
+ return idToken ? `t${hashForKey$1(idToken)}` : 'anon';
5535
5468
  }
5536
5469
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5537
- // Per-subscription wallet override (server WalletClient.subscribe): key by the
5538
- // wallet's own token, mirroring getReadPrincipalKey, so a wallet client never
5539
- // consults and never crashes on — the absent ambient env keypair, and its
5540
- // cached snapshots are never crossed with another principal's.
5541
- if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
5542
- return `${effectiveAppId}:w${overrides._walletAddress}`;
5543
- }
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.
5544
5473
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
5545
5474
  try {
5546
5475
  const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
@@ -5911,7 +5840,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5911
5840
  return connection;
5912
5841
  }
5913
5842
  function handleServerMessage(connection, message) {
5914
- var _a, _b;
5843
+ var _a;
5915
5844
  switch (message.type) {
5916
5845
  case 'authenticated': {
5917
5846
  connection.isAuthenticating = false;
@@ -6005,28 +5934,27 @@ function handleServerMessage(connection, message) {
6005
5934
  err.subscriptionId = message.subscriptionId;
6006
5935
  return err;
6007
5936
  };
5937
+ const wsError = buildWsError();
6008
5938
  // Handle CRUD request errors (requestId present)
6009
5939
  if (message.requestId) {
6010
5940
  const pendingReq = connection.pendingRequests.get(message.requestId);
6011
5941
  if (pendingReq) {
6012
5942
  connection.pendingRequests.delete(message.requestId);
6013
5943
  clearTimeout(pendingReq.timer);
6014
- pendingReq.reject(buildWsError());
5944
+ pendingReq.reject(wsError);
6015
5945
  }
6016
5946
  }
6017
5947
  if (message.subscriptionId) {
6018
5948
  // Reject pending subscription if this is a subscription error
6019
5949
  const pending = connection.pendingSubscriptions.get(message.subscriptionId);
6020
5950
  if (pending) {
6021
- pending.reject(buildWsError());
5951
+ pending.reject(wsError);
6022
5952
  connection.pendingSubscriptions.delete(message.subscriptionId);
6023
5953
  }
6024
5954
  // Notify error callbacks for this subscription
6025
5955
  const subscription = connection.subscriptions.get(message.subscriptionId);
6026
5956
  if (subscription) {
6027
- for (const callback of subscription.callbacks) {
6028
- (_b = callback.onError) === null || _b === void 0 ? void 0 : _b.call(callback, buildWsError());
6029
- }
5957
+ notifyErrorCallbacks(subscription, wsError);
6030
5958
  }
6031
5959
  }
6032
5960
  break;
@@ -6079,6 +6007,25 @@ function notifyCallbacks(subscription, data) {
6079
6007
  }
6080
6008
  }
6081
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
+ }
6082
6029
  // WebSocket readyState constants
6083
6030
  const WS_READY_STATE_OPEN = 1;
6084
6031
  const WS_READY_STATE_CLOSED = 3;
@@ -6153,10 +6100,8 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6153
6100
  const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
6154
6101
  ? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
6155
6102
  : undefined;
6156
- const principalKey = (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress)
6157
- ? `w${overrides._walletAddress}`
6158
- : (authTokenProvider ? `o${principalFromIdToken(await authTokenProvider())}` : undefined);
6159
6103
  const identity = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6104
+ const principalKey = authTokenProvider ? identity : undefined;
6160
6105
  const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity);
6161
6106
  // Deliver cached data immediately if available
6162
6107
  const cachedEntry = responseCache.get(cacheKey);
@@ -6235,7 +6180,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6235
6180
  await subscriptionPromise;
6236
6181
  }
6237
6182
  catch (error) {
6238
- 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
+ }
6239
6195
  }
6240
6196
  }
6241
6197
  // Return unsubscribe function
@@ -6398,6 +6354,13 @@ async function doReconnectWithNewAuth() {
6398
6354
  catch (error) {
6399
6355
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6400
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
+ }
6401
6364
  for (const [appId, connection] of connections) {
6402
6365
  if (!connection.ws) {
6403
6366
  continue;
@@ -6865,6 +6828,16 @@ async function idbSet(key, value) {
6865
6828
  // RealtimeStore
6866
6829
  // ---------------------------------------------------------------------------
6867
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
+ }
6868
6841
  class RealtimeStore {
6869
6842
  constructor() {
6870
6843
  this.ws = null;
@@ -6880,7 +6853,9 @@ class RealtimeStore {
6880
6853
  this.idbDirtyKeys = new Set();
6881
6854
  this.closed = false;
6882
6855
  this.authToken = null;
6856
+ this.authPrincipalKey = 'anon';
6883
6857
  this.authenticating = false;
6858
+ this.suppressNextReconnect = false;
6884
6859
  this.isServer = false;
6885
6860
  this.tokenRefreshTimer = null;
6886
6861
  // -----------------------------------------------------------------------
@@ -6900,34 +6875,98 @@ class RealtimeStore {
6900
6875
  this.startTokenRefresh();
6901
6876
  }
6902
6877
  async refreshToken() {
6878
+ let token = null;
6903
6879
  try {
6904
6880
  const { getIdToken } = await Promise.resolve().then(function () { return utils; });
6905
- const token = await getIdToken(this.isServer);
6906
- if (token)
6907
- this.authToken = token;
6881
+ token = await getIdToken(this.isServer);
6908
6882
  }
6909
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);
6910
6886
  }
6911
6887
  startTokenRefresh() {
6912
6888
  if (this.tokenRefreshTimer)
6913
6889
  return;
6914
6890
  this.tokenRefreshTimer = setInterval(async () => {
6915
- var _a;
6916
- const prevToken = this.authToken;
6891
+ const prevPrincipal = this.authPrincipalKey;
6917
6892
  await this.refreshToken();
6918
- if (this.authToken && this.authToken !== prevToken && ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
6919
- // Token changed — reconnect with fresh token
6920
- 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
+ }
6921
6900
  }
6922
6901
  }, 5 * 60 * 1000); // Check every 5 minutes
6923
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
+ }
6924
6967
  async ensureConnected() {
6925
6968
  var _a;
6926
- if (!this.appId) {
6927
- if (!this.initPromise)
6928
- this.initPromise = this.init();
6929
- await this.initPromise;
6930
- }
6969
+ await this.ensureCurrentAuth();
6931
6970
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
6932
6971
  return;
6933
6972
  if (this.connectPromise)
@@ -7011,11 +7050,20 @@ class RealtimeStore {
7011
7050
  ws.addEventListener('close', () => {
7012
7051
  if (authTimer)
7013
7052
  clearTimeout(authTimer);
7053
+ if (this.ws !== ws) {
7054
+ if (this.suppressNextReconnect)
7055
+ this.suppressNextReconnect = false;
7056
+ return;
7057
+ }
7014
7058
  this.authenticating = false;
7015
7059
  this.ws = null;
7016
7060
  this.connectPromise = null;
7017
7061
  this.rejectAllPending('WebSocket closed');
7018
7062
  this.setAllSubscriptionStatus('reconnecting');
7063
+ if (this.suppressNextReconnect) {
7064
+ this.suppressNextReconnect = false;
7065
+ return;
7066
+ }
7019
7067
  this.scheduleReconnect();
7020
7068
  });
7021
7069
  });
@@ -7191,14 +7239,34 @@ class RealtimeStore {
7191
7239
  }
7192
7240
  }
7193
7241
  handleError(msg) {
7194
- 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;
7195
7248
  const requestId = msg.requestId;
7196
7249
  if (requestId) {
7197
7250
  const pending = this.pendingRequests.get(requestId);
7198
7251
  if (pending) {
7199
7252
  this.pendingRequests.delete(requestId);
7200
7253
  clearTimeout(pending.timeout);
7201
- 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
+ }
7202
7270
  }
7203
7271
  }
7204
7272
  }
@@ -7207,6 +7275,7 @@ class RealtimeStore {
7207
7275
  // -----------------------------------------------------------------------
7208
7276
  async subscribe(path, opts = {}) {
7209
7277
  var _a;
7278
+ await this.ensureCurrentAuth();
7210
7279
  const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
7211
7280
  const subKey = this.getSubKey(path, opts);
7212
7281
  let sub = this.subscriptions.get(subKey);
@@ -7216,6 +7285,8 @@ class RealtimeStore {
7216
7285
  sub.callbacks.add(opts.onData);
7217
7286
  if (opts.onState)
7218
7287
  sub.stateCallbacks.add(opts.onState);
7288
+ if (opts.onError)
7289
+ sub.errorCallbacks.add(opts.onError);
7219
7290
  // Immediately deliver current state
7220
7291
  if (opts.onData && sub.docs.size > 0) {
7221
7292
  opts.onData(this.docsToArray(sub));
@@ -7223,7 +7294,7 @@ class RealtimeStore {
7223
7294
  if (opts.onState) {
7224
7295
  opts.onState(this.getState(sub));
7225
7296
  }
7226
- return this.createUnsubscribe(subKey, opts.onData, opts.onState);
7297
+ return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7227
7298
  }
7228
7299
  // New subscription
7229
7300
  const subId = `sub_${nextRequestId++}`;
@@ -7238,6 +7309,7 @@ class RealtimeStore {
7238
7309
  error: null,
7239
7310
  callbacks: new Set(opts.onData ? [opts.onData] : []),
7240
7311
  stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
7312
+ errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
7241
7313
  ref: { current: new Map() },
7242
7314
  };
7243
7315
  this.subscriptions.set(subKey, sub);
@@ -7267,7 +7339,7 @@ class RealtimeStore {
7267
7339
  sub.error = new Error('Connection failed');
7268
7340
  this.notifyState(sub);
7269
7341
  }
7270
- return this.createUnsubscribe(subKey, opts.onData, opts.onState);
7342
+ return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7271
7343
  }
7272
7344
  getRef(path, opts = {}) {
7273
7345
  var _a;
@@ -7341,6 +7413,7 @@ class RealtimeStore {
7341
7413
  }
7342
7414
  }
7343
7415
  async get(path) {
7416
+ await this.ensureCurrentAuth();
7344
7417
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7345
7418
  // Check local subscriptions first
7346
7419
  const collectionPath = this.getCollectionPath(normalizedPath);
@@ -7521,7 +7594,7 @@ class RealtimeStore {
7521
7594
  return docPath;
7522
7595
  }
7523
7596
  getSubKey(path, opts) {
7524
- const parts = [path];
7597
+ const parts = [this.appId, this.authPrincipalKey, path];
7525
7598
  if (opts.filter)
7526
7599
  parts.push(JSON.stringify(opts.filter));
7527
7600
  if (opts.prompt)
@@ -7531,7 +7604,7 @@ class RealtimeStore {
7531
7604
  return parts.join('::');
7532
7605
  }
7533
7606
  idbKey(path) {
7534
- return `${this.appId}:${path}`;
7607
+ return `${this.appId}:${this.authPrincipalKey}:${path}`;
7535
7608
  }
7536
7609
  markIdbDirty(path) {
7537
7610
  const sub = this.findSubscriptionByPath(path);
@@ -7556,18 +7629,23 @@ class RealtimeStore {
7556
7629
  }
7557
7630
  }
7558
7631
  }
7559
- createUnsubscribe(subKey, onData, onState) {
7632
+ createUnsubscribe(subKey, subId, onData, onState, onError) {
7560
7633
  return async () => {
7561
- 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);
7562
7636
  if (!sub)
7563
7637
  return;
7638
+ const currentSubKey = this.getSubKey(sub.path, sub.options);
7564
7639
  if (onData)
7565
7640
  sub.callbacks.delete(onData);
7566
7641
  if (onState)
7567
7642
  sub.stateCallbacks.delete(onState);
7643
+ if (onError)
7644
+ sub.errorCallbacks.delete(onError);
7568
7645
  // If no more callbacks, unsubscribe entirely
7569
- if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0) {
7646
+ if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
7570
7647
  this.subscriptions.delete(subKey);
7648
+ this.subscriptions.delete(currentSubKey);
7571
7649
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
7572
7650
  this.ws.send(JSON.stringify({
7573
7651
  type: 'unsubscribe',
@@ -7639,6 +7717,22 @@ class RealtimeStore {
7639
7717
  this.rejectAllPending('Store closed');
7640
7718
  this.subscriptions.clear();
7641
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
+ }
7642
7736
  }
7643
7737
  // ---------------------------------------------------------------------------
7644
7738
  // Singleton instance
@@ -7656,6 +7750,19 @@ function resetRealtimeStore() {
7656
7750
  storeInstance = null;
7657
7751
  }
7658
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
+ });
7659
7766
 
7660
7767
  // ---------------------------------------------------------------------------
7661
7768
  // functions.ts -- Bounded Functions client (the imperative escape hatch).
@@ -7811,6 +7918,23 @@ function realtimeHttpBase(wsApiUrl) {
7811
7918
  // Strip trailing slash from the resulting origin+path.
7812
7919
  return url.toString().replace(/\/$/, '');
7813
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
+ }
7814
7938
  /**
7815
7939
  * Send a player intent to a running live room. Returns `{ ok: true }`.
7816
7940
  *
@@ -7825,7 +7949,7 @@ function realtimeHttpBase(wsApiUrl) {
7825
7949
  * transport error / timeout.
7826
7950
  */
7827
7951
  async function intent(roomPath, intent, opts = {}) {
7828
- var _a, _b, _c, _d, _e;
7952
+ var _a, _b, _c, _d, _e, _f, _g;
7829
7953
  if (!roomPath || typeof roomPath !== 'string') {
7830
7954
  throw new LiveIntentError('A room path is required');
7831
7955
  }
@@ -7846,37 +7970,48 @@ async function intent(roomPath, intent, opts = {}) {
7846
7970
  // (e.g. the first join before subscribeView's WS connects) — HTTP also throws
7847
7971
  // on a non-2xx, so errors are surfaced there too.
7848
7972
  const normalizedRoomPath = roomPath.replace(/\/$/, '');
7849
- if (opts.fireAndForget) {
7850
- if (wsIntent(config.appId, normalizedRoomPath, intent))
7851
- return { ok: true };
7852
- }
7853
- else {
7854
- const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
7855
- if (ack)
7856
- 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
+ }
7857
7984
  }
7858
7985
  const base = realtimeHttpBase(config.wsApiUrl);
7859
- // Attach the caller's session token automatically (same token as data calls).
7860
- const authHeader = await createAuthHeader(config.isServer);
7861
- 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
+ };
7862
7992
  const controller = new AbortController();
7863
- const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 60000;
7993
+ const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
7864
7994
  const timer = setTimeout(() => controller.abort(), timeoutMs);
7865
7995
  let res;
7866
7996
  try {
7867
- res = await fetch(`${base}/live/intent`, {
7997
+ const send = async () => fetch(`${base}/live/intent`, {
7868
7998
  method: 'POST',
7869
- headers,
7999
+ headers: await buildHeaders(),
7870
8000
  body: JSON.stringify({ path: normalizedRoomPath, intent }),
7871
8001
  signal: controller.signal,
7872
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
+ }
7873
8008
  }
7874
8009
  catch (err) {
7875
8010
  clearTimeout(timer);
7876
8011
  if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError') {
7877
8012
  throw new LiveIntentError(`Live intent to "${roomPath}" timed out after ${timeoutMs}ms`);
7878
8013
  }
7879
- 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)}`);
7880
8015
  }
7881
8016
  clearTimeout(timer);
7882
8017
  let body = null;
@@ -7885,12 +8020,12 @@ async function intent(roomPath, intent, opts = {}) {
7885
8020
  try {
7886
8021
  body = JSON.parse(text);
7887
8022
  }
7888
- catch (_f) {
8023
+ catch (_h) {
7889
8024
  body = { raw: text };
7890
8025
  }
7891
8026
  }
7892
8027
  if (!res.ok) {
7893
- 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}`;
7894
8029
  throw new LiveIntentError(message, res.status, body);
7895
8030
  }
7896
8031
  return (body !== null && body !== void 0 ? body : { ok: true });