@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.
Files changed (33) hide show
  1. package/dist/client/operations.d.ts +14 -0
  2. package/dist/client/reconcile.d.ts +18 -0
  3. package/dist/client/venue-grant-write.d.ts +62 -0
  4. package/dist/client/write-outcome.d.ts +1 -0
  5. package/dist/{index--ahzzmm5.js → index-CjiaT9AT.js} +295 -62
  6. package/dist/index-CjiaT9AT.js.map +1 -0
  7. package/dist/{index-DTTh5Wt5.mjs → index-Db4lrwO6.mjs} +291 -63
  8. package/dist/index-Db4lrwO6.mjs.map +1 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.js +6 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1 -1
  13. package/dist/{realtime-store-Bcra04TH.mjs → realtime-store-CNs5zURZ.mjs} +2 -2
  14. package/dist/{realtime-store-Bcra04TH.mjs.map → realtime-store-CNs5zURZ.mjs.map} +1 -1
  15. package/dist/{realtime-store-CR80Jac5.js → realtime-store-IPPwUhJc.js} +2 -2
  16. package/dist/{realtime-store-CR80Jac5.js.map → realtime-store-IPPwUhJc.js.map} +1 -1
  17. package/dist/{solana-message-BwPW9Fst.mjs → solana-message-BO6Dtgbv.mjs} +2 -2
  18. package/dist/{solana-message-BwPW9Fst.mjs.map → solana-message-BO6Dtgbv.mjs.map} +1 -1
  19. package/dist/{solana-message-DPR9uw0N.js → solana-message-CXuLc7NX.js} +2 -2
  20. package/dist/{solana-message-DPR9uw0N.js.map → solana-message-CXuLc7NX.js.map} +1 -1
  21. package/dist/{solana-operations-BMB6sq_i.js → solana-operations-CghJe0_7.js} +3 -3
  22. package/dist/{solana-operations-BMB6sq_i.js.map → solana-operations-CghJe0_7.js.map} +1 -1
  23. package/dist/{solana-operations-BBQllFVm.mjs → solana-operations-DDt4CzRR.mjs} +3 -3
  24. package/dist/{solana-operations-BBQllFVm.mjs.map → solana-operations-DDt4CzRR.mjs.map} +1 -1
  25. package/dist/{solana-v1-operations-CCHbLEo-.js → solana-v1-operations-BY2DTfbR.js} +3 -3
  26. package/dist/{solana-v1-operations-CCHbLEo-.js.map → solana-v1-operations-BY2DTfbR.js.map} +1 -1
  27. package/dist/{solana-v1-operations-DBuA0PlD.mjs → solana-v1-operations-TH-HUzbT.mjs} +3 -3
  28. package/dist/{solana-v1-operations-DBuA0PlD.mjs.map → solana-v1-operations-TH-HUzbT.mjs.map} +1 -1
  29. package/dist/utils/refresh-error.d.ts +4 -0
  30. package/dist/utils/session-signal.d.ts +7 -0
  31. package/package.json +2 -2
  32. package/dist/index--ahzzmm5.js.map +0 -1
  33. 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-DPR9uw0N.js'); });
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-CR80Jac5.js'); });
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-BMB6sq_i.js'); });
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
- // The commit PUT is the one ambiguous step of this lane: a lost response
9045
- // may follow a commit, so its failures stay untagged (read the path back).
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
- const commitResponse = await makeApiRequest('PUT', 'items/signed', { intentId: curTx.poofnetIntentId, signedTransaction }, options === null || options === void 0 ? void 0 : options._overrides);
9048
- if (commitResponse.status !== 200) {
9049
- throw new Error(`Poofnet signed write was not committed (status ${commitResponse.status})`);
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-BMB6sq_i.js'); });
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 = await attemptMirrorSync(requestedPaths, options);
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-DPR9uw0N.js'); });
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--ahzzmm5.js.map
11704
+ //# sourceMappingURL=index-CjiaT9AT.js.map