@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
@@ -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-BwPW9Fst.mjs');
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-Bcra04TH.mjs');
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-BBQllFVm.mjs');
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
- // The commit PUT is the one ambiguous step of this lane: a lost response
9043
- // may follow a commit, so its failures stay untagged (read the path back).
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
- const commitResponse = await makeApiRequest('PUT', 'items/signed', { intentId: curTx.poofnetIntentId, signedTransaction }, options === null || options === void 0 ? void 0 : options._overrides);
9046
- if (commitResponse.status !== 200) {
9047
- throw new Error(`Poofnet signed write was not committed (status ${commitResponse.status})`);
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-BBQllFVm.mjs');
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 = await attemptMirrorSync(requestedPaths, options);
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-BwPW9Fst.mjs');
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 { increment 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, subscribeQuery as N, closeAllQuerySubscriptions as O, canonicalizeArgs as P, canonicalizeResult as Q, DEFAULT_QUERY_POLL_INTERVAL_MS as R, ScopedClientReadOnlyError as S, MIN_QUERY_POLL_INTERVAL_MS as T, BoundedDeclineError as U, deriveDecline as V, dispatchDeclineEvent as W, DECLINE_EVENT_NAME as X, dispatchRateLimitEvent as Y, RATE_LIMIT_EVENT_NAME as Z, hasActiveConnection as _, getWebhookKeysUrl as a, serverTimestamp as a0, now as a1, toSeconds as a2, toMillis as a3, getIdToken as a4, deriveUserIdentityFromIdToken as a5, getSessionGeneration as a6, WebSessionManager as a7, ReactNativeSessionManager as a8, getActiveSessionManager as a9, live as aA, intent as aB, status as aC, subscribeView as aD, LiveIntentError as aE, withEffects as aF, isEffectResult as aG, defineLiveModule as aH, EFFECT_INTENT_ADDRESS as aI, onSessionCleared as aa, SessionEnvironmentError as ab, UNSUPPORTED_ENVIRONMENT_CODE as ac, AUTH_CHANGED_CODE as ad, AUTH_EXPIRED_CODE as ae, isAuthExpiredError as af, getFreshAuthToken as ag, ServerSessionManager as ah, createSessionWithSignature as ai, refreshSession as aj, revokeSession as ak, signSessionCreateMessage as al, genAuthNonce as am, genSolanaMessage as an, createSolanaSignInRequest as ao, createSolanaSignInMessageText as ap, genSolanaSignInRequest as aq, genSolanaSignInMessage as ar, browserSignInDomain as as, SOLANA_SIGN_IN_STATEMENT as at, genSiweMessage as au, normalizeEvmAddress as av, toLowerEvmAddress as aw, functions as ax, invoke as ay, FunctionInvokeError 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 };
11371
- //# sourceMappingURL=index-DTTh5Wt5.mjs.map
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