@bounded-sh/core 0.0.70 → 0.0.72

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 (23) hide show
  1. package/dist/client/config.d.ts +22 -1
  2. package/dist/{index-Csf_2uZF.mjs → index-P4eUuhOB.mjs} +100 -17
  3. package/dist/index-P4eUuhOB.mjs.map +1 -0
  4. package/dist/{index-CqTl6vHg.js → index-yT8ZfzrL.js} +100 -17
  5. package/dist/index-yT8ZfzrL.js.map +1 -0
  6. package/dist/index.js +1 -1
  7. package/dist/index.mjs +1 -1
  8. package/dist/{realtime-store-DfeKV55-.js → realtime-store-BDpIOFMH.js} +2 -2
  9. package/dist/{realtime-store-DfeKV55-.js.map → realtime-store-BDpIOFMH.js.map} +1 -1
  10. package/dist/{realtime-store-ByP5wYsa.mjs → realtime-store-Bcnv3f0g.mjs} +2 -2
  11. package/dist/{realtime-store-ByP5wYsa.mjs.map → realtime-store-Bcnv3f0g.mjs.map} +1 -1
  12. package/dist/{solana-message-BecAT47y.js → solana-message-Dh9ryO-Y.js} +2 -2
  13. package/dist/{solana-message-BecAT47y.js.map → solana-message-Dh9ryO-Y.js.map} +1 -1
  14. package/dist/{solana-message-BFqre_Oi.mjs → solana-message-x8ElgC9P.mjs} +2 -2
  15. package/dist/{solana-message-BFqre_Oi.mjs.map → solana-message-x8ElgC9P.mjs.map} +1 -1
  16. package/dist/{solana-operations-ByMDnjog.mjs → solana-operations-DEWIxJS8.mjs} +2 -2
  17. package/dist/{solana-operations-ByMDnjog.mjs.map → solana-operations-DEWIxJS8.mjs.map} +1 -1
  18. package/dist/{solana-operations-uN89eqO2.js → solana-operations-DGEGqFd9.js} +2 -2
  19. package/dist/{solana-operations-uN89eqO2.js.map → solana-operations-DGEGqFd9.js.map} +1 -1
  20. package/dist/utils/refresh-error.d.ts +9 -0
  21. package/package.json +2 -2
  22. package/dist/index-CqTl6vHg.js.map +0 -1
  23. package/dist/index-Csf_2uZF.mjs.map +0 -1
@@ -25,7 +25,8 @@ const DEFAULT_CLIENT_CONFIG = {
25
25
  // No hidden modal fallback: browser clients start hosted Bounded Auth with
26
26
  // loginWithRedirect/loginWithPopup. For crypto/onchain wallet login use
27
27
  // authMethod:'phantom' (Solana / Phantom), and use signInAnonymously() for
28
- // zero-friction guest accounts. ('wallet' is an unimplemented stub; don't use.)
28
+ // zero-friction guest accounts. ('wallet' and 'mobile-wallet-adapter' are
29
+ // accepted spellings of the same lane and canonicalize to 'phantom'.)
29
30
  authMethod: 'none',
30
31
  chain: '',
31
32
  rpcUrl: '',
@@ -642,6 +643,53 @@ async function revokeSession(refreshToken, issuer) {
642
643
  async function signSessionCreateMessage(_signMessageFunction) {
643
644
  }
644
645
 
646
+ // Classify a session-refresh failure as a DEFINITIVE rejection of the credential
647
+ // vs a TRANSPORT failure (DS4-0054).
648
+ //
649
+ // The restore path refreshes an expired token at boot. The old code destroyed the
650
+ // local session on ANY refresh failure, so a network blip - common on mobile -
651
+ // signed the user out. Only a real HTTP rejection of the refresh token proves the
652
+ // session is invalid; anything else is unknown, and the safe move is to RETAIN local
653
+ // state and retry. Keeping a genuinely dead session locally is harmless: every
654
+ // request still fails closed server-side, and the next refresh that reaches the
655
+ // server clears it. Destroying a live session on a blip is user-visible damage.
656
+ //
657
+ // `/session/refresh` answers 401 (session_revoked / invalid_grant) when the token is
658
+ // dead. 400 (bad request), 408/429 (transient), and 5xx are NOT proof the session is
659
+ // invalid, and a network/DNS/timeout error carries no HTTP response at all.
660
+ //
661
+ // This is the single classifier for every refresh path: the per-request freshness
662
+ // check (auth-token-freshness) and the boot-time restore paths (web + RN session
663
+ // managers) all import it, so "definitive" means the same thing everywhere. It lives
664
+ // in this leaf module to avoid an import cycle between those consumers.
665
+ function isDefinitiveRejection(err) {
666
+ var _a;
667
+ const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
668
+ return status === 401 || status === 403;
669
+ }
670
+ // How long past the id token's own `exp` a session may survive purely on
671
+ // transport failures.
672
+ //
673
+ // "Retain on a blip" must not become "retain forever". A statusless failure is
674
+ // unknown, so the first ones are absorbed - but a revoked session, or a gateway
675
+ // that rewrites the issuer's 401 into a 502, would otherwise keep
676
+ // isAuthenticated() reporting a signed-in user for as long as the device never
677
+ // reaches the issuer again. One day covers an offline flight or a bad-network day
678
+ // while making the retention finite: past it, an unrefreshable session is cleared
679
+ // and the user is asked to sign in again.
680
+ const TRANSPORT_RETENTION_GRACE_MS = 24 * 60 * 60 * 1000;
681
+ /**
682
+ * May a session whose refresh failed WITHOUT a definitive rejection be kept?
683
+ *
684
+ * `expSeconds` is the id token's `exp` claim. Fails closed: a missing or
685
+ * non-numeric expiry is not a usable bound, so such a session is never retained.
686
+ */
687
+ function mayRetainAfterTransportFailure(expSeconds, now = Date.now()) {
688
+ if (typeof expSeconds !== 'number' || !Number.isFinite(expSeconds))
689
+ return false;
690
+ return now <= expSeconds * 1000 + TRANSPORT_RETENTION_GRACE_MS;
691
+ }
692
+
645
693
  // Shared session-binding guard for the web + React Native session managers, so
646
694
  // the cross-principal refresh guard (#220 / #221) lives in ONE place and the two
647
695
  // managers can't drift apart. Base64url decoding differs by platform (browser
@@ -1023,7 +1071,26 @@ class WebSessionManager {
1023
1071
  await this.clearSessionIfGeneration(generation);
1024
1072
  return null;
1025
1073
  }
1026
- const refreshed = await refreshSession(refreshToken, sessionObj.issuer);
1074
+ let refreshed;
1075
+ try {
1076
+ refreshed = await refreshSession(refreshToken, sessionObj.issuer);
1077
+ }
1078
+ catch (refreshErr) {
1079
+ // DS4-0054: only a DEFINITIVE server rejection (401/403) proves the
1080
+ // refresh token is dead. A transport failure - offline, DNS, timeout,
1081
+ // 5xx - is unknown, not proof the session is invalid. Destroying local
1082
+ // state here signs the user out on a network blip (common on mobile),
1083
+ // so RETAIN it and return the existing session; the next request will
1084
+ // retry the refresh. The retention is BOUNDED by the token's own
1085
+ // expiry plus a grace window, so a revoked session - or an issuer 401
1086
+ // a gateway rewrote into a 5xx - cannot keep isAuthenticated() true
1087
+ // forever on a device that never reaches the issuer again.
1088
+ if (isDefinitiveRejection(refreshErr) || !mayRetainAfterTransportFailure(exp)) {
1089
+ await this.clearSessionIfGeneration(generation);
1090
+ return null;
1091
+ }
1092
+ return { address: sessionObj.address, session: sessionObj };
1093
+ }
1027
1094
  if ((refreshed === null || refreshed === void 0 ? void 0 : refreshed.idToken) && (refreshed === null || refreshed === void 0 ? void 0 : refreshed.accessToken)) {
1028
1095
  const committed = await this.commitRefreshedTokens({ generation, idToken: sessionObj.idToken }, refreshed.idToken, refreshed.accessToken,
1029
1096
  // Persist the ROTATED refresh token — the old one is now
@@ -1044,8 +1111,9 @@ class WebSessionManager {
1044
1111
  return null;
1045
1112
  return { address: newObj.address, session: newObj };
1046
1113
  }
1047
- // Refresh failed — clear stale session to prevent retry loops, but only
1048
- // if it is still the session we set out to refresh.
1114
+ // The server answered but issued no usable tokens - a definitive refusal,
1115
+ // not a transport failure - so clear the stale session to stop retry loops,
1116
+ // but only if it is still the session we set out to refresh.
1049
1117
  await this.clearSessionIfGeneration(generation);
1050
1118
  return null;
1051
1119
  }
@@ -1431,7 +1499,26 @@ class ReactNativeSessionManager {
1431
1499
  await this.clearSessionIfGeneration(generation);
1432
1500
  return null;
1433
1501
  }
1434
- const refreshed = await refreshSession(refreshToken, sessionObj.issuer);
1502
+ let refreshed;
1503
+ try {
1504
+ refreshed = await refreshSession(refreshToken, sessionObj.issuer);
1505
+ }
1506
+ catch (refreshErr) {
1507
+ // DS4-0054: only a DEFINITIVE server rejection (401/403) proves the
1508
+ // refresh token is dead. A transport failure - offline, DNS, timeout,
1509
+ // 5xx - is unknown, not proof the session is invalid. On mobile these
1510
+ // blips are common, so destroying local state here signs the user out
1511
+ // on a bad network. RETAIN it and return the existing session; the next
1512
+ // request retries the refresh. The retention is BOUNDED by the token's
1513
+ // own expiry plus a grace window, so a revoked session - or an issuer
1514
+ // 401 a gateway rewrote into a 5xx - cannot keep isAuthenticated() true
1515
+ // forever on a device that never reaches the issuer again.
1516
+ if (isDefinitiveRejection(refreshErr) || !mayRetainAfterTransportFailure(exp)) {
1517
+ await this.clearSessionIfGeneration(generation);
1518
+ return null;
1519
+ }
1520
+ return { address: sessionObj.address, session: sessionObj };
1521
+ }
1435
1522
  if ((refreshed === null || refreshed === void 0 ? void 0 : refreshed.idToken) && (refreshed === null || refreshed === void 0 ? void 0 : refreshed.accessToken)) {
1436
1523
  const committed = await this.commitRefreshedTokens({ generation, idToken: sessionObj.idToken }, refreshed.idToken, refreshed.accessToken, refreshed.refreshToken);
1437
1524
  // Superseded means a login or logout landed mid-refresh. Returning
@@ -1448,8 +1535,9 @@ class ReactNativeSessionManager {
1448
1535
  return null;
1449
1536
  return { address: newObj.address, session: newObj };
1450
1537
  }
1451
- // Refresh failed — clear stale session to prevent retry loops, but only
1452
- // if it is still the session we set out to refresh.
1538
+ // The server answered but issued no usable tokens - a definitive refusal,
1539
+ // not a transport failure - so clear the stale session to stop retry loops,
1540
+ // but only if it is still the session we set out to refresh.
1453
1541
  await this.clearSessionIfGeneration(generation);
1454
1542
  return null;
1455
1543
  }
@@ -4045,7 +4133,7 @@ class ServerSessionManager {
4045
4133
  const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
4046
4134
  /* fetch nonce from auth API (bound to the snapshotted app+issuer) */
4047
4135
  const nonce = await genAuthNonce(sessionConfig);
4048
- const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-BecAT47y.js'); });
4136
+ const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-Dh9ryO-Y.js'); });
4049
4137
  const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
4050
4138
  /* sign the message */
4051
4139
  const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
@@ -4417,11 +4505,6 @@ const refreshTransactions = new Map();
4417
4505
  function transactionKey(scope, appId, generation, refreshToken) {
4418
4506
  return `${scope}:${appId}:${generation !== null && generation !== void 0 ? generation : 'none'}:${tokenFingerprint(refreshToken)}`;
4419
4507
  }
4420
- function isDefinitiveRejection(error) {
4421
- var _a;
4422
- const status = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status;
4423
- return status === 401 || status === 403;
4424
- }
4425
4508
  async function readSessionParts(isServer) {
4426
4509
  var _a, _b, _c, _d, _e, _f;
4427
4510
  if (isServer) {
@@ -6818,7 +6901,7 @@ async function doReconnectWithNewAuth() {
6818
6901
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6819
6902
  }
6820
6903
  try {
6821
- const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-DfeKV55-.js'); });
6904
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-BDpIOFMH.js'); });
6822
6905
  await reconnectRealtimeStoreWithNewAuth();
6823
6906
  }
6824
6907
  catch (error) {
@@ -8261,7 +8344,7 @@ async function setMany(many, options) {
8261
8344
  'Nothing was signed or submitted.');
8262
8345
  }
8263
8346
  const solanaChain = workerChain;
8264
- const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-uN89eqO2.js'); });
8347
+ const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-DGEGqFd9.js'); });
8265
8348
  const solanaOutcome = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
8266
8349
  // `shouldSubmitTx: false` signed WITHOUT sending — no signature exists,
8267
8350
  // and there is nothing to mirror, so no synchronization is attempted.
@@ -9702,7 +9785,7 @@ function toMillis(seconds) {
9702
9785
 
9703
9786
  /** SIWS message creation is async already, so its onchain module can be lazy. */
9704
9787
  async function genSolanaMessage(address, nonce) {
9705
- const implementation = await Promise.resolve().then(function () { return require('./solana-message-BecAT47y.js'); });
9788
+ const implementation = await Promise.resolve().then(function () { return require('./solana-message-Dh9ryO-Y.js'); });
9706
9789
  return implementation.genSolanaMessage(address, nonce);
9707
9790
  }
9708
9791
 
@@ -10263,4 +10346,4 @@ exports.toMillis = toMillis;
10263
10346
  exports.toSeconds = toSeconds;
10264
10347
  exports.waitForSetResult = waitForSetResult;
10265
10348
  exports.withEffects = withEffects;
10266
- //# sourceMappingURL=index-CqTl6vHg.js.map
10349
+ //# sourceMappingURL=index-yT8ZfzrL.js.map