@bounded-sh/core 0.0.112-ux-audit.2 → 0.0.113

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 (26) hide show
  1. package/dist/{index-CVWqE5hO.mjs → index-CkgEqMkJ.mjs} +113 -60
  2. package/dist/index-CkgEqMkJ.mjs.map +1 -0
  3. package/dist/{index-DOKk4VdC.js → index-WIHuQp4L.js} +113 -60
  4. package/dist/index-WIHuQp4L.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/index.mjs +1 -1
  7. package/dist/{realtime-store-CIrvP17-.js → realtime-store-7itLaAao.js} +2 -2
  8. package/dist/{realtime-store-CIrvP17-.js.map → realtime-store-7itLaAao.js.map} +1 -1
  9. package/dist/{realtime-store-Cno1p315.mjs → realtime-store-BIB8v265.mjs} +2 -2
  10. package/dist/{realtime-store-Cno1p315.mjs.map → realtime-store-BIB8v265.mjs.map} +1 -1
  11. package/dist/{solana-message-BTs3b7JU.mjs → solana-message-BBMxFFjM.mjs} +2 -2
  12. package/dist/{solana-message-BTs3b7JU.mjs.map → solana-message-BBMxFFjM.mjs.map} +1 -1
  13. package/dist/{solana-message-BMaEWGOW.js → solana-message-Ce6_jR6H.js} +2 -2
  14. package/dist/{solana-message-BMaEWGOW.js.map → solana-message-Ce6_jR6H.js.map} +1 -1
  15. package/dist/{solana-operations-BcFqY34S.js → solana-operations-CNeO_lJ2.js} +3 -3
  16. package/dist/{solana-operations-BcFqY34S.js.map → solana-operations-CNeO_lJ2.js.map} +1 -1
  17. package/dist/{solana-operations-CU-8CFHk.mjs → solana-operations-Cshh2EfH.mjs} +3 -3
  18. package/dist/{solana-operations-CU-8CFHk.mjs.map → solana-operations-Cshh2EfH.mjs.map} +1 -1
  19. package/dist/{solana-v1-operations-BNLWUYEm.mjs → solana-v1-operations-B8vV5teG.mjs} +3 -3
  20. package/dist/{solana-v1-operations-BNLWUYEm.mjs.map → solana-v1-operations-B8vV5teG.mjs.map} +1 -1
  21. package/dist/{solana-v1-operations-RNpyoM03.js → solana-v1-operations-c0p0YESV.js} +3 -3
  22. package/dist/{solana-v1-operations-RNpyoM03.js.map → solana-v1-operations-c0p0YESV.js.map} +1 -1
  23. package/dist/utils/auth-token-freshness.d.ts +7 -1
  24. package/package.json +1 -1
  25. package/dist/index-CVWqE5hO.mjs.map +0 -1
  26. package/dist/index-DOKk4VdC.js.map +0 -1
@@ -4190,7 +4190,7 @@ class ServerSessionManager {
4190
4190
  const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
4191
4191
  /* fetch nonce from auth API (bound to the snapshotted app+issuer) */
4192
4192
  const nonce = await genAuthNonce(sessionConfig);
4193
- const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-BMaEWGOW.js'); });
4193
+ const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-Ce6_jR6H.js'); });
4194
4194
  const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
4195
4195
  /* sign the message */
4196
4196
  const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
@@ -4520,7 +4520,7 @@ function normalizeAuthExpiredError(error, fallbackMessage) {
4520
4520
  }
4521
4521
  /** Refresh this many ms before `exp`, so a token cannot die mid-flight. */
4522
4522
  const TOKEN_EXPIRY_SKEW_MS = 60000;
4523
- function classifyToken(token, nowMs = Date.now()) {
4523
+ function classifyToken(token, nowMs = Date.now(), skewMs = TOKEN_EXPIRY_SKEW_MS) {
4524
4524
  let exp;
4525
4525
  try {
4526
4526
  const payload = JSON.parse(decodeBase64Url(token.split('.')[1]));
@@ -4535,7 +4535,7 @@ function classifyToken(token, nowMs = Date.now()) {
4535
4535
  const expiresAtMs = exp * 1000;
4536
4536
  if (nowMs >= expiresAtMs)
4537
4537
  return 'expired';
4538
- if (nowMs >= expiresAtMs - TOKEN_EXPIRY_SKEW_MS)
4538
+ if (nowMs >= expiresAtMs - skewMs)
4539
4539
  return 'expiring-soon';
4540
4540
  return 'fresh';
4541
4541
  }
@@ -4606,12 +4606,13 @@ async function commit(isServer, expected, refreshed) {
4606
4606
  * used; the caller must surface it rather than downgrade to anonymous.
4607
4607
  */
4608
4608
  async function getFreshAuthToken(isServer = false, options = {}) {
4609
+ var _a;
4609
4610
  const { idToken, refreshToken, issuer, generation } = await readSessionParts(isServer);
4610
4611
  // No session at all: anonymous is legitimate.
4611
4612
  if (!idToken)
4612
4613
  return null;
4613
4614
  const force = options.force === true;
4614
- const freshness = classifyToken(idToken);
4615
+ const freshness = classifyToken(idToken, Date.now(), Math.max(TOKEN_EXPIRY_SKEW_MS, (_a = options.refreshWithinMs) !== null && _a !== void 0 ? _a : 0));
4615
4616
  if (!force && freshness === 'fresh')
4616
4617
  return idToken;
4617
4618
  // Still valid for a little longer, so a failed refresh can fall back to it —
@@ -5334,8 +5335,11 @@ const connectionAcquisitions = new Map();
5334
5335
  // cache entry so collision isolation does not evict another principal's cache.
5335
5336
  const responseCache = new Map();
5336
5337
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
5337
- const TOKEN_REFRESH_BUFFER = 10 * 60 * 1000; // Refresh token 10 minutes before expiry
5338
5338
  const TOKEN_CHECK_INTERVAL = 5 * 60 * 1000; // Check token freshness every 5 minutes
5339
+ // The worker closes a socket once the token it authenticated with expires. A socket
5340
+ // opens with, and each check refreshes, any token that would expire before the next
5341
+ // check plus two minutes for a timer a background tab delays.
5342
+ const TOKEN_REFRESH_WITHIN_MS = TOKEN_CHECK_INTERVAL + 2 * 60 * 1000;
5339
5343
  function getResponseCacheEntry(cacheKey, identityToken) {
5340
5344
  var _a;
5341
5345
  return (_a = responseCache.get(cacheKey)) === null || _a === void 0 ? void 0 : _a.find(entry => entry.identityToken === identityToken);
@@ -5623,21 +5627,13 @@ let connectionEpoch = 0;
5623
5627
  // WebSocket paths cannot drift apart. `classifyToken` replaces the old boolean,
5624
5628
  // which collapsed "expiring soon" and "already expired" into one answer and read a
5625
5629
  // token with no numeric `exp` as fresh.
5626
- function getTokenExpirationTime(token) {
5627
- try {
5628
- const payload = JSON.parse(decodeBase64Url(token.split('.')[1]));
5629
- return payload.exp ? payload.exp * 1000 : null;
5630
- }
5631
- catch (_a) {
5632
- return null;
5633
- }
5634
- }
5635
5630
  function failConnectionAuth(connection, error) {
5636
5631
  connection.authFailure = error;
5637
5632
  connection.pendingAuthToken = null;
5638
5633
  connection.pendingAuthSnapshot = null;
5639
5634
  connection.authTokenValue = null;
5640
5635
  connection.authPrincipal = null;
5636
+ connection.reauthSnapshot = null;
5641
5637
  connection.isConnecting = false;
5642
5638
  connection.isConnected = false;
5643
5639
  connection.isAuthenticating = false;
@@ -5664,48 +5660,43 @@ function scheduleTokenRefresh(connection, isServer) {
5664
5660
  const currentToken = await getIdToken(isServer);
5665
5661
  if (!currentToken)
5666
5662
  return; // Unauthenticated, nothing to refresh
5667
- const expirationTime = getTokenExpirationTime(currentToken);
5668
- if (!expirationTime)
5669
- return;
5670
- const timeUntilExpiry = expirationTime - Date.now();
5671
- if (timeUntilExpiry <= TOKEN_REFRESH_BUFFER) {
5672
- console.info('[WS v2] Token expiring soon, proactively refreshing');
5673
- // Refresh the token and store it. Do NOT reconnect — the server only
5674
- // verifies JWT at $connect time, so the existing connection continues
5675
- // working. The fresh token is picked up by urlProvider the next time
5676
- // a reconnect naturally happens (network drop, keepalive timeout, etc.).
5677
- //
5678
- // Routed through the shared transaction. This used to read a refresh
5679
- // token, call refreshSession, and write the result with nothing binding
5680
- // it to the session it started from — so a timer that fired for S1 and
5681
- // completed after a same-wallet re-login could overwrite S2's rotated
5682
- // token, and the next refresh would trip the issuer's reuse detection
5683
- // and revoke the whole family.
5684
- try {
5685
- await getFreshAuthToken(isServer);
5686
- console.info('[WS v2] Token refreshed successfully, stored for next connection');
5687
- }
5688
- catch (refreshError) {
5689
- const definitive = (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_CHANGED_CODE
5690
- || !await getIdToken(isServer);
5691
- if (definitive) {
5692
- // The session is dead or belongs to someone else now. The shared
5693
- // helper has already cleared storage and notified auth-state
5694
- // listeners, so the UI stops showing a connection that is gone —
5695
- // this branch used to just stop the timer and leave the UI
5696
- // claiming to be connected.
5697
- console.warn('[WS v2] Session no longer usable, stopping periodic refresh');
5698
- if (connection.tokenRefreshTimer) {
5699
- clearInterval(connection.tokenRefreshTimer);
5700
- connection.tokenRefreshTimer = null;
5701
- }
5702
- return;
5663
+ // The worker closes a socket once the token it authenticated with
5664
+ // expires. Refresh any token the next check could reach too late, and
5665
+ // give the open socket the stored token whenever it changed: refreshed
5666
+ // here, in another tab, or by an HTTP call.
5667
+ //
5668
+ // Routed through the shared transaction. This used to read a refresh
5669
+ // token, call refreshSession, and write the result with nothing binding
5670
+ // it to the session it started from - so a timer that fired for S1 and
5671
+ // completed after a same-wallet re-login could overwrite S2's rotated
5672
+ // token, and the next refresh would trip the issuer's reuse detection
5673
+ // and revoke the whole family.
5674
+ try {
5675
+ const token = await getFreshAuthToken(isServer, { refreshWithinMs: TOKEN_REFRESH_WITHIN_MS });
5676
+ // A wallet-scoped socket authenticates through its own provider.
5677
+ if (token && !connection.authTokenProvider)
5678
+ reauthenticateOpenSocket(connection, token);
5679
+ }
5680
+ catch (refreshError) {
5681
+ const definitive = (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_CHANGED_CODE
5682
+ || !await getIdToken(isServer);
5683
+ if (definitive) {
5684
+ // The session is dead or belongs to someone else now. The shared
5685
+ // helper has already cleared storage and notified auth-state
5686
+ // listeners, so the UI stops showing a connection that is gone -
5687
+ // this branch used to just stop the timer and leave the UI
5688
+ // claiming to be connected.
5689
+ console.warn('[WS v2] Session no longer usable, stopping periodic refresh');
5690
+ if (connection.tokenRefreshTimer) {
5691
+ clearInterval(connection.tokenRefreshTimer);
5692
+ connection.tokenRefreshTimer = null;
5703
5693
  }
5704
- // Transient: keep the session and try again next interval.
5705
- // Status only — the raw axios error carries config.data (the refresh
5706
- // token) and must never hit the console / log sinks.
5707
- console.warn('[WS v2] Proactive token refresh failed, will retry next interval. status:', (_b = (_a = refreshError === null || refreshError === void 0 ? void 0 : refreshError.status) !== null && _a !== void 0 ? _a : refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) !== null && _b !== void 0 ? _b : 'unknown');
5694
+ return;
5708
5695
  }
5696
+ // Transient: keep the session and try again next interval.
5697
+ // Status only - the raw axios error carries config.data (the refresh
5698
+ // token) and must never hit the console / log sinks.
5699
+ console.warn('[WS v2] Proactive token refresh failed, will retry next interval. status:', (_b = (_a = refreshError === null || refreshError === void 0 ? void 0 : refreshError.status) !== null && _a !== void 0 ? _a : refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) !== null && _b !== void 0 ? _b : 'unknown');
5709
5700
  }
5710
5701
  }
5711
5702
  catch (error) {
@@ -5713,6 +5704,49 @@ function scheduleTokenRefresh(connection, isServer) {
5713
5704
  }
5714
5705
  }, TOKEN_CHECK_INTERVAL);
5715
5706
  }
5707
+ /**
5708
+ * The identity the worker keys a socket's subscriptions by (subscriptions.ts
5709
+ * connectionIdentityKey): the user id, wallet, email and EVM wallet its auth reads
5710
+ * from the token, normalized as it does, plus the guest flag. An auth frame that
5711
+ * changes it makes the worker drop every subscription on the socket.
5712
+ */
5713
+ function socketIdentityKey(idToken) {
5714
+ try {
5715
+ const { id, address, email, evmAddress } = deriveUserIdentityFromIdToken(idToken);
5716
+ const guest = JSON.parse(decodeBase64Url(idToken.split('.')[1])).is_anonymous === true;
5717
+ return id && JSON.stringify([id, address, email, evmAddress, guest]);
5718
+ }
5719
+ catch (_a) {
5720
+ return null;
5721
+ }
5722
+ }
5723
+ /**
5724
+ * Give an authenticated socket the stored token before the worker closes it at
5725
+ * its own token's expiry. For an unchanged worker identity an auth frame keeps
5726
+ * the subscriptions and moves the deadline. The same account with changed claims
5727
+ * (a refresh that bound a wallet, a guest that signed up) would lose them to that
5728
+ * frame, so it reconnects and replays them instead. Another account is the
5729
+ * auth-change path's job (reconnect with cleared read caches), never this one's.
5730
+ */
5731
+ function reauthenticateOpenSocket(connection, token) {
5732
+ // Only a socket that is OPEN: reconnecting-websocket queues anything else and
5733
+ // flushes it into the next socket ahead of that socket's own auth.
5734
+ if (!isWarmConnection(connection) || connection.reauthSnapshot)
5735
+ return;
5736
+ if (!connection.authTokenValue || connection.authTokenValue === token)
5737
+ return;
5738
+ const identity = socketIdentityKey(token);
5739
+ if (!identity)
5740
+ return;
5741
+ const snapshot = authSnapshotFromToken(token);
5742
+ if (identity === socketIdentityKey(connection.authTokenValue)) {
5743
+ connection.reauthSnapshot = snapshot;
5744
+ connection.ws.send(JSON.stringify({ type: 'auth', token }));
5745
+ }
5746
+ else if (snapshot.principal === connection.authPrincipal) {
5747
+ connection.ws.reconnect();
5748
+ }
5749
+ }
5716
5750
  /**
5717
5751
  * WS-local wrapper around the shared freshness helper.
5718
5752
  *
@@ -5723,7 +5757,8 @@ function scheduleTokenRefresh(connection, isServer) {
5723
5757
  async function getWsAuthToken(isServer) {
5724
5758
  var _a, _b;
5725
5759
  try {
5726
- return await getFreshAuthToken(isServer);
5760
+ // A socket must outlive its first token check, five minutes after it opens.
5761
+ return await getFreshAuthToken(isServer, { refreshWithinMs: TOKEN_REFRESH_WITHIN_MS });
5727
5762
  }
5728
5763
  catch (error) {
5729
5764
  // Status only — the raw axios error carries config.data (the refresh token)
@@ -6229,6 +6264,7 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6229
6264
  pendingAuthSnapshot: null,
6230
6265
  authTokenValue: null,
6231
6266
  authPrincipal: null,
6267
+ reauthSnapshot: null,
6232
6268
  socketGeneration: 0,
6233
6269
  heartbeat: null,
6234
6270
  heartbeatReconnectTimer: null,
@@ -6306,6 +6342,7 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6306
6342
  connection.socketGeneration += 1;
6307
6343
  connection.isConnecting = false;
6308
6344
  connection.isConnected = true;
6345
+ connection.reauthSnapshot = null;
6309
6346
  startHeartbeat(connection, ws);
6310
6347
  // NOTE: Do NOT reset consecutiveAuthFailures here. It is reset when a
6311
6348
  // fresh auth token is actually obtained or on an explicit auth change.
@@ -6372,6 +6409,7 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
6372
6409
  stopHeartbeat(connection);
6373
6410
  connection.isConnected = false;
6374
6411
  connection.isAuthenticating = false;
6412
+ connection.reauthSnapshot = null;
6375
6413
  const disconnectError = new Error('WebSocket disconnected');
6376
6414
  for (const subscriptionId of Array.from(connection.pendingSubscriptions.keys())) {
6377
6415
  rejectPendingSubscriptionConfirmation(connection, subscriptionId, disconnectError);
@@ -6393,6 +6431,21 @@ function handleServerMessage(connection, message) {
6393
6431
  var _a, _b, _c, _d, _e, _f;
6394
6432
  switch (message.type) {
6395
6433
  case 'authenticated': {
6434
+ const reauth = connection.reauthSnapshot;
6435
+ if (reauth) {
6436
+ // The stored token on the open socket: its subscriptions are still
6437
+ // live server-side, so there is nothing to replay, and a subscribe
6438
+ // still awaiting confirmation keeps its one retry.
6439
+ const previous = connection.authTokenValue;
6440
+ connection.reauthSnapshot = null;
6441
+ connection.authTokenValue = reauth.tokenValue;
6442
+ connection.authPrincipal = reauth.principal;
6443
+ for (const pending of connection.pendingSubscriptions.values()) {
6444
+ if (pending.authTokenValue === previous)
6445
+ pending.authTokenValue = reauth.tokenValue;
6446
+ }
6447
+ break;
6448
+ }
6396
6449
  connection.isAuthenticating = false;
6397
6450
  connection.authTokenValue = (_b = (_a = connection.pendingAuthSnapshot) === null || _a === void 0 ? void 0 : _a.tokenValue) !== null && _b !== void 0 ? _b : null;
6398
6451
  connection.authPrincipal = (_d = (_c = connection.pendingAuthSnapshot) === null || _c === void 0 ? void 0 : _c.principal) !== null && _d !== void 0 ? _d : null;
@@ -7476,7 +7529,7 @@ async function doReconnectWithNewAuth() {
7476
7529
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
7477
7530
  }
7478
7531
  try {
7479
- const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-CIrvP17-.js'); });
7532
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-7itLaAao.js'); });
7480
7533
  await reconnectRealtimeStoreWithNewAuth();
7481
7534
  }
7482
7535
  catch (error) {
@@ -8974,7 +9027,7 @@ async function setMany(many, options) {
8974
9027
  if (typeof curTx.serializedTransaction !== 'string' || typeof curTx.poofnetIntentId !== 'string') {
8975
9028
  throw new Error('Unsupported 202 transaction shape: a poofnet write needs serializedTransaction and poofnetIntentId');
8976
9029
  }
8977
- const { signPoofnetTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-BcFqY34S.js'); });
9030
+ const { signPoofnetTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-CNeO_lJ2.js'); });
8978
9031
  const signedTransaction = await signPoofnetTransaction(curTx.serializedTransaction, authProvider, options, buildExpectedWriteIntent(documents));
8979
9032
  // Signing-only, exactly as on a real chain: signed and NOT committed.
8980
9033
  if ((options === null || options === void 0 ? void 0 : options.shouldSubmitTx) === false) {
@@ -8999,7 +9052,7 @@ async function setMany(many, options) {
8999
9052
  'Nothing was signed or submitted.');
9000
9053
  }
9001
9054
  const solanaChain = workerChain;
9002
- const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-BcFqY34S.js'); });
9055
+ const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-CNeO_lJ2.js'); });
9003
9056
  // OA-0001: derive the expected intent from OUR OWN request (the one
9004
9057
  // ground truth a tampered builder response cannot change) so the
9005
9058
  // validator can reject any transaction whose operation semantics
@@ -10763,7 +10816,7 @@ function toMillis(seconds) {
10763
10816
 
10764
10817
  /** SIWS message creation is async already, so its onchain module can be lazy. */
10765
10818
  async function genSolanaMessage(address, nonce) {
10766
- const implementation = await Promise.resolve().then(function () { return require('./solana-message-BMaEWGOW.js'); });
10819
+ const implementation = await Promise.resolve().then(function () { return require('./solana-message-Ce6_jR6H.js'); });
10767
10820
  return implementation.genSolanaMessage(address, nonce);
10768
10821
  }
10769
10822
 
@@ -11411,4 +11464,4 @@ exports.toSeconds = toSeconds;
11411
11464
  exports.waitForSetResult = waitForSetResult;
11412
11465
  exports.withEffects = withEffects;
11413
11466
  exports.writeNotAppliedPhase = writeNotAppliedPhase;
11414
- //# sourceMappingURL=index-DOKk4VdC.js.map
11467
+ //# sourceMappingURL=index-WIHuQp4L.js.map