@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
@@ -185,6 +185,7 @@ export declare function getWebhookKeysUrl(): string | undefined;
185
185
  * `verifyWebhook` to bind a webhook to THIS app by default (#072) rather than trusting any
186
186
  * app's shared-key-signed delivery.
187
187
  */
188
+ export declare function getConfiguredSessionNamespace(): string;
188
189
  export declare function getConfiguredAppId(): string | undefined;
189
190
  /**
190
191
  * True when init() configured a Bounded network (the Cloudflare-native stack).
@@ -89,6 +89,9 @@ function getWebhookKeysUrl() {
89
89
  * `verifyWebhook` to bind a webhook to THIS app by default (#072) rather than trusting any
90
90
  * app's shared-key-signed delivery.
91
91
  */
92
+ function getConfiguredSessionNamespace() {
93
+ return JSON.stringify([(clientConfig.humanAuthApiUrl || "https://auth.bounded.sh").replace(/\/$/, ""), clientConfig.appId]);
94
+ }
92
95
  function getConfiguredAppId() {
93
96
  return clientConfig.appId || undefined;
94
97
  }
@@ -927,6 +930,30 @@ function decideCommit(current, expected, next) {
927
930
  }
928
931
 
929
932
  class WebSessionManager {
933
+ static scopedStorageKey() {
934
+ return 'bounded_session_storage:v2:' + getConfiguredSessionNamespace();
935
+ }
936
+ static get BOUNDED_SESSION_STORAGE_KEY() {
937
+ const scoped = this.scopedStorageKey();
938
+ return localStorage.getItem(scoped + ':scope') === 'app' || localStorage.getItem(scoped) !== null
939
+ ? scoped : 'bounded_session_storage';
940
+ }
941
+ static logoutKey() { return this.scopedStorageKey() + ':logout'; }
942
+ /** Capture before login's network work; checked under the session mutation lock. */
943
+ static captureSessionEstablishment() {
944
+ var _a, _b;
945
+ const namespace = getConfiguredSessionNamespace();
946
+ const epoch = this.establishmentEpoch;
947
+ const generation = (_b = (_a = this.readRaw()) === null || _a === void 0 ? void 0 : _a.sessionGeneration) !== null && _b !== void 0 ? _b : null;
948
+ const logout = localStorage.getItem(this.logoutKey());
949
+ return () => {
950
+ var _a, _b;
951
+ if (namespace !== getConfiguredSessionNamespace() || epoch !== this.establishmentEpoch
952
+ || generation !== ((_b = (_a = this.readRaw()) === null || _a === void 0 ? void 0 : _a.sessionGeneration) !== null && _b !== void 0 ? _b : null)
953
+ || logout !== localStorage.getItem(this.logoutKey()))
954
+ throw new Error('session_establishment_superseded');
955
+ };
956
+ }
930
957
  /** Raw read of the stored blob. Callers inside the lock use this, never a cached copy. */
931
958
  static readRaw() {
932
959
  if (typeof window === "undefined")
@@ -986,10 +1013,11 @@ class WebSessionManager {
986
1013
  /* ------------------------------------------------------------------ */
987
1014
  /* STORE */
988
1015
  /* ------------------------------------------------------------------ */
989
- static async storeSession(address, accessToken, idToken, refreshToken, issuer) {
1016
+ static async storeSession(address, accessToken, idToken, refreshToken, issuer, beforeCommit) {
990
1017
  var _a;
991
1018
  if (typeof window === "undefined")
992
1019
  return;
1020
+ let appScoped = false;
993
1021
  // JWT-wallet binding: refuse to store a session whose idToken is bound
994
1022
  // to a different wallet than `address`. Prevents races that would otherwise
995
1023
  // leave localStorage with mismatched address/token state.
@@ -997,6 +1025,7 @@ class WebSessionManager {
997
1025
  const payloadB64 = idToken.split(".")[1];
998
1026
  if (payloadB64) {
999
1027
  const payload = JSON.parse(this.decodeBase64Url(payloadB64));
1028
+ appScoped = payload["custom:walletScope"] === "app";
1000
1029
  const tokenWallet = payload["custom:walletAddress"];
1001
1030
  if (tokenWallet && tokenWallet !== address) {
1002
1031
  throw new Error(`[WebSessionManager] Refusing to store session: address (${address}) does not match idToken custom:walletAddress (${tokenWallet})`);
@@ -1014,6 +1043,9 @@ class WebSessionManager {
1014
1043
  }
1015
1044
  const config = await getConfig();
1016
1045
  const currentAppId = config.appId;
1046
+ const namespace = getConfiguredSessionNamespace();
1047
+ const storageKey = appScoped || localStorage.getItem(this.scopedStorageKey() + ":scope") === "app"
1048
+ ? this.scopedStorageKey() : "bounded_session_storage";
1017
1049
  // The auth issuer base that minted this session, so silent refresh POSTs to
1018
1050
  // the RIGHT /session/refresh. Email overrides with humanAuthApiUrl; all
1019
1051
  // wallet/guest/signature flows default to authApiUrl (the bounded-auth issuer).
@@ -1021,12 +1053,33 @@ class WebSessionManager {
1021
1053
  // A login always starts a NEW generation. Refresh commits preserve it, so
1022
1054
  // "same generation" means "same login", which is what lets a post-refresh
1023
1055
  // retry be told apart from a re-login by the same wallet.
1056
+ const generation = mintSessionGeneration();
1024
1057
  await withSessionLock(() => {
1025
- this.writeRaw(Object.assign({ address,
1058
+ beforeCommit === null || beforeCommit === void 0 ? void 0 : beforeCommit();
1059
+ if (namespace !== getConfiguredSessionNamespace())
1060
+ throw new Error("session_establishment_superseded");
1061
+ ++this.establishmentEpoch;
1062
+ if (appScoped)
1063
+ localStorage.setItem(this.scopedStorageKey() + ":scope", "app");
1064
+ localStorage.setItem(storageKey, JSON.stringify(Object.assign({ address,
1026
1065
  accessToken,
1027
1066
  idToken,
1028
- refreshToken, appId: currentAppId, sessionGeneration: mintSessionGeneration() }, (sessionIssuer ? { issuer: sessionIssuer } : {})));
1067
+ refreshToken, appId: currentAppId, sessionGeneration: generation }, (sessionIssuer ? { issuer: sessionIssuer } : {}))));
1029
1068
  });
1069
+ // Adoption can fail after persistence (for example, logout while the
1070
+ // provider is loading). Remove only this incomplete login, even if the
1071
+ // caller has since switched app configuration or another tab signed in.
1072
+ return async () => {
1073
+ const removed = await withSessionLock(() => {
1074
+ const raw = localStorage.getItem(storageKey);
1075
+ if (!raw || JSON.parse(raw).sessionGeneration !== generation)
1076
+ return false;
1077
+ localStorage.removeItem(storageKey);
1078
+ return true;
1079
+ });
1080
+ if (removed)
1081
+ notifySessionCleared();
1082
+ };
1030
1083
  }
1031
1084
  /* ------------------------------------------------------------------ */
1032
1085
  /* GET */
@@ -1141,10 +1194,14 @@ class WebSessionManager {
1141
1194
  static async clearSession() {
1142
1195
  if (typeof window === "undefined")
1143
1196
  return;
1197
+ ++this.establishmentEpoch;
1198
+ const key = this.BOUNDED_SESSION_STORAGE_KEY;
1199
+ const logoutKey = this.logoutKey();
1144
1200
  const removed = await withSessionLock(() => {
1145
- if (localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY) === null)
1201
+ localStorage.setItem(logoutKey, mintSessionGeneration());
1202
+ if (localStorage.getItem(key) === null)
1146
1203
  return false;
1147
- localStorage.removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
1204
+ localStorage.removeItem(key);
1148
1205
  return true;
1149
1206
  });
1150
1207
  // Only announce a clear that actually happened, so a no-op cannot sign out
@@ -1291,7 +1348,7 @@ class WebSessionManager {
1291
1348
  return { outcome: result.outcome, session: result.session };
1292
1349
  }
1293
1350
  }
1294
- WebSessionManager.BOUNDED_SESSION_STORAGE_KEY = "bounded_session_storage";
1351
+ WebSessionManager.establishmentEpoch = 0;
1295
1352
 
1296
1353
  let _config = null;
1297
1354
  class ReactNativeSessionManager {
@@ -4133,7 +4190,7 @@ class ServerSessionManager {
4133
4190
  const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
4134
4191
  /* fetch nonce from auth API (bound to the snapshotted app+issuer) */
4135
4192
  const nonce = await genAuthNonce(sessionConfig);
4136
- const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-Cww3s9HW.js'); });
4193
+ const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-w2FxOkFA.js'); });
4137
4194
  const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
4138
4195
  /* sign the message */
4139
4196
  const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
@@ -5241,7 +5298,6 @@ const WS_V2_PATH = '/ws/v2';
5241
5298
  let browserReconnectHooksAttached = false;
5242
5299
  let lastBrowserTriggeredReconnectAt = 0;
5243
5300
  let reconnectInProgress = null;
5244
- let ambientAuthFailure = null;
5245
5301
  // ============ Helper Functions ============
5246
5302
  function generateSubscriptionId() {
5247
5303
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
@@ -5369,34 +5425,6 @@ function getTokenExpirationTime(token) {
5369
5425
  return null;
5370
5426
  }
5371
5427
  }
5372
- /**
5373
- * Latch a failure so a dead session does not re-hammer /session/refresh every tick.
5374
- *
5375
- * NOT latched when the failure came with the session being cleared or replaced.
5376
- * The clear notifies auth-state listeners, which kicks a reconnect that resets this
5377
- * — and if we re-latched afterwards, `getConnectionAuthToken` would keep throwing
5378
- * even though storage is now empty, so an ordinary expiry left WebSocket
5379
- * acquisition permanently broken instead of falling back to an anonymous socket.
5380
- */
5381
- function rememberAmbientAuthFailure(error) {
5382
- if ((error === null || error === void 0 ? void 0 : error.code) === AUTH_CHANGED_CODE)
5383
- return error;
5384
- // The session is gone: the right next move is an anonymous connection, not a
5385
- // remembered failure.
5386
- if (!getIdTokenSync())
5387
- return error;
5388
- ambientAuthFailure = error;
5389
- return error;
5390
- }
5391
- /** Best-effort synchronous peek, only for deciding whether a session still exists. */
5392
- function getIdTokenSync() {
5393
- try {
5394
- return getActiveSessionManager().getIdToken();
5395
- }
5396
- catch (_a) {
5397
- return null;
5398
- }
5399
- }
5400
5428
  function failConnectionAuth(connection, error) {
5401
5429
  connection.authFailure = error;
5402
5430
  connection.pendingAuthToken = null;
@@ -5451,8 +5479,8 @@ function scheduleTokenRefresh(connection, isServer) {
5451
5479
  console.info('[WS v2] Token refreshed successfully, stored for next connection');
5452
5480
  }
5453
5481
  catch (refreshError) {
5454
- const definitive = (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_EXPIRED_CODE
5455
- || (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_CHANGED_CODE;
5482
+ const definitive = (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_CHANGED_CODE
5483
+ || !await getIdToken(isServer);
5456
5484
  if (definitive) {
5457
5485
  // The session is dead or belongs to someone else now. The shared
5458
5486
  // helper has already cleared storage and notified auth-state
@@ -5482,28 +5510,19 @@ function scheduleTokenRefresh(connection, isServer) {
5482
5510
  * WS-local wrapper around the shared freshness helper.
5483
5511
  *
5484
5512
  * The shared helper owns expiry classification, the deduplicated
5485
- * snapshot -> refresh -> commit transaction, and conditional clearing. This adds
5486
- * only the WS-specific storm guard: without it, a dead session would retry the
5487
- * refresh on every reconnect attempt and every 5-minute tick.
5513
+ * snapshot -> refresh -> commit transaction, and conditional clearing. A transient
5514
+ * failure must remain retryable; reconnect backoff bounds automatic attempts.
5488
5515
  */
5489
5516
  async function getWsAuthToken(isServer) {
5490
5517
  var _a, _b;
5491
- // A remembered failure short-circuits, but only while a session still exists.
5492
- // See rememberAmbientAuthFailure: once storage is cleared the correct next move
5493
- // is an anonymous connection, not a replayed error.
5494
- if (ambientAuthFailure && getIdTokenSync()) {
5495
- throw ambientAuthFailure;
5496
- }
5497
5518
  try {
5498
- const token = await getFreshAuthToken(isServer);
5499
- ambientAuthFailure = null;
5500
- return token;
5519
+ return await getFreshAuthToken(isServer);
5501
5520
  }
5502
5521
  catch (error) {
5503
5522
  // Status only — the raw axios error carries config.data (the refresh token)
5504
5523
  // and must never reach a console or log sink.
5505
5524
  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');
5506
- throw rememberAmbientAuthFailure(normalizeAuthExpiredError(error, 'Authentication expired and refresh failed'));
5525
+ throw normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5507
5526
  }
5508
5527
  }
5509
5528
  /**
@@ -5521,18 +5540,21 @@ async function recoverFromServerAuthRejection(connection) {
5521
5540
  const rejected = connection.pendingAuthToken;
5522
5541
  if (!rejected)
5523
5542
  return;
5524
- // One forced refresh per distinct rejected token value: a transiently
5525
- // unreachable issuer must not turn the reconnect loop into a refresh storm.
5543
+ // Share an in-flight attempt and stop retrying a definitively refused token.
5544
+ // Transient failures may retry on the next socket reconnect, with its backoff.
5526
5545
  if (connection.serverRejectedAuthToken === rejected)
5527
5546
  return;
5528
5547
  connection.serverRejectedAuthToken = rejected;
5529
5548
  try {
5530
5549
  await getFreshAuthToken(connection.isServer, { force: true });
5531
5550
  }
5532
- catch (_a) {
5533
- // Every failure mode is already handled inside the helper: a dead
5534
- // session was cleared (auth listeners notified), a transient refresh
5535
- // failure keeps the session and the latch above stops the storm.
5551
+ catch (error) {
5552
+ 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
5553
+ && connection.serverRejectedAuthToken === rejected) {
5554
+ connection.serverRejectedAuthToken = null;
5555
+ }
5556
+ // The shared helper clears definitively rejected sessions and preserves
5557
+ // transient failures. Never reuse the failed result as a credential.
5536
5558
  }
5537
5559
  }
5538
5560
  async function getConnectionAuthToken(isServer, authTokenProvider, anonymous) {
@@ -5543,9 +5565,6 @@ async function getConnectionAuthToken(isServer, authTokenProvider, anonymous) {
5543
5565
  return null;
5544
5566
  }
5545
5567
  if (!authTokenProvider) {
5546
- // getWsAuthToken, not the shared helper directly: the wrapper carries the
5547
- // reconnect-storm guard that keeps a dead session from re-refreshing on
5548
- // every reconnect attempt.
5549
5568
  return getWsAuthToken(isServer);
5550
5569
  }
5551
5570
  try {
@@ -5650,11 +5669,13 @@ function cancelIdleConnectionClose(connection) {
5650
5669
  connection.idleCloseTimer = null;
5651
5670
  }
5652
5671
  function closeConnectionNow(connection) {
5672
+ var _a;
5653
5673
  if (connections.get(connection.key) !== connection)
5654
5674
  return;
5655
5675
  if (!connection.ws)
5656
5676
  return;
5657
5677
  cancelIdleConnectionClose(connection);
5678
+ (_a = connection.authRetryWake) === null || _a === void 0 ? void 0 : _a.call(connection);
5658
5679
  if (connection.tokenRefreshTimer) {
5659
5680
  clearInterval(connection.tokenRefreshTimer);
5660
5681
  connection.tokenRefreshTimer = null;
@@ -5675,7 +5696,7 @@ function closeConnectionNow(connection) {
5675
5696
  try {
5676
5697
  ws.close();
5677
5698
  }
5678
- catch ( /* already closing */_a) { /* already closing */ }
5699
+ catch ( /* already closing */_b) { /* already closing */ }
5679
5700
  }
5680
5701
  function scheduleConnectionCloseIfIdle(connection) {
5681
5702
  var _a;
@@ -5761,6 +5782,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5761
5782
  }
5762
5783
  }
5763
5784
  async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey, source) {
5785
+ var _a, _b;
5764
5786
  attachBrowserReconnectHooksOnce();
5765
5787
  const anonymous = (source === null || source === void 0 ? void 0 : source.anonymous) === true;
5766
5788
  // A per-room subscription gets its OWN connection routed to the room DO; all
@@ -5807,9 +5829,6 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
5807
5829
  // lease also guards this window, but cancelling here avoids a needless
5808
5830
  // grace-timer round-trip on the common reuse path.
5809
5831
  cancelIdleConnectionClose(connection);
5810
- if (connection.authFailure) {
5811
- throw connection.authFailure;
5812
- }
5813
5832
  let currentAuthToken;
5814
5833
  try {
5815
5834
  currentAuthToken = await getConnectionAuthToken(isServer, authTokenProvider, anonymous);
@@ -5819,6 +5838,14 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
5819
5838
  failConnectionAuth(connection, authError);
5820
5839
  throw authError;
5821
5840
  }
5841
+ if (connection.authFailure) {
5842
+ // The preflight recovered, but failConnectionAuth invalidated this
5843
+ // socket's auth state. Authenticate again before replaying any reads.
5844
+ connection.authFailure = null;
5845
+ (_a = connection.authRetryWake) === null || _a === void 0 ? void 0 : _a.call(connection);
5846
+ connection.ws.reconnect();
5847
+ return connection;
5848
+ }
5822
5849
  if (connection.isConnected &&
5823
5850
  !connection.isAuthenticating &&
5824
5851
  !connectionMatchesAuthToken(connection, currentAuthToken)) {
@@ -5861,50 +5888,59 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
5861
5888
  isServer,
5862
5889
  serverRejectedAuthToken: null,
5863
5890
  };
5891
+ // Resolve configuration before handing control to reconnecting-websocket.
5892
+ // Its 4.4.0 URL-provider promise has no rejection handler and a rejected
5893
+ // provider permanently holds _connectLock. Credential failures therefore
5894
+ // stay inside this bounded, cancellable acquisition loop.
5895
+ const wsApiUrl = (_b = source === null || source === void 0 ? void 0 : source.wsApiUrl) !== null && _b !== void 0 ? _b : (await getConfig()).wsApiUrl;
5896
+ const wsUrl = new URL(wsApiUrl);
5897
+ wsUrl.pathname = WS_V2_PATH;
5898
+ wsUrl.searchParams.append('appId', appId || (await getConfig()).appId);
5899
+ if (connection.routePath)
5900
+ wsUrl.searchParams.append('routePath', connection.routePath);
5864
5901
  connections.set(connKey, connection);
5865
- // URL provider for reconnection with fresh tokens
5866
5902
  const urlProvider = async () => {
5867
- var _a;
5868
- // A scoped createClient() connection targets its own realtime endpoint;
5869
- // the default path reads the module-global config.
5870
- const wsApiUrl = (_a = source === null || source === void 0 ? void 0 : source.wsApiUrl) !== null && _a !== void 0 ? _a : (await getConfig()).wsApiUrl;
5871
- const wsUrl = new URL(wsApiUrl);
5872
- // Always use v2 path
5873
- wsUrl.pathname = WS_V2_PATH;
5874
- // Set appId from the configured SDK source, or an explicit per-call
5875
- // override passed through subscribeV2. Do not consult ambient globals:
5876
- // app identity must come from init({ appId }) or an explicit call option.
5877
- // `appId` here is already the resolved effectiveAppId; fall back to the
5878
- // global only on the default path when it was somehow empty.
5879
- wsUrl.searchParams.append('appId', appId || (await getConfig()).appId);
5880
- // Per-room connection: carry the room path so the worker routes this WS
5881
- // to the room DO (appId#room#roomId) where the live view fan-out lives.
5882
- if (connection.routePath) {
5883
- wsUrl.searchParams.append('routePath', connection.routePath);
5884
- }
5885
- // Resolve auth token if available. A wallet-scoped connection resolves
5886
- // its token from the wallet's own session (self-refreshing); all others
5887
- // use the browser session or fail closed for top-level server calls. The token is sent as the first WS
5888
- // frame after open, never as a URL query parameter.
5889
- let authToken;
5890
- try {
5891
- authToken = initialAuthToken !== undefined
5892
- ? initialAuthToken
5893
- : await getConnectionAuthToken(isServer, connection.authTokenProvider, connection.anonymous);
5894
- initialAuthToken = undefined;
5895
- }
5896
- catch (error) {
5897
- const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5898
- failConnectionAuth(connection, authError);
5899
- throw authError;
5900
- }
5901
- connection.pendingAuthToken = authToken || null;
5902
- connection.pendingAuthSnapshot = authSnapshotFromToken(authToken);
5903
- if (authToken) {
5904
- // Successful token acquisition — reset failure counter
5903
+ let retryDelay = WS_CONFIG.minReconnectionDelay;
5904
+ while (connections.get(connKey) === connection) {
5905
+ let authToken;
5906
+ try {
5907
+ authToken = initialAuthToken !== undefined
5908
+ ? initialAuthToken
5909
+ : await getConnectionAuthToken(isServer, connection.authTokenProvider, connection.anonymous);
5910
+ initialAuthToken = undefined;
5911
+ }
5912
+ catch (error) {
5913
+ if (connections.get(connKey) !== connection)
5914
+ break;
5915
+ const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5916
+ failConnectionAuth(connection, authError);
5917
+ // An error callback may synchronously tear down or replace this
5918
+ // connection. Do not install a retry after its cleanup completed.
5919
+ if (connections.get(connKey) !== connection)
5920
+ break;
5921
+ await new Promise((resolve) => {
5922
+ const wake = () => {
5923
+ clearTimeout(timer);
5924
+ if (connection.authRetryWake === wake)
5925
+ connection.authRetryWake = undefined;
5926
+ resolve();
5927
+ };
5928
+ const timer = setTimeout(wake, retryDelay);
5929
+ connection.authRetryWake = wake;
5930
+ });
5931
+ retryDelay = Math.min(MAX_RECONNECT_DELAY_MS, retryDelay * RECONNECT_DELAY_GROW_FACTOR);
5932
+ continue;
5933
+ }
5934
+ if (connections.get(connKey) !== connection)
5935
+ break;
5936
+ connection.pendingAuthToken = authToken || null;
5937
+ connection.pendingAuthSnapshot = authSnapshotFromToken(authToken);
5905
5938
  connection.consecutiveAuthFailures = 0;
5906
5939
  connection.authFailure = null;
5940
+ return wsUrl.toString();
5907
5941
  }
5942
+ // Teardown has called close() on the wrapper, which prevents creation
5943
+ // of a native socket when this abandoned provider resolves.
5908
5944
  return wsUrl.toString();
5909
5945
  };
5910
5946
  // Create WebSocket connection
@@ -6713,6 +6749,13 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6713
6749
  authPrincipal: connection.authPrincipal,
6714
6750
  };
6715
6751
  connection.pendingSubscriptions.set(subscriptionId, pending);
6752
+ // A fast auth refusal can open and close the socket before the first
6753
+ // caller finishes acquiring it. Keep this subscription for recovery,
6754
+ // but report the already observed disconnect without a timeout delay.
6755
+ if (pending.socketGeneration > 0 && socket.readyState === WS_READY_STATE_CLOSED) {
6756
+ rejectPendingSubscriptionConfirmation(connection, subscriptionId, new Error('WebSocket disconnected'));
6757
+ return;
6758
+ }
6716
6759
  // Bound a cold connection/auth wait as well. Once the socket becomes
6717
6760
  // ready, sendSubscribeWithConfirmation() replaces this timer with a
6718
6761
  // fresh per-socket confirmation window.
@@ -6790,6 +6833,7 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
6790
6833
  * Close all v2 subscriptions.
6791
6834
  */
6792
6835
  async function closeAllSubscriptionsV2() {
6836
+ var _a;
6793
6837
  const closePromises = [];
6794
6838
  for (const [appId, connection] of connections) {
6795
6839
  cancelIdleConnectionClose(connection);
@@ -6816,8 +6860,14 @@ async function closeAllSubscriptionsV2() {
6816
6860
  ws.close();
6817
6861
  }));
6818
6862
  }
6863
+ else {
6864
+ // CLOSED may mean a URL/auth acquisition is still pending.
6865
+ // close() also disables the wrapper's future native socket.
6866
+ ws.close();
6867
+ }
6819
6868
  }
6820
6869
  connections.delete(appId);
6870
+ (_a = connection.authRetryWake) === null || _a === void 0 ? void 0 : _a.call(connection);
6821
6871
  }
6822
6872
  connectionAcquisitions.clear();
6823
6873
  await Promise.all(closePromises);
@@ -6896,7 +6946,7 @@ async function reconnectWithNewAuthV2() {
6896
6946
  }
6897
6947
  }
6898
6948
  async function doReconnectWithNewAuth() {
6899
- ambientAuthFailure = null;
6949
+ var _a;
6900
6950
  // SECURITY (H1): the logged-in identity is changing (login / logout / switch).
6901
6951
  // Wipe ALL principal-scoped read caches so the new identity can never observe
6902
6952
  // data cached for the previous one. Clear both the WS response cache and the
@@ -6918,7 +6968,7 @@ async function doReconnectWithNewAuth() {
6918
6968
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6919
6969
  }
6920
6970
  try {
6921
- const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-Dytq73zO.js'); });
6971
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-D7mJ7rTj.js'); });
6922
6972
  await reconnectRealtimeStoreWithNewAuth();
6923
6973
  }
6924
6974
  catch (error) {
@@ -6943,6 +6993,7 @@ async function doReconnectWithNewAuth() {
6943
6993
  // Reset auth failure counter — this is a proactive reconnect (login, token refresh)
6944
6994
  connection.consecutiveAuthFailures = 0;
6945
6995
  connection.authFailure = null;
6996
+ (_a = connection.authRetryWake) === null || _a === void 0 ? void 0 : _a.call(connection);
6946
6997
  // Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
6947
6998
  // We use reconnect() which will close and re-open with fresh URL (including new token)
6948
6999
  try {
@@ -8368,7 +8419,7 @@ async function setMany(many, options) {
8368
8419
  'Nothing was signed or submitted.');
8369
8420
  }
8370
8421
  const solanaChain = workerChain;
8371
- const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-SR1En23U.js'); });
8422
+ const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-DJQDYdrk.js'); });
8372
8423
  // OA-0001: derive the expected intent from OUR OWN request (the one
8373
8424
  // ground truth a tampered builder response cannot change) so the
8374
8425
  // validator can reject any transaction whose operation semantics
@@ -9853,7 +9904,7 @@ function toMillis(seconds) {
9853
9904
 
9854
9905
  /** SIWS message creation is async already, so its onchain module can be lazy. */
9855
9906
  async function genSolanaMessage(address, nonce) {
9856
- const implementation = await Promise.resolve().then(function () { return require('./solana-message-Cww3s9HW.js'); });
9907
+ const implementation = await Promise.resolve().then(function () { return require('./solana-message-w2FxOkFA.js'); });
9857
9908
  return implementation.genSolanaMessage(address, nonce);
9858
9909
  }
9859
9910
 
@@ -10496,4 +10547,4 @@ exports.toMillis = toMillis;
10496
10547
  exports.toSeconds = toSeconds;
10497
10548
  exports.waitForSetResult = waitForSetResult;
10498
10549
  exports.withEffects = withEffects;
10499
- //# sourceMappingURL=index-D-dJjY-Q.js.map
10550
+ //# sourceMappingURL=index-B8PpRj6x.js.map