@bounded-sh/core 0.0.98 → 0.0.99

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.
Files changed (27) hide show
  1. package/dist/client/config.d.ts +1 -0
  2. package/dist/{index-D-dJjY-Q.js → index-B8PpRj6x.js} +159 -108
  3. package/dist/index-B8PpRj6x.js.map +1 -0
  4. package/dist/{index-BDW82Uxu.mjs → index-DW5yMQwV.mjs} +159 -108
  5. package/dist/index-DW5yMQwV.mjs.map +1 -0
  6. package/dist/index.js +1 -1
  7. package/dist/index.mjs +1 -1
  8. package/dist/{realtime-store-DTS5OLm2.mjs → realtime-store-CeDT7vJF.mjs} +2 -2
  9. package/dist/{realtime-store-DTS5OLm2.mjs.map → realtime-store-CeDT7vJF.mjs.map} +1 -1
  10. package/dist/{realtime-store-Dytq73zO.js → realtime-store-D7mJ7rTj.js} +2 -2
  11. package/dist/{realtime-store-Dytq73zO.js.map → realtime-store-D7mJ7rTj.js.map} +1 -1
  12. package/dist/{solana-message-j1s9osZD.mjs → solana-message-BGkYFU3C.mjs} +2 -2
  13. package/dist/{solana-message-j1s9osZD.mjs.map → solana-message-BGkYFU3C.mjs.map} +1 -1
  14. package/dist/{solana-message-Cww3s9HW.js → solana-message-w2FxOkFA.js} +2 -2
  15. package/dist/{solana-message-Cww3s9HW.js.map → solana-message-w2FxOkFA.js.map} +1 -1
  16. package/dist/{solana-operations-da26Ject.mjs → solana-operations-CHgW7Pue.mjs} +4 -4
  17. package/dist/{solana-operations-da26Ject.mjs.map → solana-operations-CHgW7Pue.mjs.map} +1 -1
  18. package/dist/{solana-operations-SR1En23U.js → solana-operations-DJQDYdrk.js} +3 -3
  19. package/dist/{solana-operations-SR1En23U.js.map → solana-operations-DJQDYdrk.js.map} +1 -1
  20. package/dist/{solana-v1-operations-DRm8TzzJ.js → solana-v1-operations-BF2rjG23.js} +3 -3
  21. package/dist/{solana-v1-operations-DRm8TzzJ.js.map → solana-v1-operations-BF2rjG23.js.map} +1 -1
  22. package/dist/{solana-v1-operations-BQQfm-ni.mjs → solana-v1-operations-BPn-poee.mjs} +3 -3
  23. package/dist/{solana-v1-operations-BQQfm-ni.mjs.map → solana-v1-operations-BPn-poee.mjs.map} +1 -1
  24. package/dist/utils/web-session-manager.d.ts +7 -2
  25. package/package.json +8 -2
  26. package/dist/index-BDW82Uxu.mjs.map +0 -1
  27. package/dist/index-D-dJjY-Q.js.map +0 -1
@@ -87,6 +87,9 @@ function getWebhookKeysUrl() {
87
87
  * `verifyWebhook` to bind a webhook to THIS app by default (#072) rather than trusting any
88
88
  * app's shared-key-signed delivery.
89
89
  */
90
+ function getConfiguredSessionNamespace() {
91
+ return JSON.stringify([(clientConfig.humanAuthApiUrl || "https://auth.bounded.sh").replace(/\/$/, ""), clientConfig.appId]);
92
+ }
90
93
  function getConfiguredAppId() {
91
94
  return clientConfig.appId || undefined;
92
95
  }
@@ -925,6 +928,30 @@ function decideCommit(current, expected, next) {
925
928
  }
926
929
 
927
930
  class WebSessionManager {
931
+ static scopedStorageKey() {
932
+ return 'bounded_session_storage:v2:' + getConfiguredSessionNamespace();
933
+ }
934
+ static get BOUNDED_SESSION_STORAGE_KEY() {
935
+ const scoped = this.scopedStorageKey();
936
+ return localStorage.getItem(scoped + ':scope') === 'app' || localStorage.getItem(scoped) !== null
937
+ ? scoped : 'bounded_session_storage';
938
+ }
939
+ static logoutKey() { return this.scopedStorageKey() + ':logout'; }
940
+ /** Capture before login's network work; checked under the session mutation lock. */
941
+ static captureSessionEstablishment() {
942
+ var _a, _b;
943
+ const namespace = getConfiguredSessionNamespace();
944
+ const epoch = this.establishmentEpoch;
945
+ const generation = (_b = (_a = this.readRaw()) === null || _a === void 0 ? void 0 : _a.sessionGeneration) !== null && _b !== void 0 ? _b : null;
946
+ const logout = localStorage.getItem(this.logoutKey());
947
+ return () => {
948
+ var _a, _b;
949
+ if (namespace !== getConfiguredSessionNamespace() || epoch !== this.establishmentEpoch
950
+ || generation !== ((_b = (_a = this.readRaw()) === null || _a === void 0 ? void 0 : _a.sessionGeneration) !== null && _b !== void 0 ? _b : null)
951
+ || logout !== localStorage.getItem(this.logoutKey()))
952
+ throw new Error('session_establishment_superseded');
953
+ };
954
+ }
928
955
  /** Raw read of the stored blob. Callers inside the lock use this, never a cached copy. */
929
956
  static readRaw() {
930
957
  if (typeof window === "undefined")
@@ -984,10 +1011,11 @@ class WebSessionManager {
984
1011
  /* ------------------------------------------------------------------ */
985
1012
  /* STORE */
986
1013
  /* ------------------------------------------------------------------ */
987
- static async storeSession(address, accessToken, idToken, refreshToken, issuer) {
1014
+ static async storeSession(address, accessToken, idToken, refreshToken, issuer, beforeCommit) {
988
1015
  var _a;
989
1016
  if (typeof window === "undefined")
990
1017
  return;
1018
+ let appScoped = false;
991
1019
  // JWT-wallet binding: refuse to store a session whose idToken is bound
992
1020
  // to a different wallet than `address`. Prevents races that would otherwise
993
1021
  // leave localStorage with mismatched address/token state.
@@ -995,6 +1023,7 @@ class WebSessionManager {
995
1023
  const payloadB64 = idToken.split(".")[1];
996
1024
  if (payloadB64) {
997
1025
  const payload = JSON.parse(this.decodeBase64Url(payloadB64));
1026
+ appScoped = payload["custom:walletScope"] === "app";
998
1027
  const tokenWallet = payload["custom:walletAddress"];
999
1028
  if (tokenWallet && tokenWallet !== address) {
1000
1029
  throw new Error(`[WebSessionManager] Refusing to store session: address (${address}) does not match idToken custom:walletAddress (${tokenWallet})`);
@@ -1012,6 +1041,9 @@ class WebSessionManager {
1012
1041
  }
1013
1042
  const config = await getConfig();
1014
1043
  const currentAppId = config.appId;
1044
+ const namespace = getConfiguredSessionNamespace();
1045
+ const storageKey = appScoped || localStorage.getItem(this.scopedStorageKey() + ":scope") === "app"
1046
+ ? this.scopedStorageKey() : "bounded_session_storage";
1015
1047
  // The auth issuer base that minted this session, so silent refresh POSTs to
1016
1048
  // the RIGHT /session/refresh. Email overrides with humanAuthApiUrl; all
1017
1049
  // wallet/guest/signature flows default to authApiUrl (the bounded-auth issuer).
@@ -1019,12 +1051,33 @@ class WebSessionManager {
1019
1051
  // A login always starts a NEW generation. Refresh commits preserve it, so
1020
1052
  // "same generation" means "same login", which is what lets a post-refresh
1021
1053
  // retry be told apart from a re-login by the same wallet.
1054
+ const generation = mintSessionGeneration();
1022
1055
  await withSessionLock(() => {
1023
- this.writeRaw(Object.assign({ address,
1056
+ beforeCommit === null || beforeCommit === void 0 ? void 0 : beforeCommit();
1057
+ if (namespace !== getConfiguredSessionNamespace())
1058
+ throw new Error("session_establishment_superseded");
1059
+ ++this.establishmentEpoch;
1060
+ if (appScoped)
1061
+ localStorage.setItem(this.scopedStorageKey() + ":scope", "app");
1062
+ localStorage.setItem(storageKey, JSON.stringify(Object.assign({ address,
1024
1063
  accessToken,
1025
1064
  idToken,
1026
- refreshToken, appId: currentAppId, sessionGeneration: mintSessionGeneration() }, (sessionIssuer ? { issuer: sessionIssuer } : {})));
1065
+ refreshToken, appId: currentAppId, sessionGeneration: generation }, (sessionIssuer ? { issuer: sessionIssuer } : {}))));
1027
1066
  });
1067
+ // Adoption can fail after persistence (for example, logout while the
1068
+ // provider is loading). Remove only this incomplete login, even if the
1069
+ // caller has since switched app configuration or another tab signed in.
1070
+ return async () => {
1071
+ const removed = await withSessionLock(() => {
1072
+ const raw = localStorage.getItem(storageKey);
1073
+ if (!raw || JSON.parse(raw).sessionGeneration !== generation)
1074
+ return false;
1075
+ localStorage.removeItem(storageKey);
1076
+ return true;
1077
+ });
1078
+ if (removed)
1079
+ notifySessionCleared();
1080
+ };
1028
1081
  }
1029
1082
  /* ------------------------------------------------------------------ */
1030
1083
  /* GET */
@@ -1139,10 +1192,14 @@ class WebSessionManager {
1139
1192
  static async clearSession() {
1140
1193
  if (typeof window === "undefined")
1141
1194
  return;
1195
+ ++this.establishmentEpoch;
1196
+ const key = this.BOUNDED_SESSION_STORAGE_KEY;
1197
+ const logoutKey = this.logoutKey();
1142
1198
  const removed = await withSessionLock(() => {
1143
- if (localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY) === null)
1199
+ localStorage.setItem(logoutKey, mintSessionGeneration());
1200
+ if (localStorage.getItem(key) === null)
1144
1201
  return false;
1145
- localStorage.removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
1202
+ localStorage.removeItem(key);
1146
1203
  return true;
1147
1204
  });
1148
1205
  // Only announce a clear that actually happened, so a no-op cannot sign out
@@ -1289,7 +1346,7 @@ class WebSessionManager {
1289
1346
  return { outcome: result.outcome, session: result.session };
1290
1347
  }
1291
1348
  }
1292
- WebSessionManager.BOUNDED_SESSION_STORAGE_KEY = "bounded_session_storage";
1349
+ WebSessionManager.establishmentEpoch = 0;
1293
1350
 
1294
1351
  let _config = null;
1295
1352
  class ReactNativeSessionManager {
@@ -4131,7 +4188,7 @@ class ServerSessionManager {
4131
4188
  const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
4132
4189
  /* fetch nonce from auth API (bound to the snapshotted app+issuer) */
4133
4190
  const nonce = await genAuthNonce(sessionConfig);
4134
- const { genSolanaMessage } = await import('./solana-message-j1s9osZD.mjs');
4191
+ const { genSolanaMessage } = await import('./solana-message-BGkYFU3C.mjs');
4135
4192
  const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
4136
4193
  /* sign the message */
4137
4194
  const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
@@ -5239,7 +5296,6 @@ const WS_V2_PATH = '/ws/v2';
5239
5296
  let browserReconnectHooksAttached = false;
5240
5297
  let lastBrowserTriggeredReconnectAt = 0;
5241
5298
  let reconnectInProgress = null;
5242
- let ambientAuthFailure = null;
5243
5299
  // ============ Helper Functions ============
5244
5300
  function generateSubscriptionId() {
5245
5301
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
@@ -5367,34 +5423,6 @@ function getTokenExpirationTime(token) {
5367
5423
  return null;
5368
5424
  }
5369
5425
  }
5370
- /**
5371
- * Latch a failure so a dead session does not re-hammer /session/refresh every tick.
5372
- *
5373
- * NOT latched when the failure came with the session being cleared or replaced.
5374
- * The clear notifies auth-state listeners, which kicks a reconnect that resets this
5375
- * — and if we re-latched afterwards, `getConnectionAuthToken` would keep throwing
5376
- * even though storage is now empty, so an ordinary expiry left WebSocket
5377
- * acquisition permanently broken instead of falling back to an anonymous socket.
5378
- */
5379
- function rememberAmbientAuthFailure(error) {
5380
- if ((error === null || error === void 0 ? void 0 : error.code) === AUTH_CHANGED_CODE)
5381
- return error;
5382
- // The session is gone: the right next move is an anonymous connection, not a
5383
- // remembered failure.
5384
- if (!getIdTokenSync())
5385
- return error;
5386
- ambientAuthFailure = error;
5387
- return error;
5388
- }
5389
- /** Best-effort synchronous peek, only for deciding whether a session still exists. */
5390
- function getIdTokenSync() {
5391
- try {
5392
- return getActiveSessionManager().getIdToken();
5393
- }
5394
- catch (_a) {
5395
- return null;
5396
- }
5397
- }
5398
5426
  function failConnectionAuth(connection, error) {
5399
5427
  connection.authFailure = error;
5400
5428
  connection.pendingAuthToken = null;
@@ -5449,8 +5477,8 @@ function scheduleTokenRefresh(connection, isServer) {
5449
5477
  console.info('[WS v2] Token refreshed successfully, stored for next connection');
5450
5478
  }
5451
5479
  catch (refreshError) {
5452
- const definitive = (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_EXPIRED_CODE
5453
- || (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_CHANGED_CODE;
5480
+ const definitive = (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_CHANGED_CODE
5481
+ || !await getIdToken(isServer);
5454
5482
  if (definitive) {
5455
5483
  // The session is dead or belongs to someone else now. The shared
5456
5484
  // helper has already cleared storage and notified auth-state
@@ -5480,28 +5508,19 @@ function scheduleTokenRefresh(connection, isServer) {
5480
5508
  * WS-local wrapper around the shared freshness helper.
5481
5509
  *
5482
5510
  * The shared helper owns expiry classification, the deduplicated
5483
- * snapshot -> refresh -> commit transaction, and conditional clearing. This adds
5484
- * only the WS-specific storm guard: without it, a dead session would retry the
5485
- * refresh on every reconnect attempt and every 5-minute tick.
5511
+ * snapshot -> refresh -> commit transaction, and conditional clearing. A transient
5512
+ * failure must remain retryable; reconnect backoff bounds automatic attempts.
5486
5513
  */
5487
5514
  async function getWsAuthToken(isServer) {
5488
5515
  var _a, _b;
5489
- // A remembered failure short-circuits, but only while a session still exists.
5490
- // See rememberAmbientAuthFailure: once storage is cleared the correct next move
5491
- // is an anonymous connection, not a replayed error.
5492
- if (ambientAuthFailure && getIdTokenSync()) {
5493
- throw ambientAuthFailure;
5494
- }
5495
5516
  try {
5496
- const token = await getFreshAuthToken(isServer);
5497
- ambientAuthFailure = null;
5498
- return token;
5517
+ return await getFreshAuthToken(isServer);
5499
5518
  }
5500
5519
  catch (error) {
5501
5520
  // Status only — the raw axios error carries config.data (the refresh token)
5502
5521
  // and must never reach a console or log sink.
5503
5522
  console.error('[WS v2] Error acquiring auth token. status:', (_b = (_a = error === null || error === void 0 ? void 0 : error.status) !== null && _a !== void 0 ? _a : error === null || error === void 0 ? void 0 : error.code) !== null && _b !== void 0 ? _b : 'unknown');
5504
- throw rememberAmbientAuthFailure(normalizeAuthExpiredError(error, 'Authentication expired and refresh failed'));
5523
+ throw normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5505
5524
  }
5506
5525
  }
5507
5526
  /**
@@ -5519,18 +5538,21 @@ async function recoverFromServerAuthRejection(connection) {
5519
5538
  const rejected = connection.pendingAuthToken;
5520
5539
  if (!rejected)
5521
5540
  return;
5522
- // One forced refresh per distinct rejected token value: a transiently
5523
- // unreachable issuer must not turn the reconnect loop into a refresh storm.
5541
+ // Share an in-flight attempt and stop retrying a definitively refused token.
5542
+ // Transient failures may retry on the next socket reconnect, with its backoff.
5524
5543
  if (connection.serverRejectedAuthToken === rejected)
5525
5544
  return;
5526
5545
  connection.serverRejectedAuthToken = rejected;
5527
5546
  try {
5528
5547
  await getFreshAuthToken(connection.isServer, { force: true });
5529
5548
  }
5530
- catch (_a) {
5531
- // Every failure mode is already handled inside the helper: a dead
5532
- // session was cleared (auth listeners notified), a transient refresh
5533
- // failure keeps the session and the latch above stops the storm.
5549
+ catch (error) {
5550
+ if ((error === null || error === void 0 ? void 0 : error.code) !== AUTH_CHANGED_CODE && (error === null || error === void 0 ? void 0 : error.status) !== 401 && (error === null || error === void 0 ? void 0 : error.status) !== 403
5551
+ && connection.serverRejectedAuthToken === rejected) {
5552
+ connection.serverRejectedAuthToken = null;
5553
+ }
5554
+ // The shared helper clears definitively rejected sessions and preserves
5555
+ // transient failures. Never reuse the failed result as a credential.
5534
5556
  }
5535
5557
  }
5536
5558
  async function getConnectionAuthToken(isServer, authTokenProvider, anonymous) {
@@ -5541,9 +5563,6 @@ async function getConnectionAuthToken(isServer, authTokenProvider, anonymous) {
5541
5563
  return null;
5542
5564
  }
5543
5565
  if (!authTokenProvider) {
5544
- // getWsAuthToken, not the shared helper directly: the wrapper carries the
5545
- // reconnect-storm guard that keeps a dead session from re-refreshing on
5546
- // every reconnect attempt.
5547
5566
  return getWsAuthToken(isServer);
5548
5567
  }
5549
5568
  try {
@@ -5648,11 +5667,13 @@ function cancelIdleConnectionClose(connection) {
5648
5667
  connection.idleCloseTimer = null;
5649
5668
  }
5650
5669
  function closeConnectionNow(connection) {
5670
+ var _a;
5651
5671
  if (connections.get(connection.key) !== connection)
5652
5672
  return;
5653
5673
  if (!connection.ws)
5654
5674
  return;
5655
5675
  cancelIdleConnectionClose(connection);
5676
+ (_a = connection.authRetryWake) === null || _a === void 0 ? void 0 : _a.call(connection);
5656
5677
  if (connection.tokenRefreshTimer) {
5657
5678
  clearInterval(connection.tokenRefreshTimer);
5658
5679
  connection.tokenRefreshTimer = null;
@@ -5673,7 +5694,7 @@ function closeConnectionNow(connection) {
5673
5694
  try {
5674
5695
  ws.close();
5675
5696
  }
5676
- catch ( /* already closing */_a) { /* already closing */ }
5697
+ catch ( /* already closing */_b) { /* already closing */ }
5677
5698
  }
5678
5699
  function scheduleConnectionCloseIfIdle(connection) {
5679
5700
  var _a;
@@ -5759,6 +5780,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5759
5780
  }
5760
5781
  }
5761
5782
  async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey, source) {
5783
+ var _a, _b;
5762
5784
  attachBrowserReconnectHooksOnce();
5763
5785
  const anonymous = (source === null || source === void 0 ? void 0 : source.anonymous) === true;
5764
5786
  // A per-room subscription gets its OWN connection routed to the room DO; all
@@ -5805,9 +5827,6 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
5805
5827
  // lease also guards this window, but cancelling here avoids a needless
5806
5828
  // grace-timer round-trip on the common reuse path.
5807
5829
  cancelIdleConnectionClose(connection);
5808
- if (connection.authFailure) {
5809
- throw connection.authFailure;
5810
- }
5811
5830
  let currentAuthToken;
5812
5831
  try {
5813
5832
  currentAuthToken = await getConnectionAuthToken(isServer, authTokenProvider, anonymous);
@@ -5817,6 +5836,14 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
5817
5836
  failConnectionAuth(connection, authError);
5818
5837
  throw authError;
5819
5838
  }
5839
+ if (connection.authFailure) {
5840
+ // The preflight recovered, but failConnectionAuth invalidated this
5841
+ // socket's auth state. Authenticate again before replaying any reads.
5842
+ connection.authFailure = null;
5843
+ (_a = connection.authRetryWake) === null || _a === void 0 ? void 0 : _a.call(connection);
5844
+ connection.ws.reconnect();
5845
+ return connection;
5846
+ }
5820
5847
  if (connection.isConnected &&
5821
5848
  !connection.isAuthenticating &&
5822
5849
  !connectionMatchesAuthToken(connection, currentAuthToken)) {
@@ -5859,50 +5886,59 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
5859
5886
  isServer,
5860
5887
  serverRejectedAuthToken: null,
5861
5888
  };
5889
+ // Resolve configuration before handing control to reconnecting-websocket.
5890
+ // Its 4.4.0 URL-provider promise has no rejection handler and a rejected
5891
+ // provider permanently holds _connectLock. Credential failures therefore
5892
+ // stay inside this bounded, cancellable acquisition loop.
5893
+ const wsApiUrl = (_b = source === null || source === void 0 ? void 0 : source.wsApiUrl) !== null && _b !== void 0 ? _b : (await getConfig()).wsApiUrl;
5894
+ const wsUrl = new URL(wsApiUrl);
5895
+ wsUrl.pathname = WS_V2_PATH;
5896
+ wsUrl.searchParams.append('appId', appId || (await getConfig()).appId);
5897
+ if (connection.routePath)
5898
+ wsUrl.searchParams.append('routePath', connection.routePath);
5862
5899
  connections.set(connKey, connection);
5863
- // URL provider for reconnection with fresh tokens
5864
5900
  const urlProvider = async () => {
5865
- var _a;
5866
- // A scoped createClient() connection targets its own realtime endpoint;
5867
- // the default path reads the module-global config.
5868
- const wsApiUrl = (_a = source === null || source === void 0 ? void 0 : source.wsApiUrl) !== null && _a !== void 0 ? _a : (await getConfig()).wsApiUrl;
5869
- const wsUrl = new URL(wsApiUrl);
5870
- // Always use v2 path
5871
- wsUrl.pathname = WS_V2_PATH;
5872
- // Set appId from the configured SDK source, or an explicit per-call
5873
- // override passed through subscribeV2. Do not consult ambient globals:
5874
- // app identity must come from init({ appId }) or an explicit call option.
5875
- // `appId` here is already the resolved effectiveAppId; fall back to the
5876
- // global only on the default path when it was somehow empty.
5877
- wsUrl.searchParams.append('appId', appId || (await getConfig()).appId);
5878
- // Per-room connection: carry the room path so the worker routes this WS
5879
- // to the room DO (appId#room#roomId) where the live view fan-out lives.
5880
- if (connection.routePath) {
5881
- wsUrl.searchParams.append('routePath', connection.routePath);
5882
- }
5883
- // Resolve auth token if available. A wallet-scoped connection resolves
5884
- // its token from the wallet's own session (self-refreshing); all others
5885
- // use the browser session or fail closed for top-level server calls. The token is sent as the first WS
5886
- // frame after open, never as a URL query parameter.
5887
- let authToken;
5888
- try {
5889
- authToken = initialAuthToken !== undefined
5890
- ? initialAuthToken
5891
- : await getConnectionAuthToken(isServer, connection.authTokenProvider, connection.anonymous);
5892
- initialAuthToken = undefined;
5893
- }
5894
- catch (error) {
5895
- const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5896
- failConnectionAuth(connection, authError);
5897
- throw authError;
5898
- }
5899
- connection.pendingAuthToken = authToken || null;
5900
- connection.pendingAuthSnapshot = authSnapshotFromToken(authToken);
5901
- if (authToken) {
5902
- // Successful token acquisition — reset failure counter
5901
+ let retryDelay = WS_CONFIG.minReconnectionDelay;
5902
+ while (connections.get(connKey) === connection) {
5903
+ let authToken;
5904
+ try {
5905
+ authToken = initialAuthToken !== undefined
5906
+ ? initialAuthToken
5907
+ : await getConnectionAuthToken(isServer, connection.authTokenProvider, connection.anonymous);
5908
+ initialAuthToken = undefined;
5909
+ }
5910
+ catch (error) {
5911
+ if (connections.get(connKey) !== connection)
5912
+ break;
5913
+ const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5914
+ failConnectionAuth(connection, authError);
5915
+ // An error callback may synchronously tear down or replace this
5916
+ // connection. Do not install a retry after its cleanup completed.
5917
+ if (connections.get(connKey) !== connection)
5918
+ break;
5919
+ await new Promise((resolve) => {
5920
+ const wake = () => {
5921
+ clearTimeout(timer);
5922
+ if (connection.authRetryWake === wake)
5923
+ connection.authRetryWake = undefined;
5924
+ resolve();
5925
+ };
5926
+ const timer = setTimeout(wake, retryDelay);
5927
+ connection.authRetryWake = wake;
5928
+ });
5929
+ retryDelay = Math.min(MAX_RECONNECT_DELAY_MS, retryDelay * RECONNECT_DELAY_GROW_FACTOR);
5930
+ continue;
5931
+ }
5932
+ if (connections.get(connKey) !== connection)
5933
+ break;
5934
+ connection.pendingAuthToken = authToken || null;
5935
+ connection.pendingAuthSnapshot = authSnapshotFromToken(authToken);
5903
5936
  connection.consecutiveAuthFailures = 0;
5904
5937
  connection.authFailure = null;
5938
+ return wsUrl.toString();
5905
5939
  }
5940
+ // Teardown has called close() on the wrapper, which prevents creation
5941
+ // of a native socket when this abandoned provider resolves.
5906
5942
  return wsUrl.toString();
5907
5943
  };
5908
5944
  // Create WebSocket connection
@@ -6711,6 +6747,13 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6711
6747
  authPrincipal: connection.authPrincipal,
6712
6748
  };
6713
6749
  connection.pendingSubscriptions.set(subscriptionId, pending);
6750
+ // A fast auth refusal can open and close the socket before the first
6751
+ // caller finishes acquiring it. Keep this subscription for recovery,
6752
+ // but report the already observed disconnect without a timeout delay.
6753
+ if (pending.socketGeneration > 0 && socket.readyState === WS_READY_STATE_CLOSED) {
6754
+ rejectPendingSubscriptionConfirmation(connection, subscriptionId, new Error('WebSocket disconnected'));
6755
+ return;
6756
+ }
6714
6757
  // Bound a cold connection/auth wait as well. Once the socket becomes
6715
6758
  // ready, sendSubscribeWithConfirmation() replaces this timer with a
6716
6759
  // fresh per-socket confirmation window.
@@ -6788,6 +6831,7 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
6788
6831
  * Close all v2 subscriptions.
6789
6832
  */
6790
6833
  async function closeAllSubscriptionsV2() {
6834
+ var _a;
6791
6835
  const closePromises = [];
6792
6836
  for (const [appId, connection] of connections) {
6793
6837
  cancelIdleConnectionClose(connection);
@@ -6814,8 +6858,14 @@ async function closeAllSubscriptionsV2() {
6814
6858
  ws.close();
6815
6859
  }));
6816
6860
  }
6861
+ else {
6862
+ // CLOSED may mean a URL/auth acquisition is still pending.
6863
+ // close() also disables the wrapper's future native socket.
6864
+ ws.close();
6865
+ }
6817
6866
  }
6818
6867
  connections.delete(appId);
6868
+ (_a = connection.authRetryWake) === null || _a === void 0 ? void 0 : _a.call(connection);
6819
6869
  }
6820
6870
  connectionAcquisitions.clear();
6821
6871
  await Promise.all(closePromises);
@@ -6894,7 +6944,7 @@ async function reconnectWithNewAuthV2() {
6894
6944
  }
6895
6945
  }
6896
6946
  async function doReconnectWithNewAuth() {
6897
- ambientAuthFailure = null;
6947
+ var _a;
6898
6948
  // SECURITY (H1): the logged-in identity is changing (login / logout / switch).
6899
6949
  // Wipe ALL principal-scoped read caches so the new identity can never observe
6900
6950
  // data cached for the previous one. Clear both the WS response cache and the
@@ -6916,7 +6966,7 @@ async function doReconnectWithNewAuth() {
6916
6966
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6917
6967
  }
6918
6968
  try {
6919
- const { reconnectRealtimeStoreWithNewAuth } = await import('./realtime-store-DTS5OLm2.mjs');
6969
+ const { reconnectRealtimeStoreWithNewAuth } = await import('./realtime-store-CeDT7vJF.mjs');
6920
6970
  await reconnectRealtimeStoreWithNewAuth();
6921
6971
  }
6922
6972
  catch (error) {
@@ -6941,6 +6991,7 @@ async function doReconnectWithNewAuth() {
6941
6991
  // Reset auth failure counter — this is a proactive reconnect (login, token refresh)
6942
6992
  connection.consecutiveAuthFailures = 0;
6943
6993
  connection.authFailure = null;
6994
+ (_a = connection.authRetryWake) === null || _a === void 0 ? void 0 : _a.call(connection);
6944
6995
  // Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
6945
6996
  // We use reconnect() which will close and re-open with fresh URL (including new token)
6946
6997
  try {
@@ -8366,7 +8417,7 @@ async function setMany(many, options) {
8366
8417
  'Nothing was signed or submitted.');
8367
8418
  }
8368
8419
  const solanaChain = workerChain;
8369
- const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await import('./solana-operations-da26Ject.mjs');
8420
+ const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await import('./solana-operations-CHgW7Pue.mjs');
8370
8421
  // OA-0001: derive the expected intent from OUR OWN request (the one
8371
8422
  // ground truth a tampered builder response cannot change) so the
8372
8423
  // validator can reject any transaction whose operation semantics
@@ -9851,7 +9902,7 @@ function toMillis(seconds) {
9851
9902
 
9852
9903
  /** SIWS message creation is async already, so its onchain module can be lazy. */
9853
9904
  async function genSolanaMessage(address, nonce) {
9854
- const implementation = await import('./solana-message-j1s9osZD.mjs');
9905
+ const implementation = await import('./solana-message-BGkYFU3C.mjs');
9855
9906
  return implementation.genSolanaMessage(address, nonce);
9856
9907
  }
9857
9908
 
@@ -10401,4 +10452,4 @@ function defineLiveModule(mod) {
10401
10452
  }
10402
10453
 
10403
10454
  export { getIdToken as $, subscribe as A, closeAllSubscriptions as B, clearCache as C, getCachedData as D, reconnectWithNewAuth as E, createClient as F, ScopedClientReadOnlyError as G, subscribeQuery as H, InsufficientBalanceError as I, closeAllQuerySubscriptions as J, canonicalizeArgs as K, canonicalizeResult as L, DEFAULT_QUERY_POLL_INTERVAL_MS as M, MIN_QUERY_POLL_INTERVAL_MS as N, BoundedDeclineError as O, deriveDecline as P, dispatchDeclineEvent as Q, DECLINE_EVENT_NAME as R, SOLANA_MAX_SUPPORTED_TRANSACTION_VERSION as S, dispatchRateLimitEvent as T, RATE_LIMIT_EVENT_NAME as U, hasActiveConnection as V, increment as W, serverTimestamp as X, now as Y, toSeconds as Z, toMillis as _, getTransactionJson as a, deriveUserIdentityFromIdToken as a0, getSessionGeneration as a1, WebSessionManager as a2, ReactNativeSessionManager as a3, getActiveSessionManager as a4, onSessionCleared as a5, SessionEnvironmentError as a6, UNSUPPORTED_ENVIRONMENT_CODE as a7, AUTH_CHANGED_CODE as a8, AUTH_EXPIRED_CODE as a9, withEffects as aA, isEffectResult as aB, defineLiveModule as aC, EFFECT_INTENT_ADDRESS as aD, isAuthExpiredError as aa, getFreshAuthToken as ab, ServerSessionManager as ac, createSessionWithSignature as ad, refreshSession as ae, revokeSession as af, signSessionCreateMessage as ag, genAuthNonce as ah, genSolanaMessage as ai, createSolanaSignInRequest as aj, createSolanaSignInMessageText as ak, genSolanaSignInRequest as al, genSolanaSignInMessage as am, browserSignInDomain as an, SOLANA_SIGN_IN_STATEMENT as ao, genSiweMessage as ap, normalizeEvmAddress as aq, toLowerEvmAddress as ar, functions as as, invoke as at, FunctionInvokeError as au, live as av, intent as aw, status as ax, subscribeView as ay, LiveIntentError as az, bufferExports as b, getWebhookKeysUrl as c, getConfiguredAppId as d, get as e, getMany as f, getConfig as g, docId as h, init as i, setMany as j, setFile as k, getFiles as l, search as m, runQueryMany as n, runExpression as o, runExpressionMany as p, queryAggregate as q, runQuery as r, set as s, signMessage as t, signTransaction as u, signAndSubmitTransaction as v, count as w, aggregate as x, reconcileSetResult as y, waitForSetResult as z };
10404
- //# sourceMappingURL=index-BDW82Uxu.mjs.map
10455
+ //# sourceMappingURL=index-DW5yMQwV.mjs.map