@bounded-sh/core 0.0.69 → 0.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/config.d.ts +22 -1
- package/dist/{index-B3aNfGi6.mjs → index-P4eUuhOB.mjs} +141 -17
- package/dist/index-P4eUuhOB.mjs.map +1 -0
- package/dist/{index-DlJKTmRv.js → index-yT8ZfzrL.js} +141 -17
- package/dist/index-yT8ZfzrL.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{realtime-store-CkgpUOrE.js → realtime-store-BDpIOFMH.js} +2 -2
- package/dist/{realtime-store-CkgpUOrE.js.map → realtime-store-BDpIOFMH.js.map} +1 -1
- package/dist/{realtime-store-i8pxSuZE.mjs → realtime-store-Bcnv3f0g.mjs} +2 -2
- package/dist/{realtime-store-i8pxSuZE.mjs.map → realtime-store-Bcnv3f0g.mjs.map} +1 -1
- package/dist/{solana-message-DnV4FQ__.js → solana-message-Dh9ryO-Y.js} +2 -2
- package/dist/{solana-message-DnV4FQ__.js.map → solana-message-Dh9ryO-Y.js.map} +1 -1
- package/dist/{solana-message-AXfv2U1S.mjs → solana-message-x8ElgC9P.mjs} +2 -2
- package/dist/{solana-message-AXfv2U1S.mjs.map → solana-message-x8ElgC9P.mjs.map} +1 -1
- package/dist/{solana-operations-Bte92rmq.mjs → solana-operations-DEWIxJS8.mjs} +127 -8
- package/dist/solana-operations-DEWIxJS8.mjs.map +1 -0
- package/dist/{solana-operations-DPul1dWo.js → solana-operations-DGEGqFd9.js} +127 -8
- package/dist/solana-operations-DGEGqFd9.js.map +1 -0
- package/dist/utils/refresh-error.d.ts +9 -0
- package/package.json +2 -2
- package/dist/index-B3aNfGi6.mjs.map +0 -1
- package/dist/index-DlJKTmRv.js.map +0 -1
- package/dist/solana-operations-Bte92rmq.mjs.map +0 -1
- package/dist/solana-operations-DPul1dWo.js.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'
|
|
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
|
-
|
|
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
|
-
//
|
|
1048
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
1452
|
-
//
|
|
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-
|
|
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) {
|
|
@@ -5406,6 +5489,35 @@ async function getWsAuthToken(isServer) {
|
|
|
5406
5489
|
throw rememberAmbientAuthFailure(normalizeAuthExpiredError(error, 'Authentication expired and refresh failed'));
|
|
5407
5490
|
}
|
|
5408
5491
|
}
|
|
5492
|
+
/**
|
|
5493
|
+
* The server refused the token sent on the auth frame: its error frame arrives
|
|
5494
|
+
* while the auth ack is still pending, just before the 1008 close. Local expiry
|
|
5495
|
+
* had vouched for that exact token (unexpired but invalid - e.g. a rotated
|
|
5496
|
+
* issuer key), so re-sending it would only be refused again. Force a genuinely
|
|
5497
|
+
* new one: a definitive issuer rejection clears the session inside the shared
|
|
5498
|
+
* helper (firing notifySessionCleared, which signs the UI out), a successful
|
|
5499
|
+
* refresh commits a new token - and the ordinary reconnect loop picks up either
|
|
5500
|
+
* outcome on its own. Fresh token: authenticates. No token: connects
|
|
5501
|
+
* anonymously and public reads replay instead of erroring forever.
|
|
5502
|
+
*/
|
|
5503
|
+
async function recoverFromServerAuthRejection(connection) {
|
|
5504
|
+
const rejected = connection.pendingAuthToken;
|
|
5505
|
+
if (!rejected)
|
|
5506
|
+
return;
|
|
5507
|
+
// One forced refresh per distinct rejected token value: a transiently
|
|
5508
|
+
// unreachable issuer must not turn the reconnect loop into a refresh storm.
|
|
5509
|
+
if (connection.serverRejectedAuthToken === rejected)
|
|
5510
|
+
return;
|
|
5511
|
+
connection.serverRejectedAuthToken = rejected;
|
|
5512
|
+
try {
|
|
5513
|
+
await getFreshAuthToken(connection.isServer, { force: true });
|
|
5514
|
+
}
|
|
5515
|
+
catch (_a) {
|
|
5516
|
+
// Every failure mode is already handled inside the helper: a dead
|
|
5517
|
+
// session was cleared (auth listeners notified), a transient refresh
|
|
5518
|
+
// failure keeps the session and the latch above stops the storm.
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5409
5521
|
async function getConnectionAuthToken(isServer, authTokenProvider, anonymous) {
|
|
5410
5522
|
// A read-only scoped instance connects anonymously: no auth frame, and it
|
|
5411
5523
|
// must never read (or refresh) the ambient global session - that session
|
|
@@ -5729,6 +5841,8 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
|
|
|
5729
5841
|
authFailure: null,
|
|
5730
5842
|
source,
|
|
5731
5843
|
anonymous,
|
|
5844
|
+
isServer,
|
|
5845
|
+
serverRejectedAuthToken: null,
|
|
5732
5846
|
};
|
|
5733
5847
|
connections.set(connKey, connection);
|
|
5734
5848
|
// URL provider for reconnection with fresh tokens
|
|
@@ -5994,6 +6108,16 @@ function handleServerMessage(connection, message) {
|
|
|
5994
6108
|
}
|
|
5995
6109
|
case 'error': {
|
|
5996
6110
|
console.error('[WS v2] Server error:', message.code, message.message);
|
|
6111
|
+
// The auth frame was just refused: while the auth ack is pending the
|
|
6112
|
+
// server sends no other error frame, and this one is immediately
|
|
6113
|
+
// followed by its 1008 close. Kick the forced-refresh recovery - the
|
|
6114
|
+
// ordinary reconnect loop then re-auths with a fresh token or, once a
|
|
6115
|
+
// definitively dead session is cleared, connects anonymously so
|
|
6116
|
+
// public reads still replay. Ambient connections only: wallet-scoped
|
|
6117
|
+
// connections refresh through their own provider.
|
|
6118
|
+
if (connection.isAuthenticating && connection.pendingAuthToken && !connection.authTokenProvider && !connection.anonymous) {
|
|
6119
|
+
void recoverFromServerAuthRejection(connection);
|
|
6120
|
+
}
|
|
5997
6121
|
// Build an Error that ALSO carries the stable `code` as a structured
|
|
5998
6122
|
// field (not just string-interpolated into the message) so callers can
|
|
5999
6123
|
// branch on it — e.g. `catch (e) { if (e.code === 'invariant_violation') }`
|
|
@@ -6777,7 +6901,7 @@ async function doReconnectWithNewAuth() {
|
|
|
6777
6901
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
6778
6902
|
}
|
|
6779
6903
|
try {
|
|
6780
|
-
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-
|
|
6904
|
+
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-BDpIOFMH.js'); });
|
|
6781
6905
|
await reconnectRealtimeStoreWithNewAuth();
|
|
6782
6906
|
}
|
|
6783
6907
|
catch (error) {
|
|
@@ -8220,7 +8344,7 @@ async function setMany(many, options) {
|
|
|
8220
8344
|
'Nothing was signed or submitted.');
|
|
8221
8345
|
}
|
|
8222
8346
|
const solanaChain = workerChain;
|
|
8223
|
-
const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-
|
|
8347
|
+
const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-DGEGqFd9.js'); });
|
|
8224
8348
|
const solanaOutcome = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
|
|
8225
8349
|
// `shouldSubmitTx: false` signed WITHOUT sending — no signature exists,
|
|
8226
8350
|
// and there is nothing to mirror, so no synchronization is attempted.
|
|
@@ -9661,7 +9785,7 @@ function toMillis(seconds) {
|
|
|
9661
9785
|
|
|
9662
9786
|
/** SIWS message creation is async already, so its onchain module can be lazy. */
|
|
9663
9787
|
async function genSolanaMessage(address, nonce) {
|
|
9664
|
-
const implementation = await Promise.resolve().then(function () { return require('./solana-message-
|
|
9788
|
+
const implementation = await Promise.resolve().then(function () { return require('./solana-message-Dh9ryO-Y.js'); });
|
|
9665
9789
|
return implementation.genSolanaMessage(address, nonce);
|
|
9666
9790
|
}
|
|
9667
9791
|
|
|
@@ -10222,4 +10346,4 @@ exports.toMillis = toMillis;
|
|
|
10222
10346
|
exports.toSeconds = toSeconds;
|
|
10223
10347
|
exports.waitForSetResult = waitForSetResult;
|
|
10224
10348
|
exports.withEffects = withEffects;
|
|
10225
|
-
//# sourceMappingURL=index-
|
|
10349
|
+
//# sourceMappingURL=index-yT8ZfzrL.js.map
|