@bounded-sh/core 0.0.114 → 0.0.116
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/operations.d.ts +14 -0
- package/dist/client/reconcile.d.ts +18 -0
- package/dist/client/venue-grant-write.d.ts +62 -0
- package/dist/client/write-outcome.d.ts +1 -0
- package/dist/{index--ahzzmm5.js → index-CjiaT9AT.js} +295 -62
- package/dist/index-CjiaT9AT.js.map +1 -0
- package/dist/{index-DTTh5Wt5.mjs → index-Db4lrwO6.mjs} +291 -63
- package/dist/index-Db4lrwO6.mjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{realtime-store-Bcra04TH.mjs → realtime-store-CNs5zURZ.mjs} +2 -2
- package/dist/{realtime-store-Bcra04TH.mjs.map → realtime-store-CNs5zURZ.mjs.map} +1 -1
- package/dist/{realtime-store-CR80Jac5.js → realtime-store-IPPwUhJc.js} +2 -2
- package/dist/{realtime-store-CR80Jac5.js.map → realtime-store-IPPwUhJc.js.map} +1 -1
- package/dist/{solana-message-BwPW9Fst.mjs → solana-message-BO6Dtgbv.mjs} +2 -2
- package/dist/{solana-message-BwPW9Fst.mjs.map → solana-message-BO6Dtgbv.mjs.map} +1 -1
- package/dist/{solana-message-DPR9uw0N.js → solana-message-CXuLc7NX.js} +2 -2
- package/dist/{solana-message-DPR9uw0N.js.map → solana-message-CXuLc7NX.js.map} +1 -1
- package/dist/{solana-operations-BMB6sq_i.js → solana-operations-CghJe0_7.js} +3 -3
- package/dist/{solana-operations-BMB6sq_i.js.map → solana-operations-CghJe0_7.js.map} +1 -1
- package/dist/{solana-operations-BBQllFVm.mjs → solana-operations-DDt4CzRR.mjs} +3 -3
- package/dist/{solana-operations-BBQllFVm.mjs.map → solana-operations-DDt4CzRR.mjs.map} +1 -1
- package/dist/{solana-v1-operations-CCHbLEo-.js → solana-v1-operations-BY2DTfbR.js} +3 -3
- package/dist/{solana-v1-operations-CCHbLEo-.js.map → solana-v1-operations-BY2DTfbR.js.map} +1 -1
- package/dist/{solana-v1-operations-DBuA0PlD.mjs → solana-v1-operations-TH-HUzbT.mjs} +3 -3
- package/dist/{solana-v1-operations-DBuA0PlD.mjs.map → solana-v1-operations-TH-HUzbT.mjs.map} +1 -1
- package/dist/utils/refresh-error.d.ts +4 -0
- package/dist/utils/session-signal.d.ts +7 -0
- package/package.json +2 -2
- package/dist/index--ahzzmm5.js.map +0 -1
- package/dist/index-DTTh5Wt5.mjs.map +0 -1
|
@@ -421,6 +421,97 @@ axiosRetry.exponentialDelay = exponentialDelay;
|
|
|
421
421
|
axiosRetry.linearDelay = linearDelay;
|
|
422
422
|
axiosRetry.isRetryableError = isRetryableError;
|
|
423
423
|
|
|
424
|
+
// Classify a session-refresh failure as a DEFINITIVE rejection of the credential
|
|
425
|
+
// vs a TRANSPORT failure (DS4-0054).
|
|
426
|
+
//
|
|
427
|
+
// The restore path refreshes an expired token at boot. The old code destroyed the
|
|
428
|
+
// local session on ANY refresh failure, so a network blip - common on mobile -
|
|
429
|
+
// signed the user out. Only a real HTTP rejection of the refresh token proves the
|
|
430
|
+
// session is invalid; anything else is unknown, and the safe move is to RETAIN local
|
|
431
|
+
// state and retry. Keeping a genuinely dead session locally is harmless: every
|
|
432
|
+
// request still fails closed server-side, and the next refresh that reaches the
|
|
433
|
+
// server clears it. Destroying a live session on a blip is user-visible damage.
|
|
434
|
+
//
|
|
435
|
+
// `/session/refresh` answers 401 (session_revoked / invalid_grant) when the token is
|
|
436
|
+
// dead. 400 (bad request), 408/429 (transient), and 5xx are NOT proof the session is
|
|
437
|
+
// invalid, and a network/DNS/timeout error carries no HTTP response at all.
|
|
438
|
+
//
|
|
439
|
+
// This is the single classifier for every refresh path: the per-request freshness
|
|
440
|
+
// check (auth-token-freshness) and the boot-time restore paths (web + RN session
|
|
441
|
+
// managers) all import it, so "definitive" means the same thing everywhere. It lives
|
|
442
|
+
// in this leaf module to avoid an import cycle between those consumers.
|
|
443
|
+
function isDefinitiveRejection(err) {
|
|
444
|
+
var _a;
|
|
445
|
+
const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
446
|
+
return status === 401 || status === 403;
|
|
447
|
+
}
|
|
448
|
+
// How long past the id token's own `exp` a session may survive purely on
|
|
449
|
+
// transport failures.
|
|
450
|
+
//
|
|
451
|
+
// "Retain on a blip" must not become "retain forever". A statusless failure is
|
|
452
|
+
// unknown, so the first ones are absorbed - but a revoked session, or a gateway
|
|
453
|
+
// that rewrites the issuer's 401 into a 502, would otherwise keep
|
|
454
|
+
// isAuthenticated() reporting a signed-in user for as long as the device never
|
|
455
|
+
// reaches the issuer again. One day covers an offline flight or a bad-network day
|
|
456
|
+
// while making the retention finite: past it, an unrefreshable session is cleared
|
|
457
|
+
// and the user is asked to sign in again.
|
|
458
|
+
const TRANSPORT_RETENTION_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
459
|
+
/**
|
|
460
|
+
* May a session whose refresh failed WITHOUT a definitive rejection be kept?
|
|
461
|
+
*
|
|
462
|
+
* `expSeconds` is the id token's `exp` claim. Fails closed: a missing or
|
|
463
|
+
* non-numeric expiry is not a usable bound, so such a session is never retained.
|
|
464
|
+
*/
|
|
465
|
+
function mayRetainAfterTransportFailure(expSeconds, now = Date.now()) {
|
|
466
|
+
if (typeof expSeconds !== 'number' || !Number.isFinite(expSeconds))
|
|
467
|
+
return false;
|
|
468
|
+
return now <= expSeconds * 1000 + TRANSPORT_RETENTION_GRACE_MS;
|
|
469
|
+
}
|
|
470
|
+
// Backoff between refresh attempts that failed WITHOUT a definitive rejection.
|
|
471
|
+
//
|
|
472
|
+
// A transport failure retains the session (above), and every authenticated request
|
|
473
|
+
// and the realtime token timer asks for a fresh token - so without a pause, an issuer
|
|
474
|
+
// that answers 503 or 429 gets one refresh per request from every open tab, which is
|
|
475
|
+
// the exact load that keeps it down. After such a failure the same refresh token is
|
|
476
|
+
// not sent again until the backoff passes: 15s, doubling, capped at 10 minutes, or the
|
|
477
|
+
// issuer's own Retry-After when it is longer. Callers inside the window get a
|
|
478
|
+
// non-definitive error at once (`code: 'refresh_backoff'`) and treat it like the
|
|
479
|
+
// failure that started it. A success, or a different token, starts clean.
|
|
480
|
+
const REFRESH_BACKOFF_BASE_MS = 15000;
|
|
481
|
+
const REFRESH_BACKOFF_MAX_MS = 10 * 60000;
|
|
482
|
+
const refreshBackoff = new Map();
|
|
483
|
+
function retryAfterMs(err) {
|
|
484
|
+
var _a, _b;
|
|
485
|
+
const headers = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.headers;
|
|
486
|
+
const raw = headers && ((_b = headers['retry-after']) !== null && _b !== void 0 ? _b : headers['Retry-After']);
|
|
487
|
+
const seconds = Number(raw);
|
|
488
|
+
return Number.isFinite(seconds) && seconds > 0 ? Math.min(seconds * 1000, REFRESH_BACKOFF_MAX_MS) : 0;
|
|
489
|
+
}
|
|
490
|
+
/** Throws a non-definitive `refresh_backoff` error while `key` is still backing off. */
|
|
491
|
+
function assertRefreshAllowed(key, now = Date.now()) {
|
|
492
|
+
const entry = refreshBackoff.get(key);
|
|
493
|
+
if (!entry || now >= entry.until)
|
|
494
|
+
return;
|
|
495
|
+
const err = new Error('Session refresh is backing off after a failed attempt');
|
|
496
|
+
err.code = 'refresh_backoff';
|
|
497
|
+
err.retryAt = entry.until;
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
/** Record the outcome of a refresh attempt for `key`. */
|
|
501
|
+
function recordRefreshOutcome(key, err, now = Date.now()) {
|
|
502
|
+
var _a, _b;
|
|
503
|
+
if (!err || isDefinitiveRejection(err)) {
|
|
504
|
+
refreshBackoff.delete(key);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const failures = ((_b = (_a = refreshBackoff.get(key)) === null || _a === void 0 ? void 0 : _a.failures) !== null && _b !== void 0 ? _b : 0) + 1;
|
|
508
|
+
const backoff = Math.min(REFRESH_BACKOFF_BASE_MS * 2 ** (failures - 1), REFRESH_BACKOFF_MAX_MS);
|
|
509
|
+
refreshBackoff.set(key, { failures, until: now + Math.max(backoff, retryAfterMs(err)) });
|
|
510
|
+
// One entry per refresh token; a long-lived tab rotates through a handful at most.
|
|
511
|
+
if (refreshBackoff.size > 32)
|
|
512
|
+
refreshBackoff.delete(refreshBackoff.keys().next().value);
|
|
513
|
+
}
|
|
514
|
+
|
|
424
515
|
/**
|
|
425
516
|
* Resolve the refresh/revoke base URL to a CONFIGURED issuer only. The session's
|
|
426
517
|
* `issuer` is persisted in client storage; if that storage is tampered (e.g. XSS)
|
|
@@ -574,6 +665,9 @@ async function refreshSession(refreshToken, issuer) {
|
|
|
574
665
|
const existing = refreshInFlightByToken.get(dedupeKey);
|
|
575
666
|
if (existing)
|
|
576
667
|
return existing;
|
|
668
|
+
// Inside a backoff window this throws at once, so a failing issuer is not asked again
|
|
669
|
+
// by every request and timer in the page (see refresh-error.ts).
|
|
670
|
+
assertRefreshAllowed(dedupeKey);
|
|
577
671
|
let currentFlight;
|
|
578
672
|
currentFlight = (async () => {
|
|
579
673
|
try {
|
|
@@ -587,8 +681,13 @@ async function refreshSession(refreshToken, issuer) {
|
|
|
587
681
|
refreshToken,
|
|
588
682
|
appId
|
|
589
683
|
}, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
|
|
684
|
+
recordRefreshOutcome(dedupeKey, null);
|
|
590
685
|
return response.data;
|
|
591
686
|
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
recordRefreshOutcome(dedupeKey, error);
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
592
691
|
finally {
|
|
593
692
|
// Only remove our own entry, and only if it is still the active flight
|
|
594
693
|
// for this token (a later refresh of the same token cannot have replaced
|
|
@@ -646,53 +745,6 @@ async function revokeSession(refreshToken, issuer) {
|
|
|
646
745
|
async function signSessionCreateMessage(_signMessageFunction) {
|
|
647
746
|
}
|
|
648
747
|
|
|
649
|
-
// Classify a session-refresh failure as a DEFINITIVE rejection of the credential
|
|
650
|
-
// vs a TRANSPORT failure (DS4-0054).
|
|
651
|
-
//
|
|
652
|
-
// The restore path refreshes an expired token at boot. The old code destroyed the
|
|
653
|
-
// local session on ANY refresh failure, so a network blip - common on mobile -
|
|
654
|
-
// signed the user out. Only a real HTTP rejection of the refresh token proves the
|
|
655
|
-
// session is invalid; anything else is unknown, and the safe move is to RETAIN local
|
|
656
|
-
// state and retry. Keeping a genuinely dead session locally is harmless: every
|
|
657
|
-
// request still fails closed server-side, and the next refresh that reaches the
|
|
658
|
-
// server clears it. Destroying a live session on a blip is user-visible damage.
|
|
659
|
-
//
|
|
660
|
-
// `/session/refresh` answers 401 (session_revoked / invalid_grant) when the token is
|
|
661
|
-
// dead. 400 (bad request), 408/429 (transient), and 5xx are NOT proof the session is
|
|
662
|
-
// invalid, and a network/DNS/timeout error carries no HTTP response at all.
|
|
663
|
-
//
|
|
664
|
-
// This is the single classifier for every refresh path: the per-request freshness
|
|
665
|
-
// check (auth-token-freshness) and the boot-time restore paths (web + RN session
|
|
666
|
-
// managers) all import it, so "definitive" means the same thing everywhere. It lives
|
|
667
|
-
// in this leaf module to avoid an import cycle between those consumers.
|
|
668
|
-
function isDefinitiveRejection(err) {
|
|
669
|
-
var _a;
|
|
670
|
-
const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
671
|
-
return status === 401 || status === 403;
|
|
672
|
-
}
|
|
673
|
-
// How long past the id token's own `exp` a session may survive purely on
|
|
674
|
-
// transport failures.
|
|
675
|
-
//
|
|
676
|
-
// "Retain on a blip" must not become "retain forever". A statusless failure is
|
|
677
|
-
// unknown, so the first ones are absorbed - but a revoked session, or a gateway
|
|
678
|
-
// that rewrites the issuer's 401 into a 502, would otherwise keep
|
|
679
|
-
// isAuthenticated() reporting a signed-in user for as long as the device never
|
|
680
|
-
// reaches the issuer again. One day covers an offline flight or a bad-network day
|
|
681
|
-
// while making the retention finite: past it, an unrefreshable session is cleared
|
|
682
|
-
// and the user is asked to sign in again.
|
|
683
|
-
const TRANSPORT_RETENTION_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
684
|
-
/**
|
|
685
|
-
* May a session whose refresh failed WITHOUT a definitive rejection be kept?
|
|
686
|
-
*
|
|
687
|
-
* `expSeconds` is the id token's `exp` claim. Fails closed: a missing or
|
|
688
|
-
* non-numeric expiry is not a usable bound, so such a session is never retained.
|
|
689
|
-
*/
|
|
690
|
-
function mayRetainAfterTransportFailure(expSeconds, now = Date.now()) {
|
|
691
|
-
if (typeof expSeconds !== 'number' || !Number.isFinite(expSeconds))
|
|
692
|
-
return false;
|
|
693
|
-
return now <= expSeconds * 1000 + TRANSPORT_RETENTION_GRACE_MS;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
748
|
// Shared session-binding guard for the web + React Native session managers, so
|
|
697
749
|
// the cross-principal refresh guard (#220 / #221) lives in ONE place and the two
|
|
698
750
|
// managers can't drift apart. Base64url decoding differs by platform (browser
|
|
@@ -774,6 +826,63 @@ function notifySessionCleared() {
|
|
|
774
826
|
}
|
|
775
827
|
}
|
|
776
828
|
|
|
829
|
+
// Same-tab session change signal.
|
|
830
|
+
//
|
|
831
|
+
// A page can run more than one copy of the SDK: the app's own bundle and the hosted widget
|
|
832
|
+
// bounded-router injects (which bundles its own, pinned copy). They share one session record in
|
|
833
|
+
// localStorage, keyed by issuer and app id, but the browser fires `storage` events only in OTHER
|
|
834
|
+
// tabs, so a copy that signs in never tells the other copy in the same tab. Until a reload, the
|
|
835
|
+
// app stayed signed out after a widget sign-in and the widget stayed signed out after an app
|
|
836
|
+
// sign-in (observed on staging, 2026-10-07).
|
|
837
|
+
//
|
|
838
|
+
// This announces "the record changed" on a BroadcastChannel, which reaches every other channel
|
|
839
|
+
// on the origin, same tab included, and never the announcing instance itself. The message is a
|
|
840
|
+
// nudge, not an authority: it carries no token and no identity, and a listener only re-reads the
|
|
841
|
+
// record through its ordinary restore path. Nothing signs in or out because of the message alone.
|
|
842
|
+
const SESSION_SIGNAL_CHANNEL = "bounded.session";
|
|
843
|
+
let channel;
|
|
844
|
+
function openChannel() {
|
|
845
|
+
var _a;
|
|
846
|
+
if (channel !== undefined)
|
|
847
|
+
return channel;
|
|
848
|
+
try {
|
|
849
|
+
channel = typeof BroadcastChannel === "function" ? new BroadcastChannel(SESSION_SIGNAL_CHANNEL) : null;
|
|
850
|
+
// Node's BroadcastChannel holds the event loop open until closed; a Node consumer (tests,
|
|
851
|
+
// tarobase-server) must not hang on exit because of a nudge channel. Browsers have no unref.
|
|
852
|
+
(_a = channel === null || channel === void 0 ? void 0 : channel.unref) === null || _a === void 0 ? void 0 : _a.call(channel);
|
|
853
|
+
}
|
|
854
|
+
catch (_b) {
|
|
855
|
+
channel = null;
|
|
856
|
+
}
|
|
857
|
+
return channel;
|
|
858
|
+
}
|
|
859
|
+
/** Tell the other SDK copies on this origin that the session record under `key` changed. */
|
|
860
|
+
function announceSessionChange(key) {
|
|
861
|
+
const ch = openChannel();
|
|
862
|
+
if (!ch)
|
|
863
|
+
return;
|
|
864
|
+
try {
|
|
865
|
+
ch.postMessage({ type: "session-changed", key });
|
|
866
|
+
}
|
|
867
|
+
catch (_a) {
|
|
868
|
+
// A closed or unavailable channel loses the nudge; the record itself is unaffected.
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/** Run `listener` when another SDK copy announces a session change. Returns the unsubscribe. */
|
|
872
|
+
function onSessionChange(listener) {
|
|
873
|
+
const ch = openChannel();
|
|
874
|
+
if (!ch)
|
|
875
|
+
return () => { };
|
|
876
|
+
const handler = (event) => {
|
|
877
|
+
const data = event.data;
|
|
878
|
+
if (!data || data.type !== "session-changed" || typeof data.key !== "string")
|
|
879
|
+
return;
|
|
880
|
+
listener(data.key);
|
|
881
|
+
};
|
|
882
|
+
ch.addEventListener("message", handler);
|
|
883
|
+
return () => ch.removeEventListener("message", handler);
|
|
884
|
+
}
|
|
885
|
+
|
|
777
886
|
/**
|
|
778
887
|
* The one protocol every session-storage mutation goes through.
|
|
779
888
|
*
|
|
@@ -970,6 +1079,7 @@ class WebSessionManager {
|
|
|
970
1079
|
}
|
|
971
1080
|
static writeRaw(sessionObj) {
|
|
972
1081
|
localStorage.setItem(this.BOUNDED_SESSION_STORAGE_KEY, JSON.stringify(sessionObj));
|
|
1082
|
+
announceSessionChange(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
973
1083
|
}
|
|
974
1084
|
/**
|
|
975
1085
|
* Read the session, stamping a `sessionGeneration` if it predates them.
|
|
@@ -1066,6 +1176,7 @@ class WebSessionManager {
|
|
|
1066
1176
|
idToken,
|
|
1067
1177
|
refreshToken, appId: currentAppId, sessionGeneration: generation }, (sessionIssuer ? { issuer: sessionIssuer } : {}))));
|
|
1068
1178
|
});
|
|
1179
|
+
announceSessionChange(storageKey);
|
|
1069
1180
|
// Adoption can fail after persistence (for example, logout while the
|
|
1070
1181
|
// provider is loading). Remove only this incomplete login, even if the
|
|
1071
1182
|
// caller has since switched app configuration or another tab signed in.
|
|
@@ -1077,6 +1188,8 @@ class WebSessionManager {
|
|
|
1077
1188
|
localStorage.removeItem(storageKey);
|
|
1078
1189
|
return true;
|
|
1079
1190
|
});
|
|
1191
|
+
if (removed)
|
|
1192
|
+
announceSessionChange(storageKey);
|
|
1080
1193
|
if (removed)
|
|
1081
1194
|
notifySessionCleared();
|
|
1082
1195
|
};
|
|
@@ -1206,8 +1319,10 @@ class WebSessionManager {
|
|
|
1206
1319
|
});
|
|
1207
1320
|
// Only announce a clear that actually happened, so a no-op cannot sign out
|
|
1208
1321
|
// a UI that was already signed out.
|
|
1209
|
-
if (removed)
|
|
1322
|
+
if (removed) {
|
|
1210
1323
|
notifySessionCleared();
|
|
1324
|
+
announceSessionChange(key);
|
|
1325
|
+
}
|
|
1211
1326
|
}
|
|
1212
1327
|
/**
|
|
1213
1328
|
* Remove the session only if it is still the one identified by `generation`.
|
|
@@ -1232,8 +1347,10 @@ class WebSessionManager {
|
|
|
1232
1347
|
localStorage.removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
1233
1348
|
return true;
|
|
1234
1349
|
});
|
|
1235
|
-
if (removed)
|
|
1350
|
+
if (removed) {
|
|
1236
1351
|
notifySessionCleared();
|
|
1352
|
+
announceSessionChange(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
1353
|
+
}
|
|
1237
1354
|
}
|
|
1238
1355
|
/* ------------------------------------------------------------------ */
|
|
1239
1356
|
/* IS-AUTH */
|
|
@@ -4190,7 +4307,7 @@ class ServerSessionManager {
|
|
|
4190
4307
|
const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
|
|
4191
4308
|
/* fetch nonce from auth API (bound to the snapshotted app+issuer) */
|
|
4192
4309
|
const nonce = await genAuthNonce(sessionConfig);
|
|
4193
|
-
const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-
|
|
4310
|
+
const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-CXuLc7NX.js'); });
|
|
4194
4311
|
const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
|
|
4195
4312
|
/* sign the message */
|
|
4196
4313
|
const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
|
|
@@ -4539,6 +4656,16 @@ function classifyToken(token, nowMs = Date.now(), skewMs = TOKEN_EXPIRY_SKEW_MS)
|
|
|
4539
4656
|
return 'expiring-soon';
|
|
4540
4657
|
return 'fresh';
|
|
4541
4658
|
}
|
|
4659
|
+
function tokenExpSeconds(token) {
|
|
4660
|
+
var _a;
|
|
4661
|
+
try {
|
|
4662
|
+
const exp = (_a = JSON.parse(decodeBase64Url(token.split('.')[1]))) === null || _a === void 0 ? void 0 : _a.exp;
|
|
4663
|
+
return typeof exp === 'number' ? exp : undefined;
|
|
4664
|
+
}
|
|
4665
|
+
catch (_b) {
|
|
4666
|
+
return undefined;
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4542
4669
|
/** Short, non-secret fingerprint used only to key the in-flight map. Never logged. */
|
|
4543
4670
|
function tokenFingerprint(token) {
|
|
4544
4671
|
let h = 5381;
|
|
@@ -4667,6 +4794,12 @@ async function getFreshAuthToken(isServer = false, options = {}) {
|
|
|
4667
4794
|
// it is valid right now, so use it rather than failing the request.
|
|
4668
4795
|
if (stillUsable)
|
|
4669
4796
|
return idToken;
|
|
4797
|
+
// ...but not forever: past its expiry plus the same grace the restore path
|
|
4798
|
+
// allows, a session that still cannot refresh is dropped, so an open tab stops
|
|
4799
|
+
// asking an issuer that will never answer it.
|
|
4800
|
+
if (!mayRetainAfterTransportFailure(tokenExpSeconds(idToken))) {
|
|
4801
|
+
await clearDeadSession(isServer, generation);
|
|
4802
|
+
}
|
|
4670
4803
|
throw makeAuthExpiredError('Authentication expired and refresh failed', status);
|
|
4671
4804
|
}
|
|
4672
4805
|
finally {
|
|
@@ -7533,7 +7666,7 @@ async function doReconnectWithNewAuth() {
|
|
|
7533
7666
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
7534
7667
|
}
|
|
7535
7668
|
try {
|
|
7536
|
-
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-
|
|
7669
|
+
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-IPPwUhJc.js'); });
|
|
7537
7670
|
await reconnectRealtimeStoreWithNewAuth();
|
|
7538
7671
|
}
|
|
7539
7672
|
catch (error) {
|
|
@@ -8911,7 +9044,7 @@ async function set(path, document, options) {
|
|
|
8911
9044
|
* state appears only under `observation`/`mirrorSync` once actually observed.
|
|
8912
9045
|
*/
|
|
8913
9046
|
async function setMany(many, options) {
|
|
8914
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
9047
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
8915
9048
|
// Returns the data that was set, or undefined if the document was already set.
|
|
8916
9049
|
try {
|
|
8917
9050
|
// A server WalletClient threads its immutable config snapshot; the default
|
|
@@ -9025,13 +9158,15 @@ async function setMany(many, options) {
|
|
|
9025
9158
|
throw new Error('The write response carried no execution-chain tag, so this receipt could not state where the write ran. ' +
|
|
9026
9159
|
'Nothing was signed or submitted.');
|
|
9027
9160
|
}
|
|
9161
|
+
const grantLane = (_l = options === null || options === void 0 ? void 0 : options._overrides) === null || _l === void 0 ? void 0 : _l._grantLane;
|
|
9162
|
+
grantLane === null || grantLane === void 0 ? void 0 : grantLane.assertLane(workerChain);
|
|
9028
9163
|
if (workerChain === 'realtime_offchain') {
|
|
9029
9164
|
// Poofnet: the wallet signs the worker's unsendable approval message and
|
|
9030
9165
|
// the worker verifies and commits it. Nothing is submitted to any chain.
|
|
9031
9166
|
if (typeof curTx.serializedTransaction !== 'string' || typeof curTx.poofnetIntentId !== 'string') {
|
|
9032
9167
|
throw new Error('Unsupported 202 transaction shape: a poofnet write needs serializedTransaction and poofnetIntentId');
|
|
9033
9168
|
}
|
|
9034
|
-
const { signPoofnetTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-
|
|
9169
|
+
const { signPoofnetTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-CghJe0_7.js'); });
|
|
9035
9170
|
const signedTransaction = await signPoofnetTransaction(curTx.serializedTransaction, authProvider, options, buildExpectedWriteIntent(documents));
|
|
9036
9171
|
// Signing-only, exactly as on a real chain: signed and NOT committed.
|
|
9037
9172
|
if ((options === null || options === void 0 ? void 0 : options.shouldSubmitTx) === false) {
|
|
@@ -9041,12 +9176,25 @@ async function setMany(many, options) {
|
|
|
9041
9176
|
poofnetIntentId: curTx.poofnetIntentId,
|
|
9042
9177
|
};
|
|
9043
9178
|
}
|
|
9044
|
-
//
|
|
9045
|
-
//
|
|
9179
|
+
// A grant lane's first credential is spent by the PUT that built this approval, so the
|
|
9180
|
+
// commit's is minted now, before `sending`: failing to mint sends nothing.
|
|
9181
|
+
const commitHeaders = await (grantLane === null || grantLane === void 0 ? void 0 : grantLane.commitHeaders());
|
|
9182
|
+
// The commit PUT is the one ambiguous step of this lane: a lost response may follow a
|
|
9183
|
+
// commit, so a transport failure or a 5xx stays untagged (read the path back). A 4xx is
|
|
9184
|
+
// realtime stating it committed nothing (the intent expired or was taken, the signer did
|
|
9185
|
+
// not match, a rule refused it), as for the first PUT. 408 and 499 are a proxy's words,
|
|
9186
|
+
// not realtime's, and may follow a commit, so they stay ambiguous too.
|
|
9046
9187
|
sending = true;
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9188
|
+
let commitResponse;
|
|
9189
|
+
try {
|
|
9190
|
+
commitResponse = await makeApiRequest('PUT', 'items/signed', { intentId: curTx.poofnetIntentId, signedTransaction }, commitHeaders ? Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options._overrides), { _getAuthHeaders: async () => commitHeaders }) : options === null || options === void 0 ? void 0 : options._overrides);
|
|
9191
|
+
if (commitResponse.status !== 200) {
|
|
9192
|
+
throw Object.assign(new Error(`Poofnet signed write was not committed (status ${commitResponse.status})`), { statusCode: commitResponse.status });
|
|
9193
|
+
}
|
|
9194
|
+
}
|
|
9195
|
+
catch (error) {
|
|
9196
|
+
const status = Number(error === null || error === void 0 ? void 0 : error.statusCode);
|
|
9197
|
+
throw status >= 400 && status < 500 && status !== 408 && status !== 499 ? markWriteNotApplied(error, 'broadcast_rejected') : error;
|
|
9050
9198
|
}
|
|
9051
9199
|
return directCommitResult(commitResponse, documents);
|
|
9052
9200
|
}
|
|
@@ -9056,7 +9204,7 @@ async function setMany(many, options) {
|
|
|
9056
9204
|
'Nothing was signed or submitted.');
|
|
9057
9205
|
}
|
|
9058
9206
|
const solanaChain = workerChain;
|
|
9059
|
-
const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-
|
|
9207
|
+
const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-CghJe0_7.js'); });
|
|
9060
9208
|
// OA-0001: derive the expected intent from OUR OWN request (the one
|
|
9061
9209
|
// ground truth a tampered builder response cannot change) so the
|
|
9062
9210
|
// validator can reject any transaction whose operation semantics
|
|
@@ -9095,7 +9243,9 @@ async function setMany(many, options) {
|
|
|
9095
9243
|
}
|
|
9096
9244
|
// Confirmed: the receipt is already complete, so the mirror read is a
|
|
9097
9245
|
// best-effort enrichment reported in `mirrorSync`, never a rejection.
|
|
9098
|
-
const mirrorSync =
|
|
9246
|
+
const mirrorSync = grantLane
|
|
9247
|
+
? { observed: false, reason: 'not attempted: a venue grant authorizes only the write' }
|
|
9248
|
+
: await attemptMirrorSync(requestedPaths, options);
|
|
9099
9249
|
return {
|
|
9100
9250
|
status: 'confirmed', appId, requestedDocuments, chain: solanaChain,
|
|
9101
9251
|
transactionId: solanaOutcome.transactionSignature,
|
|
@@ -10118,6 +10268,84 @@ function createAppTokenAuthHeaders(options) {
|
|
|
10118
10268
|
};
|
|
10119
10269
|
}
|
|
10120
10270
|
|
|
10271
|
+
/**
|
|
10272
|
+
* The venue grant write lane: create ONE document in another app (an OpenApps venue) as the
|
|
10273
|
+
* wallet signed in here, authorized by a single-use grant instead of a venue session
|
|
10274
|
+
* (VENUE-GRANT-DESIGN.md). The grant only lets the venue build that one transaction for that
|
|
10275
|
+
* wallet; the wallet's own signature, checked by the venue's own rule, is the authority.
|
|
10276
|
+
*
|
|
10277
|
+
* The grant itself is minted by Bounded Auth at `POST {humanAuthApiUrl}/app-token/venue-grant`
|
|
10278
|
+
* (contract: packages/cdk/cloudflare/shared/venue-grant.ts, which this package cannot import
|
|
10279
|
+
* because the SDK packages publish separately). This module only spends one.
|
|
10280
|
+
*
|
|
10281
|
+
* It reuses `setMany`'s request and 202 handling through `_overrides`: the `_config` snapshot is
|
|
10282
|
+
* this app's own config re-pointed at the VENUE app id, so the pre-sign validator (OA-0001) binds
|
|
10283
|
+
* the venue app id and the full expected intent, and the signer stays this app's own AuthProvider
|
|
10284
|
+
* (a Turnkey session or a Wallet Standard wallet). Nothing here is reachable from `createClient()`,
|
|
10285
|
+
* which stays read-only.
|
|
10286
|
+
*
|
|
10287
|
+
* The chain a write runs on is the VENUE's, stated by its 202, never the app's: the app's own
|
|
10288
|
+
* `chain` can only agree with it or be refused, and the transaction is broadcast through the app's
|
|
10289
|
+
* `rpcUrl` only when the app configured that same chain, otherwise through `resolveRpcUrl(chain)`.
|
|
10290
|
+
*/
|
|
10291
|
+
function laneError(code, message) {
|
|
10292
|
+
return Object.assign(new Error(message), { code });
|
|
10293
|
+
}
|
|
10294
|
+
const isSolanaLane = (chain) => chain === 'solana_devnet' || chain === 'solana_mainnet';
|
|
10295
|
+
/**
|
|
10296
|
+
* The RPC a venue write on Solana lane `chain` is broadcast through, and a saved receipt of it is
|
|
10297
|
+
* reconciled through. The app's own `rpcUrl` counts only when the app also configured this chain
|
|
10298
|
+
* (an `rpcUrl` alone does not say which chain it speaks); otherwise `resolveRpcUrl(chain)`. Null
|
|
10299
|
+
* when neither yields one.
|
|
10300
|
+
*
|
|
10301
|
+
* @internal Shared by the lane and by the client SDK's reconciliation of a saved receipt.
|
|
10302
|
+
*/
|
|
10303
|
+
function resolveVenueRpcUrl(config, chain, resolveRpcUrl) {
|
|
10304
|
+
var _a, _b;
|
|
10305
|
+
const own = requiredSolanaChain(config) === chain ? (_a = config.rpcUrl) === null || _a === void 0 ? void 0 : _a.trim() : '';
|
|
10306
|
+
return own || ((_b = resolveRpcUrl === null || resolveRpcUrl === void 0 ? void 0 : resolveRpcUrl(chain)) === null || _b === void 0 ? void 0 : _b.trim()) || null;
|
|
10307
|
+
}
|
|
10308
|
+
/**
|
|
10309
|
+
* Refuse, before the wallet is asked to sign, a 202 whose execution lane this app is not
|
|
10310
|
+
* configured for: a mainnet app must never sign a devnet or poofnet approval (a poofnet "funding"
|
|
10311
|
+
* moves no real USDC). Then point the Solana lane's broadcast at the RPC for the venue's chain.
|
|
10312
|
+
* Poofnet broadcasts nothing, so it needs no RPC.
|
|
10313
|
+
*/
|
|
10314
|
+
function assertExecutionLane(config, chain, options) {
|
|
10315
|
+
const required = requiredSolanaChain(config);
|
|
10316
|
+
if (required && chain !== required) {
|
|
10317
|
+
throw laneError('chain_mismatch', `The venue answered on "${chain}" but this app is configured for "${config.chain}". Nothing was signed.`);
|
|
10318
|
+
}
|
|
10319
|
+
if (!isSolanaLane(chain))
|
|
10320
|
+
return;
|
|
10321
|
+
const rpcUrl = resolveVenueRpcUrl(config, chain, options.resolveRpcUrl);
|
|
10322
|
+
if (rpcUrl)
|
|
10323
|
+
config.rpcUrl = rpcUrl;
|
|
10324
|
+
else if (!options._solanaRpc) {
|
|
10325
|
+
throw laneError('chain_not_configured', `The venue runs on ${chain}, but no RPC could be resolved to broadcast through. ` +
|
|
10326
|
+
`Pass chain: '${chain}' and an rpcUrl to init(). Nothing was signed.`);
|
|
10327
|
+
}
|
|
10328
|
+
}
|
|
10329
|
+
/**
|
|
10330
|
+
* Write `document` at `path` in the venue `venueAppId` with a single-use grant. Resolves to the
|
|
10331
|
+
* same discriminated {@link SetResult} as `set`, and throws exactly as `set` does, so the same
|
|
10332
|
+
* `isWriteNotApplied` reading applies.
|
|
10333
|
+
*
|
|
10334
|
+
* @internal Used by `fundVenueEscrow` and `disputeVenueEscrow` in @bounded-sh/client. Not a stable API.
|
|
10335
|
+
*/
|
|
10336
|
+
async function setWithVenueGrant(path, document, options) {
|
|
10337
|
+
// A private copy of this app's config for this one write: the lane fills in its RPC below.
|
|
10338
|
+
const config = Object.assign(Object.assign({}, (await getConfig())), { appId: options.venueAppId });
|
|
10339
|
+
const headers = (grant) => ({ Authorization: `Bearer ${grant}` });
|
|
10340
|
+
return setMany([{ path, document }], {
|
|
10341
|
+
onBroadcast: options.onBroadcast,
|
|
10342
|
+
_overrides: Object.assign({ _config: config, _getAuthHeaders: async () => headers(options.grant), _grantLane: {
|
|
10343
|
+
assertLane: (chain) => assertExecutionLane(config, chain, options),
|
|
10344
|
+
commitHeaders: async () => headers(await options.commitGrant()),
|
|
10345
|
+
} }, (options._solanaRpc ? { _solanaRpc: options._solanaRpc } : {})),
|
|
10346
|
+
});
|
|
10347
|
+
}
|
|
10348
|
+
|
|
10121
10349
|
/**
|
|
10122
10350
|
* Live named-query subscriptions (subscribeQuery) - HTTP client poller (v1).
|
|
10123
10351
|
*
|
|
@@ -10820,7 +11048,7 @@ function toMillis(seconds) {
|
|
|
10820
11048
|
|
|
10821
11049
|
/** SIWS message creation is async already, so its onchain module can be lazy. */
|
|
10822
11050
|
async function genSolanaMessage(address, nonce) {
|
|
10823
|
-
const implementation = await Promise.resolve().then(function () { return require('./solana-message-
|
|
11051
|
+
const implementation = await Promise.resolve().then(function () { return require('./solana-message-CXuLc7NX.js'); });
|
|
10824
11052
|
return implementation.genSolanaMessage(address, nonce);
|
|
10825
11053
|
}
|
|
10826
11054
|
|
|
@@ -11382,6 +11610,7 @@ exports.LiveIntentError = LiveIntentError;
|
|
|
11382
11610
|
exports.MIN_QUERY_POLL_INTERVAL_MS = MIN_QUERY_POLL_INTERVAL_MS;
|
|
11383
11611
|
exports.RATE_LIMIT_EVENT_NAME = RATE_LIMIT_EVENT_NAME;
|
|
11384
11612
|
exports.ReactNativeSessionManager = ReactNativeSessionManager;
|
|
11613
|
+
exports.SESSION_SIGNAL_CHANNEL = SESSION_SIGNAL_CHANNEL;
|
|
11385
11614
|
exports.SOLANA_SIGN_IN_STATEMENT = SOLANA_SIGN_IN_STATEMENT;
|
|
11386
11615
|
exports.ScopedClientReadOnlyError = ScopedClientReadOnlyError;
|
|
11387
11616
|
exports.ServerSessionManager = ServerSessionManager;
|
|
@@ -11389,6 +11618,7 @@ exports.SessionEnvironmentError = SessionEnvironmentError;
|
|
|
11389
11618
|
exports.UNSUPPORTED_ENVIRONMENT_CODE = UNSUPPORTED_ENVIRONMENT_CODE;
|
|
11390
11619
|
exports.WebSessionManager = WebSessionManager;
|
|
11391
11620
|
exports.aggregate = aggregate;
|
|
11621
|
+
exports.announceSessionChange = announceSessionChange;
|
|
11392
11622
|
exports.browserSignInDomain = browserSignInDomain;
|
|
11393
11623
|
exports.bufferExports = bufferExports;
|
|
11394
11624
|
exports.canonicalizeArgs = canonicalizeArgs;
|
|
@@ -11439,11 +11669,13 @@ exports.live = live;
|
|
|
11439
11669
|
exports.markWriteNotApplied = markWriteNotApplied;
|
|
11440
11670
|
exports.normalizeEvmAddress = normalizeEvmAddress;
|
|
11441
11671
|
exports.now = now;
|
|
11672
|
+
exports.onSessionChange = onSessionChange;
|
|
11442
11673
|
exports.onSessionCleared = onSessionCleared;
|
|
11443
11674
|
exports.queryAggregate = queryAggregate;
|
|
11444
11675
|
exports.reconcileSetResult = reconcileSetResult;
|
|
11445
11676
|
exports.reconnectWithNewAuth = reconnectWithNewAuth;
|
|
11446
11677
|
exports.refreshSession = refreshSession;
|
|
11678
|
+
exports.resolveVenueRpcUrl = resolveVenueRpcUrl;
|
|
11447
11679
|
exports.revokeSession = revokeSession;
|
|
11448
11680
|
exports.runExpression = runExpression;
|
|
11449
11681
|
exports.runExpressionMany = runExpressionMany;
|
|
@@ -11454,6 +11686,7 @@ exports.serverTimestamp = serverTimestamp;
|
|
|
11454
11686
|
exports.set = set;
|
|
11455
11687
|
exports.setFile = setFile;
|
|
11456
11688
|
exports.setMany = setMany;
|
|
11689
|
+
exports.setWithVenueGrant = setWithVenueGrant;
|
|
11457
11690
|
exports.signAndSubmitTransaction = signAndSubmitTransaction;
|
|
11458
11691
|
exports.signMessage = signMessage;
|
|
11459
11692
|
exports.signSessionCreateMessage = signSessionCreateMessage;
|
|
@@ -11468,4 +11701,4 @@ exports.toSeconds = toSeconds;
|
|
|
11468
11701
|
exports.waitForSetResult = waitForSetResult;
|
|
11469
11702
|
exports.withEffects = withEffects;
|
|
11470
11703
|
exports.writeNotAppliedPhase = writeNotAppliedPhase;
|
|
11471
|
-
//# sourceMappingURL=index
|
|
11704
|
+
//# sourceMappingURL=index-CjiaT9AT.js.map
|