@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
|
@@ -419,6 +419,97 @@ axiosRetry.exponentialDelay = exponentialDelay;
|
|
|
419
419
|
axiosRetry.linearDelay = linearDelay;
|
|
420
420
|
axiosRetry.isRetryableError = isRetryableError;
|
|
421
421
|
|
|
422
|
+
// Classify a session-refresh failure as a DEFINITIVE rejection of the credential
|
|
423
|
+
// vs a TRANSPORT failure (DS4-0054).
|
|
424
|
+
//
|
|
425
|
+
// The restore path refreshes an expired token at boot. The old code destroyed the
|
|
426
|
+
// local session on ANY refresh failure, so a network blip - common on mobile -
|
|
427
|
+
// signed the user out. Only a real HTTP rejection of the refresh token proves the
|
|
428
|
+
// session is invalid; anything else is unknown, and the safe move is to RETAIN local
|
|
429
|
+
// state and retry. Keeping a genuinely dead session locally is harmless: every
|
|
430
|
+
// request still fails closed server-side, and the next refresh that reaches the
|
|
431
|
+
// server clears it. Destroying a live session on a blip is user-visible damage.
|
|
432
|
+
//
|
|
433
|
+
// `/session/refresh` answers 401 (session_revoked / invalid_grant) when the token is
|
|
434
|
+
// dead. 400 (bad request), 408/429 (transient), and 5xx are NOT proof the session is
|
|
435
|
+
// invalid, and a network/DNS/timeout error carries no HTTP response at all.
|
|
436
|
+
//
|
|
437
|
+
// This is the single classifier for every refresh path: the per-request freshness
|
|
438
|
+
// check (auth-token-freshness) and the boot-time restore paths (web + RN session
|
|
439
|
+
// managers) all import it, so "definitive" means the same thing everywhere. It lives
|
|
440
|
+
// in this leaf module to avoid an import cycle between those consumers.
|
|
441
|
+
function isDefinitiveRejection(err) {
|
|
442
|
+
var _a;
|
|
443
|
+
const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
444
|
+
return status === 401 || status === 403;
|
|
445
|
+
}
|
|
446
|
+
// How long past the id token's own `exp` a session may survive purely on
|
|
447
|
+
// transport failures.
|
|
448
|
+
//
|
|
449
|
+
// "Retain on a blip" must not become "retain forever". A statusless failure is
|
|
450
|
+
// unknown, so the first ones are absorbed - but a revoked session, or a gateway
|
|
451
|
+
// that rewrites the issuer's 401 into a 502, would otherwise keep
|
|
452
|
+
// isAuthenticated() reporting a signed-in user for as long as the device never
|
|
453
|
+
// reaches the issuer again. One day covers an offline flight or a bad-network day
|
|
454
|
+
// while making the retention finite: past it, an unrefreshable session is cleared
|
|
455
|
+
// and the user is asked to sign in again.
|
|
456
|
+
const TRANSPORT_RETENTION_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
457
|
+
/**
|
|
458
|
+
* May a session whose refresh failed WITHOUT a definitive rejection be kept?
|
|
459
|
+
*
|
|
460
|
+
* `expSeconds` is the id token's `exp` claim. Fails closed: a missing or
|
|
461
|
+
* non-numeric expiry is not a usable bound, so such a session is never retained.
|
|
462
|
+
*/
|
|
463
|
+
function mayRetainAfterTransportFailure(expSeconds, now = Date.now()) {
|
|
464
|
+
if (typeof expSeconds !== 'number' || !Number.isFinite(expSeconds))
|
|
465
|
+
return false;
|
|
466
|
+
return now <= expSeconds * 1000 + TRANSPORT_RETENTION_GRACE_MS;
|
|
467
|
+
}
|
|
468
|
+
// Backoff between refresh attempts that failed WITHOUT a definitive rejection.
|
|
469
|
+
//
|
|
470
|
+
// A transport failure retains the session (above), and every authenticated request
|
|
471
|
+
// and the realtime token timer asks for a fresh token - so without a pause, an issuer
|
|
472
|
+
// that answers 503 or 429 gets one refresh per request from every open tab, which is
|
|
473
|
+
// the exact load that keeps it down. After such a failure the same refresh token is
|
|
474
|
+
// not sent again until the backoff passes: 15s, doubling, capped at 10 minutes, or the
|
|
475
|
+
// issuer's own Retry-After when it is longer. Callers inside the window get a
|
|
476
|
+
// non-definitive error at once (`code: 'refresh_backoff'`) and treat it like the
|
|
477
|
+
// failure that started it. A success, or a different token, starts clean.
|
|
478
|
+
const REFRESH_BACKOFF_BASE_MS = 15000;
|
|
479
|
+
const REFRESH_BACKOFF_MAX_MS = 10 * 60000;
|
|
480
|
+
const refreshBackoff = new Map();
|
|
481
|
+
function retryAfterMs(err) {
|
|
482
|
+
var _a, _b;
|
|
483
|
+
const headers = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.headers;
|
|
484
|
+
const raw = headers && ((_b = headers['retry-after']) !== null && _b !== void 0 ? _b : headers['Retry-After']);
|
|
485
|
+
const seconds = Number(raw);
|
|
486
|
+
return Number.isFinite(seconds) && seconds > 0 ? Math.min(seconds * 1000, REFRESH_BACKOFF_MAX_MS) : 0;
|
|
487
|
+
}
|
|
488
|
+
/** Throws a non-definitive `refresh_backoff` error while `key` is still backing off. */
|
|
489
|
+
function assertRefreshAllowed(key, now = Date.now()) {
|
|
490
|
+
const entry = refreshBackoff.get(key);
|
|
491
|
+
if (!entry || now >= entry.until)
|
|
492
|
+
return;
|
|
493
|
+
const err = new Error('Session refresh is backing off after a failed attempt');
|
|
494
|
+
err.code = 'refresh_backoff';
|
|
495
|
+
err.retryAt = entry.until;
|
|
496
|
+
throw err;
|
|
497
|
+
}
|
|
498
|
+
/** Record the outcome of a refresh attempt for `key`. */
|
|
499
|
+
function recordRefreshOutcome(key, err, now = Date.now()) {
|
|
500
|
+
var _a, _b;
|
|
501
|
+
if (!err || isDefinitiveRejection(err)) {
|
|
502
|
+
refreshBackoff.delete(key);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const failures = ((_b = (_a = refreshBackoff.get(key)) === null || _a === void 0 ? void 0 : _a.failures) !== null && _b !== void 0 ? _b : 0) + 1;
|
|
506
|
+
const backoff = Math.min(REFRESH_BACKOFF_BASE_MS * 2 ** (failures - 1), REFRESH_BACKOFF_MAX_MS);
|
|
507
|
+
refreshBackoff.set(key, { failures, until: now + Math.max(backoff, retryAfterMs(err)) });
|
|
508
|
+
// One entry per refresh token; a long-lived tab rotates through a handful at most.
|
|
509
|
+
if (refreshBackoff.size > 32)
|
|
510
|
+
refreshBackoff.delete(refreshBackoff.keys().next().value);
|
|
511
|
+
}
|
|
512
|
+
|
|
422
513
|
/**
|
|
423
514
|
* Resolve the refresh/revoke base URL to a CONFIGURED issuer only. The session's
|
|
424
515
|
* `issuer` is persisted in client storage; if that storage is tampered (e.g. XSS)
|
|
@@ -572,6 +663,9 @@ async function refreshSession(refreshToken, issuer) {
|
|
|
572
663
|
const existing = refreshInFlightByToken.get(dedupeKey);
|
|
573
664
|
if (existing)
|
|
574
665
|
return existing;
|
|
666
|
+
// Inside a backoff window this throws at once, so a failing issuer is not asked again
|
|
667
|
+
// by every request and timer in the page (see refresh-error.ts).
|
|
668
|
+
assertRefreshAllowed(dedupeKey);
|
|
575
669
|
let currentFlight;
|
|
576
670
|
currentFlight = (async () => {
|
|
577
671
|
try {
|
|
@@ -585,8 +679,13 @@ async function refreshSession(refreshToken, issuer) {
|
|
|
585
679
|
refreshToken,
|
|
586
680
|
appId
|
|
587
681
|
}, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
|
|
682
|
+
recordRefreshOutcome(dedupeKey, null);
|
|
588
683
|
return response.data;
|
|
589
684
|
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
recordRefreshOutcome(dedupeKey, error);
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
590
689
|
finally {
|
|
591
690
|
// Only remove our own entry, and only if it is still the active flight
|
|
592
691
|
// for this token (a later refresh of the same token cannot have replaced
|
|
@@ -644,53 +743,6 @@ async function revokeSession(refreshToken, issuer) {
|
|
|
644
743
|
async function signSessionCreateMessage(_signMessageFunction) {
|
|
645
744
|
}
|
|
646
745
|
|
|
647
|
-
// Classify a session-refresh failure as a DEFINITIVE rejection of the credential
|
|
648
|
-
// vs a TRANSPORT failure (DS4-0054).
|
|
649
|
-
//
|
|
650
|
-
// The restore path refreshes an expired token at boot. The old code destroyed the
|
|
651
|
-
// local session on ANY refresh failure, so a network blip - common on mobile -
|
|
652
|
-
// signed the user out. Only a real HTTP rejection of the refresh token proves the
|
|
653
|
-
// session is invalid; anything else is unknown, and the safe move is to RETAIN local
|
|
654
|
-
// state and retry. Keeping a genuinely dead session locally is harmless: every
|
|
655
|
-
// request still fails closed server-side, and the next refresh that reaches the
|
|
656
|
-
// server clears it. Destroying a live session on a blip is user-visible damage.
|
|
657
|
-
//
|
|
658
|
-
// `/session/refresh` answers 401 (session_revoked / invalid_grant) when the token is
|
|
659
|
-
// dead. 400 (bad request), 408/429 (transient), and 5xx are NOT proof the session is
|
|
660
|
-
// invalid, and a network/DNS/timeout error carries no HTTP response at all.
|
|
661
|
-
//
|
|
662
|
-
// This is the single classifier for every refresh path: the per-request freshness
|
|
663
|
-
// check (auth-token-freshness) and the boot-time restore paths (web + RN session
|
|
664
|
-
// managers) all import it, so "definitive" means the same thing everywhere. It lives
|
|
665
|
-
// in this leaf module to avoid an import cycle between those consumers.
|
|
666
|
-
function isDefinitiveRejection(err) {
|
|
667
|
-
var _a;
|
|
668
|
-
const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
669
|
-
return status === 401 || status === 403;
|
|
670
|
-
}
|
|
671
|
-
// How long past the id token's own `exp` a session may survive purely on
|
|
672
|
-
// transport failures.
|
|
673
|
-
//
|
|
674
|
-
// "Retain on a blip" must not become "retain forever". A statusless failure is
|
|
675
|
-
// unknown, so the first ones are absorbed - but a revoked session, or a gateway
|
|
676
|
-
// that rewrites the issuer's 401 into a 502, would otherwise keep
|
|
677
|
-
// isAuthenticated() reporting a signed-in user for as long as the device never
|
|
678
|
-
// reaches the issuer again. One day covers an offline flight or a bad-network day
|
|
679
|
-
// while making the retention finite: past it, an unrefreshable session is cleared
|
|
680
|
-
// and the user is asked to sign in again.
|
|
681
|
-
const TRANSPORT_RETENTION_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
682
|
-
/**
|
|
683
|
-
* May a session whose refresh failed WITHOUT a definitive rejection be kept?
|
|
684
|
-
*
|
|
685
|
-
* `expSeconds` is the id token's `exp` claim. Fails closed: a missing or
|
|
686
|
-
* non-numeric expiry is not a usable bound, so such a session is never retained.
|
|
687
|
-
*/
|
|
688
|
-
function mayRetainAfterTransportFailure(expSeconds, now = Date.now()) {
|
|
689
|
-
if (typeof expSeconds !== 'number' || !Number.isFinite(expSeconds))
|
|
690
|
-
return false;
|
|
691
|
-
return now <= expSeconds * 1000 + TRANSPORT_RETENTION_GRACE_MS;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
746
|
// Shared session-binding guard for the web + React Native session managers, so
|
|
695
747
|
// the cross-principal refresh guard (#220 / #221) lives in ONE place and the two
|
|
696
748
|
// managers can't drift apart. Base64url decoding differs by platform (browser
|
|
@@ -772,6 +824,63 @@ function notifySessionCleared() {
|
|
|
772
824
|
}
|
|
773
825
|
}
|
|
774
826
|
|
|
827
|
+
// Same-tab session change signal.
|
|
828
|
+
//
|
|
829
|
+
// A page can run more than one copy of the SDK: the app's own bundle and the hosted widget
|
|
830
|
+
// bounded-router injects (which bundles its own, pinned copy). They share one session record in
|
|
831
|
+
// localStorage, keyed by issuer and app id, but the browser fires `storage` events only in OTHER
|
|
832
|
+
// tabs, so a copy that signs in never tells the other copy in the same tab. Until a reload, the
|
|
833
|
+
// app stayed signed out after a widget sign-in and the widget stayed signed out after an app
|
|
834
|
+
// sign-in (observed on staging, 2026-10-07).
|
|
835
|
+
//
|
|
836
|
+
// This announces "the record changed" on a BroadcastChannel, which reaches every other channel
|
|
837
|
+
// on the origin, same tab included, and never the announcing instance itself. The message is a
|
|
838
|
+
// nudge, not an authority: it carries no token and no identity, and a listener only re-reads the
|
|
839
|
+
// record through its ordinary restore path. Nothing signs in or out because of the message alone.
|
|
840
|
+
const SESSION_SIGNAL_CHANNEL = "bounded.session";
|
|
841
|
+
let channel;
|
|
842
|
+
function openChannel() {
|
|
843
|
+
var _a;
|
|
844
|
+
if (channel !== undefined)
|
|
845
|
+
return channel;
|
|
846
|
+
try {
|
|
847
|
+
channel = typeof BroadcastChannel === "function" ? new BroadcastChannel(SESSION_SIGNAL_CHANNEL) : null;
|
|
848
|
+
// Node's BroadcastChannel holds the event loop open until closed; a Node consumer (tests,
|
|
849
|
+
// tarobase-server) must not hang on exit because of a nudge channel. Browsers have no unref.
|
|
850
|
+
(_a = channel === null || channel === void 0 ? void 0 : channel.unref) === null || _a === void 0 ? void 0 : _a.call(channel);
|
|
851
|
+
}
|
|
852
|
+
catch (_b) {
|
|
853
|
+
channel = null;
|
|
854
|
+
}
|
|
855
|
+
return channel;
|
|
856
|
+
}
|
|
857
|
+
/** Tell the other SDK copies on this origin that the session record under `key` changed. */
|
|
858
|
+
function announceSessionChange(key) {
|
|
859
|
+
const ch = openChannel();
|
|
860
|
+
if (!ch)
|
|
861
|
+
return;
|
|
862
|
+
try {
|
|
863
|
+
ch.postMessage({ type: "session-changed", key });
|
|
864
|
+
}
|
|
865
|
+
catch (_a) {
|
|
866
|
+
// A closed or unavailable channel loses the nudge; the record itself is unaffected.
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/** Run `listener` when another SDK copy announces a session change. Returns the unsubscribe. */
|
|
870
|
+
function onSessionChange(listener) {
|
|
871
|
+
const ch = openChannel();
|
|
872
|
+
if (!ch)
|
|
873
|
+
return () => { };
|
|
874
|
+
const handler = (event) => {
|
|
875
|
+
const data = event.data;
|
|
876
|
+
if (!data || data.type !== "session-changed" || typeof data.key !== "string")
|
|
877
|
+
return;
|
|
878
|
+
listener(data.key);
|
|
879
|
+
};
|
|
880
|
+
ch.addEventListener("message", handler);
|
|
881
|
+
return () => ch.removeEventListener("message", handler);
|
|
882
|
+
}
|
|
883
|
+
|
|
775
884
|
/**
|
|
776
885
|
* The one protocol every session-storage mutation goes through.
|
|
777
886
|
*
|
|
@@ -968,6 +1077,7 @@ class WebSessionManager {
|
|
|
968
1077
|
}
|
|
969
1078
|
static writeRaw(sessionObj) {
|
|
970
1079
|
localStorage.setItem(this.BOUNDED_SESSION_STORAGE_KEY, JSON.stringify(sessionObj));
|
|
1080
|
+
announceSessionChange(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
971
1081
|
}
|
|
972
1082
|
/**
|
|
973
1083
|
* Read the session, stamping a `sessionGeneration` if it predates them.
|
|
@@ -1064,6 +1174,7 @@ class WebSessionManager {
|
|
|
1064
1174
|
idToken,
|
|
1065
1175
|
refreshToken, appId: currentAppId, sessionGeneration: generation }, (sessionIssuer ? { issuer: sessionIssuer } : {}))));
|
|
1066
1176
|
});
|
|
1177
|
+
announceSessionChange(storageKey);
|
|
1067
1178
|
// Adoption can fail after persistence (for example, logout while the
|
|
1068
1179
|
// provider is loading). Remove only this incomplete login, even if the
|
|
1069
1180
|
// caller has since switched app configuration or another tab signed in.
|
|
@@ -1075,6 +1186,8 @@ class WebSessionManager {
|
|
|
1075
1186
|
localStorage.removeItem(storageKey);
|
|
1076
1187
|
return true;
|
|
1077
1188
|
});
|
|
1189
|
+
if (removed)
|
|
1190
|
+
announceSessionChange(storageKey);
|
|
1078
1191
|
if (removed)
|
|
1079
1192
|
notifySessionCleared();
|
|
1080
1193
|
};
|
|
@@ -1204,8 +1317,10 @@ class WebSessionManager {
|
|
|
1204
1317
|
});
|
|
1205
1318
|
// Only announce a clear that actually happened, so a no-op cannot sign out
|
|
1206
1319
|
// a UI that was already signed out.
|
|
1207
|
-
if (removed)
|
|
1320
|
+
if (removed) {
|
|
1208
1321
|
notifySessionCleared();
|
|
1322
|
+
announceSessionChange(key);
|
|
1323
|
+
}
|
|
1209
1324
|
}
|
|
1210
1325
|
/**
|
|
1211
1326
|
* Remove the session only if it is still the one identified by `generation`.
|
|
@@ -1230,8 +1345,10 @@ class WebSessionManager {
|
|
|
1230
1345
|
localStorage.removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
1231
1346
|
return true;
|
|
1232
1347
|
});
|
|
1233
|
-
if (removed)
|
|
1348
|
+
if (removed) {
|
|
1234
1349
|
notifySessionCleared();
|
|
1350
|
+
announceSessionChange(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
1351
|
+
}
|
|
1235
1352
|
}
|
|
1236
1353
|
/* ------------------------------------------------------------------ */
|
|
1237
1354
|
/* IS-AUTH */
|
|
@@ -4188,7 +4305,7 @@ class ServerSessionManager {
|
|
|
4188
4305
|
const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
|
|
4189
4306
|
/* fetch nonce from auth API (bound to the snapshotted app+issuer) */
|
|
4190
4307
|
const nonce = await genAuthNonce(sessionConfig);
|
|
4191
|
-
const { genSolanaMessage } = await import('./solana-message-
|
|
4308
|
+
const { genSolanaMessage } = await import('./solana-message-BO6Dtgbv.mjs');
|
|
4192
4309
|
const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
|
|
4193
4310
|
/* sign the message */
|
|
4194
4311
|
const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
|
|
@@ -4537,6 +4654,16 @@ function classifyToken(token, nowMs = Date.now(), skewMs = TOKEN_EXPIRY_SKEW_MS)
|
|
|
4537
4654
|
return 'expiring-soon';
|
|
4538
4655
|
return 'fresh';
|
|
4539
4656
|
}
|
|
4657
|
+
function tokenExpSeconds(token) {
|
|
4658
|
+
var _a;
|
|
4659
|
+
try {
|
|
4660
|
+
const exp = (_a = JSON.parse(decodeBase64Url(token.split('.')[1]))) === null || _a === void 0 ? void 0 : _a.exp;
|
|
4661
|
+
return typeof exp === 'number' ? exp : undefined;
|
|
4662
|
+
}
|
|
4663
|
+
catch (_b) {
|
|
4664
|
+
return undefined;
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4540
4667
|
/** Short, non-secret fingerprint used only to key the in-flight map. Never logged. */
|
|
4541
4668
|
function tokenFingerprint(token) {
|
|
4542
4669
|
let h = 5381;
|
|
@@ -4665,6 +4792,12 @@ async function getFreshAuthToken(isServer = false, options = {}) {
|
|
|
4665
4792
|
// it is valid right now, so use it rather than failing the request.
|
|
4666
4793
|
if (stillUsable)
|
|
4667
4794
|
return idToken;
|
|
4795
|
+
// ...but not forever: past its expiry plus the same grace the restore path
|
|
4796
|
+
// allows, a session that still cannot refresh is dropped, so an open tab stops
|
|
4797
|
+
// asking an issuer that will never answer it.
|
|
4798
|
+
if (!mayRetainAfterTransportFailure(tokenExpSeconds(idToken))) {
|
|
4799
|
+
await clearDeadSession(isServer, generation);
|
|
4800
|
+
}
|
|
4668
4801
|
throw makeAuthExpiredError('Authentication expired and refresh failed', status);
|
|
4669
4802
|
}
|
|
4670
4803
|
finally {
|
|
@@ -7531,7 +7664,7 @@ async function doReconnectWithNewAuth() {
|
|
|
7531
7664
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
7532
7665
|
}
|
|
7533
7666
|
try {
|
|
7534
|
-
const { reconnectRealtimeStoreWithNewAuth } = await import('./realtime-store-
|
|
7667
|
+
const { reconnectRealtimeStoreWithNewAuth } = await import('./realtime-store-CNs5zURZ.mjs');
|
|
7535
7668
|
await reconnectRealtimeStoreWithNewAuth();
|
|
7536
7669
|
}
|
|
7537
7670
|
catch (error) {
|
|
@@ -8909,7 +9042,7 @@ async function set(path, document, options) {
|
|
|
8909
9042
|
* state appears only under `observation`/`mirrorSync` once actually observed.
|
|
8910
9043
|
*/
|
|
8911
9044
|
async function setMany(many, options) {
|
|
8912
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
9045
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
8913
9046
|
// Returns the data that was set, or undefined if the document was already set.
|
|
8914
9047
|
try {
|
|
8915
9048
|
// A server WalletClient threads its immutable config snapshot; the default
|
|
@@ -9023,13 +9156,15 @@ async function setMany(many, options) {
|
|
|
9023
9156
|
throw new Error('The write response carried no execution-chain tag, so this receipt could not state where the write ran. ' +
|
|
9024
9157
|
'Nothing was signed or submitted.');
|
|
9025
9158
|
}
|
|
9159
|
+
const grantLane = (_l = options === null || options === void 0 ? void 0 : options._overrides) === null || _l === void 0 ? void 0 : _l._grantLane;
|
|
9160
|
+
grantLane === null || grantLane === void 0 ? void 0 : grantLane.assertLane(workerChain);
|
|
9026
9161
|
if (workerChain === 'realtime_offchain') {
|
|
9027
9162
|
// Poofnet: the wallet signs the worker's unsendable approval message and
|
|
9028
9163
|
// the worker verifies and commits it. Nothing is submitted to any chain.
|
|
9029
9164
|
if (typeof curTx.serializedTransaction !== 'string' || typeof curTx.poofnetIntentId !== 'string') {
|
|
9030
9165
|
throw new Error('Unsupported 202 transaction shape: a poofnet write needs serializedTransaction and poofnetIntentId');
|
|
9031
9166
|
}
|
|
9032
|
-
const { signPoofnetTransaction, buildExpectedWriteIntent } = await import('./solana-operations-
|
|
9167
|
+
const { signPoofnetTransaction, buildExpectedWriteIntent } = await import('./solana-operations-DDt4CzRR.mjs');
|
|
9033
9168
|
const signedTransaction = await signPoofnetTransaction(curTx.serializedTransaction, authProvider, options, buildExpectedWriteIntent(documents));
|
|
9034
9169
|
// Signing-only, exactly as on a real chain: signed and NOT committed.
|
|
9035
9170
|
if ((options === null || options === void 0 ? void 0 : options.shouldSubmitTx) === false) {
|
|
@@ -9039,12 +9174,25 @@ async function setMany(many, options) {
|
|
|
9039
9174
|
poofnetIntentId: curTx.poofnetIntentId,
|
|
9040
9175
|
};
|
|
9041
9176
|
}
|
|
9042
|
-
//
|
|
9043
|
-
//
|
|
9177
|
+
// A grant lane's first credential is spent by the PUT that built this approval, so the
|
|
9178
|
+
// commit's is minted now, before `sending`: failing to mint sends nothing.
|
|
9179
|
+
const commitHeaders = await (grantLane === null || grantLane === void 0 ? void 0 : grantLane.commitHeaders());
|
|
9180
|
+
// The commit PUT is the one ambiguous step of this lane: a lost response may follow a
|
|
9181
|
+
// commit, so a transport failure or a 5xx stays untagged (read the path back). A 4xx is
|
|
9182
|
+
// realtime stating it committed nothing (the intent expired or was taken, the signer did
|
|
9183
|
+
// not match, a rule refused it), as for the first PUT. 408 and 499 are a proxy's words,
|
|
9184
|
+
// not realtime's, and may follow a commit, so they stay ambiguous too.
|
|
9044
9185
|
sending = true;
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9186
|
+
let commitResponse;
|
|
9187
|
+
try {
|
|
9188
|
+
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);
|
|
9189
|
+
if (commitResponse.status !== 200) {
|
|
9190
|
+
throw Object.assign(new Error(`Poofnet signed write was not committed (status ${commitResponse.status})`), { statusCode: commitResponse.status });
|
|
9191
|
+
}
|
|
9192
|
+
}
|
|
9193
|
+
catch (error) {
|
|
9194
|
+
const status = Number(error === null || error === void 0 ? void 0 : error.statusCode);
|
|
9195
|
+
throw status >= 400 && status < 500 && status !== 408 && status !== 499 ? markWriteNotApplied(error, 'broadcast_rejected') : error;
|
|
9048
9196
|
}
|
|
9049
9197
|
return directCommitResult(commitResponse, documents);
|
|
9050
9198
|
}
|
|
@@ -9054,7 +9202,7 @@ async function setMany(many, options) {
|
|
|
9054
9202
|
'Nothing was signed or submitted.');
|
|
9055
9203
|
}
|
|
9056
9204
|
const solanaChain = workerChain;
|
|
9057
|
-
const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await import('./solana-operations-
|
|
9205
|
+
const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await import('./solana-operations-DDt4CzRR.mjs');
|
|
9058
9206
|
// OA-0001: derive the expected intent from OUR OWN request (the one
|
|
9059
9207
|
// ground truth a tampered builder response cannot change) so the
|
|
9060
9208
|
// validator can reject any transaction whose operation semantics
|
|
@@ -9093,7 +9241,9 @@ async function setMany(many, options) {
|
|
|
9093
9241
|
}
|
|
9094
9242
|
// Confirmed: the receipt is already complete, so the mirror read is a
|
|
9095
9243
|
// best-effort enrichment reported in `mirrorSync`, never a rejection.
|
|
9096
|
-
const mirrorSync =
|
|
9244
|
+
const mirrorSync = grantLane
|
|
9245
|
+
? { observed: false, reason: 'not attempted: a venue grant authorizes only the write' }
|
|
9246
|
+
: await attemptMirrorSync(requestedPaths, options);
|
|
9097
9247
|
return {
|
|
9098
9248
|
status: 'confirmed', appId, requestedDocuments, chain: solanaChain,
|
|
9099
9249
|
transactionId: solanaOutcome.transactionSignature,
|
|
@@ -10116,6 +10266,84 @@ function createAppTokenAuthHeaders(options) {
|
|
|
10116
10266
|
};
|
|
10117
10267
|
}
|
|
10118
10268
|
|
|
10269
|
+
/**
|
|
10270
|
+
* The venue grant write lane: create ONE document in another app (an OpenApps venue) as the
|
|
10271
|
+
* wallet signed in here, authorized by a single-use grant instead of a venue session
|
|
10272
|
+
* (VENUE-GRANT-DESIGN.md). The grant only lets the venue build that one transaction for that
|
|
10273
|
+
* wallet; the wallet's own signature, checked by the venue's own rule, is the authority.
|
|
10274
|
+
*
|
|
10275
|
+
* The grant itself is minted by Bounded Auth at `POST {humanAuthApiUrl}/app-token/venue-grant`
|
|
10276
|
+
* (contract: packages/cdk/cloudflare/shared/venue-grant.ts, which this package cannot import
|
|
10277
|
+
* because the SDK packages publish separately). This module only spends one.
|
|
10278
|
+
*
|
|
10279
|
+
* It reuses `setMany`'s request and 202 handling through `_overrides`: the `_config` snapshot is
|
|
10280
|
+
* this app's own config re-pointed at the VENUE app id, so the pre-sign validator (OA-0001) binds
|
|
10281
|
+
* the venue app id and the full expected intent, and the signer stays this app's own AuthProvider
|
|
10282
|
+
* (a Turnkey session or a Wallet Standard wallet). Nothing here is reachable from `createClient()`,
|
|
10283
|
+
* which stays read-only.
|
|
10284
|
+
*
|
|
10285
|
+
* The chain a write runs on is the VENUE's, stated by its 202, never the app's: the app's own
|
|
10286
|
+
* `chain` can only agree with it or be refused, and the transaction is broadcast through the app's
|
|
10287
|
+
* `rpcUrl` only when the app configured that same chain, otherwise through `resolveRpcUrl(chain)`.
|
|
10288
|
+
*/
|
|
10289
|
+
function laneError(code, message) {
|
|
10290
|
+
return Object.assign(new Error(message), { code });
|
|
10291
|
+
}
|
|
10292
|
+
const isSolanaLane = (chain) => chain === 'solana_devnet' || chain === 'solana_mainnet';
|
|
10293
|
+
/**
|
|
10294
|
+
* The RPC a venue write on Solana lane `chain` is broadcast through, and a saved receipt of it is
|
|
10295
|
+
* reconciled through. The app's own `rpcUrl` counts only when the app also configured this chain
|
|
10296
|
+
* (an `rpcUrl` alone does not say which chain it speaks); otherwise `resolveRpcUrl(chain)`. Null
|
|
10297
|
+
* when neither yields one.
|
|
10298
|
+
*
|
|
10299
|
+
* @internal Shared by the lane and by the client SDK's reconciliation of a saved receipt.
|
|
10300
|
+
*/
|
|
10301
|
+
function resolveVenueRpcUrl(config, chain, resolveRpcUrl) {
|
|
10302
|
+
var _a, _b;
|
|
10303
|
+
const own = requiredSolanaChain(config) === chain ? (_a = config.rpcUrl) === null || _a === void 0 ? void 0 : _a.trim() : '';
|
|
10304
|
+
return own || ((_b = resolveRpcUrl === null || resolveRpcUrl === void 0 ? void 0 : resolveRpcUrl(chain)) === null || _b === void 0 ? void 0 : _b.trim()) || null;
|
|
10305
|
+
}
|
|
10306
|
+
/**
|
|
10307
|
+
* Refuse, before the wallet is asked to sign, a 202 whose execution lane this app is not
|
|
10308
|
+
* configured for: a mainnet app must never sign a devnet or poofnet approval (a poofnet "funding"
|
|
10309
|
+
* moves no real USDC). Then point the Solana lane's broadcast at the RPC for the venue's chain.
|
|
10310
|
+
* Poofnet broadcasts nothing, so it needs no RPC.
|
|
10311
|
+
*/
|
|
10312
|
+
function assertExecutionLane(config, chain, options) {
|
|
10313
|
+
const required = requiredSolanaChain(config);
|
|
10314
|
+
if (required && chain !== required) {
|
|
10315
|
+
throw laneError('chain_mismatch', `The venue answered on "${chain}" but this app is configured for "${config.chain}". Nothing was signed.`);
|
|
10316
|
+
}
|
|
10317
|
+
if (!isSolanaLane(chain))
|
|
10318
|
+
return;
|
|
10319
|
+
const rpcUrl = resolveVenueRpcUrl(config, chain, options.resolveRpcUrl);
|
|
10320
|
+
if (rpcUrl)
|
|
10321
|
+
config.rpcUrl = rpcUrl;
|
|
10322
|
+
else if (!options._solanaRpc) {
|
|
10323
|
+
throw laneError('chain_not_configured', `The venue runs on ${chain}, but no RPC could be resolved to broadcast through. ` +
|
|
10324
|
+
`Pass chain: '${chain}' and an rpcUrl to init(). Nothing was signed.`);
|
|
10325
|
+
}
|
|
10326
|
+
}
|
|
10327
|
+
/**
|
|
10328
|
+
* Write `document` at `path` in the venue `venueAppId` with a single-use grant. Resolves to the
|
|
10329
|
+
* same discriminated {@link SetResult} as `set`, and throws exactly as `set` does, so the same
|
|
10330
|
+
* `isWriteNotApplied` reading applies.
|
|
10331
|
+
*
|
|
10332
|
+
* @internal Used by `fundVenueEscrow` and `disputeVenueEscrow` in @bounded-sh/client. Not a stable API.
|
|
10333
|
+
*/
|
|
10334
|
+
async function setWithVenueGrant(path, document, options) {
|
|
10335
|
+
// A private copy of this app's config for this one write: the lane fills in its RPC below.
|
|
10336
|
+
const config = Object.assign(Object.assign({}, (await getConfig())), { appId: options.venueAppId });
|
|
10337
|
+
const headers = (grant) => ({ Authorization: `Bearer ${grant}` });
|
|
10338
|
+
return setMany([{ path, document }], {
|
|
10339
|
+
onBroadcast: options.onBroadcast,
|
|
10340
|
+
_overrides: Object.assign({ _config: config, _getAuthHeaders: async () => headers(options.grant), _grantLane: {
|
|
10341
|
+
assertLane: (chain) => assertExecutionLane(config, chain, options),
|
|
10342
|
+
commitHeaders: async () => headers(await options.commitGrant()),
|
|
10343
|
+
} }, (options._solanaRpc ? { _solanaRpc: options._solanaRpc } : {})),
|
|
10344
|
+
});
|
|
10345
|
+
}
|
|
10346
|
+
|
|
10119
10347
|
/**
|
|
10120
10348
|
* Live named-query subscriptions (subscribeQuery) - HTTP client poller (v1).
|
|
10121
10349
|
*
|
|
@@ -10818,7 +11046,7 @@ function toMillis(seconds) {
|
|
|
10818
11046
|
|
|
10819
11047
|
/** SIWS message creation is async already, so its onchain module can be lazy. */
|
|
10820
11048
|
async function genSolanaMessage(address, nonce) {
|
|
10821
|
-
const implementation = await import('./solana-message-
|
|
11049
|
+
const implementation = await import('./solana-message-BO6Dtgbv.mjs');
|
|
10822
11050
|
return implementation.genSolanaMessage(address, nonce);
|
|
10823
11051
|
}
|
|
10824
11052
|
|
|
@@ -11367,5 +11595,5 @@ function defineLiveModule(mod) {
|
|
|
11367
11595
|
return mod;
|
|
11368
11596
|
}
|
|
11369
11597
|
|
|
11370
|
-
export {
|
|
11371
|
-
//# sourceMappingURL=index-
|
|
11598
|
+
export { RATE_LIMIT_EVENT_NAME as $, isWriteNotApplied as A, writeNotAppliedPhase as B, subscribe as C, closeAllSubscriptions as D, clearCache as E, getCachedData as F, reconnectWithNewAuth as G, createClient as H, InsufficientBalanceError as I, exchangeAppToken as J, clearAppTokenCache as K, createAppTokenAuthHeaders as L, AppTokenExchangeError as M, setWithVenueGrant as N, resolveVenueRpcUrl as O, subscribeQuery as P, closeAllQuerySubscriptions as Q, canonicalizeArgs as R, ScopedClientReadOnlyError as S, canonicalizeResult as T, DEFAULT_QUERY_POLL_INTERVAL_MS as U, MIN_QUERY_POLL_INTERVAL_MS as V, BoundedDeclineError as W, deriveDecline as X, dispatchDeclineEvent as Y, DECLINE_EVENT_NAME as Z, dispatchRateLimitEvent as _, getWebhookKeysUrl as a, hasActiveConnection as a0, increment as a1, serverTimestamp as a2, now as a3, toSeconds as a4, toMillis as a5, getIdToken as a6, deriveUserIdentityFromIdToken as a7, getSessionGeneration as a8, WebSessionManager as a9, normalizeEvmAddress as aA, toLowerEvmAddress as aB, functions as aC, invoke as aD, FunctionInvokeError as aE, live as aF, intent as aG, status as aH, subscribeView as aI, LiveIntentError as aJ, withEffects as aK, isEffectResult as aL, defineLiveModule as aM, EFFECT_INTENT_ADDRESS as aN, ReactNativeSessionManager as aa, getActiveSessionManager as ab, onSessionCleared as ac, announceSessionChange as ad, onSessionChange as ae, SESSION_SIGNAL_CHANNEL as af, SessionEnvironmentError as ag, UNSUPPORTED_ENVIRONMENT_CODE as ah, AUTH_CHANGED_CODE as ai, AUTH_EXPIRED_CODE as aj, isAuthExpiredError as ak, getFreshAuthToken as al, ServerSessionManager as am, createSessionWithSignature as an, refreshSession as ao, revokeSession as ap, signSessionCreateMessage as aq, genAuthNonce as ar, genSolanaMessage as as, createSolanaSignInRequest as at, createSolanaSignInMessageText as au, genSolanaSignInRequest as av, genSolanaSignInMessage as aw, browserSignInDomain as ax, SOLANA_SIGN_IN_STATEMENT as ay, genSiweMessage as az, bufferExports as b, getConfiguredAppId as c, get as d, getMany as e, docId as f, getConfig as g, setMany as h, init as i, setFile as j, getFiles as k, search as l, markWriteNotApplied as m, runQueryMany as n, runExpression as o, runExpressionMany as p, queryAggregate as q, runQuery as r, set as s, signMessage as t, signTransaction as u, signAndSubmitTransaction as v, count as w, aggregate as x, reconcileSetResult as y, waitForSetResult as z };
|
|
11599
|
+
//# sourceMappingURL=index-Db4lrwO6.mjs.map
|