@bounded-sh/core 0.0.65 → 0.0.67

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 (32) hide show
  1. package/dist/client/decline.d.ts +5 -3
  2. package/dist/client/live-effects.d.ts +4 -2
  3. package/dist/client/operations.d.ts +90 -5
  4. package/dist/client/reconcile.d.ts +51 -0
  5. package/dist/client/solana-operations.d.ts +13 -2
  6. package/dist/{index-C8ocsC_W.js → index-DNZifL5j.js} +801 -91
  7. package/dist/index-DNZifL5j.js.map +1 -0
  8. package/dist/{index-BNOU00XY.mjs → index-TyXiW4Uf.mjs} +800 -92
  9. package/dist/index-TyXiW4Uf.mjs.map +1 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.js +3 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +1 -1
  14. package/dist/{realtime-store-I_bcryPz.js → realtime-store-CINyAkPw.js} +2 -2
  15. package/dist/{realtime-store-I_bcryPz.js.map → realtime-store-CINyAkPw.js.map} +1 -1
  16. package/dist/{realtime-store-mzFJ8bHE.mjs → realtime-store-CpbnXM7q.mjs} +2 -2
  17. package/dist/{realtime-store-mzFJ8bHE.mjs.map → realtime-store-CpbnXM7q.mjs.map} +1 -1
  18. package/dist/{solana-message-D_CN0H6r.js → solana-message-Bycv-L5n.js} +2 -2
  19. package/dist/{solana-message-D_CN0H6r.js.map → solana-message-Bycv-L5n.js.map} +1 -1
  20. package/dist/{solana-message-VZBGvV0S.mjs → solana-message-DXXwC95Z.mjs} +2 -2
  21. package/dist/{solana-message-VZBGvV0S.mjs.map → solana-message-DXXwC95Z.mjs.map} +1 -1
  22. package/dist/{solana-operations-BlCYbdiN.mjs → solana-operations-ChHG25O0.mjs} +157 -20
  23. package/dist/solana-operations-ChHG25O0.mjs.map +1 -0
  24. package/dist/{solana-operations-CYpu6EU8.js → solana-operations-Dm_4VnlB.js} +158 -19
  25. package/dist/solana-operations-Dm_4VnlB.js.map +1 -0
  26. package/dist/types.d.ts +370 -4
  27. package/dist/utils/core-platform.d.ts +7 -0
  28. package/package.json +2 -2
  29. package/dist/index-BNOU00XY.mjs.map +0 -1
  30. package/dist/index-C8ocsC_W.js.map +0 -1
  31. package/dist/solana-operations-BlCYbdiN.mjs.map +0 -1
  32. package/dist/solana-operations-CYpu6EU8.js.map +0 -1
@@ -553,22 +553,25 @@ async function createSessionWithSignature(address, message, signature, authMetho
553
553
  // Deduplicate concurrent refreshSession calls to prevent multiple code paths
554
554
  // (WebSocket reconnection, periodic refresh, API 401 retry) from all hitting
555
555
  // /session/refresh simultaneously.
556
- // Note: module-level state is shared across requests in SSR/Node — acceptable
557
- // since there is only one active session per server instance (same pattern as
558
- // refreshInFlight in api.ts).
559
- let refreshInFlight = null;
560
- let refreshInFlightToken = null;
556
+ //
557
+ // Keyed PER (issuer, token) in a Map, not one global slot (DS2-0373): the rotating
558
+ // refresh token is single-use, so two concurrent refreshes of the SAME token must
559
+ // share one in-flight request. A single global slot could hold only one token at a
560
+ // time, so an interleaved refresh of a DIFFERENT session's token evicted the first
561
+ // token's in-flight promise; a later second refresh of the first token then missed
562
+ // its own in-flight request and re-POSTed the already-spent token - the issuer sees
563
+ // reuse and revokes the whole family. Keying by token lets unrelated sessions
564
+ // coalesce independently.
565
+ // Note: module-level state is shared across requests in SSR/Node - acceptable since
566
+ // each key is bound to a specific session's token (same pattern as api.ts).
567
+ const refreshInFlightByToken = new Map();
561
568
  async function refreshSession(refreshToken, issuer) {
562
- // Dedupe per (issuer, token): the rotating token is single-use, so two
563
- // concurrent refreshes of the SAME token must share one in-flight request or
564
- // the second would look like reuse and revoke the family.
565
569
  const dedupeKey = `${issuer || ''}::${refreshToken}`;
566
- if (refreshInFlight && refreshInFlightToken === dedupeKey) {
567
- return refreshInFlight;
568
- }
569
- refreshInFlightToken = dedupeKey;
570
+ const existing = refreshInFlightByToken.get(dedupeKey);
571
+ if (existing)
572
+ return existing;
570
573
  let currentFlight;
571
- currentFlight = refreshInFlight = (async () => {
574
+ currentFlight = (async () => {
572
575
  try {
573
576
  const config = await getConfig();
574
577
  const appId = config.appId;
@@ -583,15 +586,16 @@ async function refreshSession(refreshToken, issuer) {
583
586
  return response.data;
584
587
  }
585
588
  finally {
586
- // Only clear if we're still the active in-flight request.
587
- // A second call with a different token may have replaced us.
588
- if (refreshInFlight === currentFlight) {
589
- refreshInFlight = null;
590
- refreshInFlightToken = null;
589
+ // Only remove our own entry, and only if it is still the active flight
590
+ // for this token (a later refresh of the same token cannot have replaced
591
+ // it while we were in flight, but this stays correct if that ever changes).
592
+ if (refreshInFlightByToken.get(dedupeKey) === currentFlight) {
593
+ refreshInFlightByToken.delete(dedupeKey);
591
594
  }
592
595
  }
593
596
  })();
594
- return refreshInFlight;
597
+ refreshInFlightByToken.set(dedupeKey, currentFlight);
598
+ return currentFlight;
595
599
  }
596
600
  class SessionRevokeError extends Error {
597
601
  constructor(message, cause) {
@@ -1005,7 +1009,12 @@ class WebSessionManager {
1005
1009
  // hosted login. The idToken carries the canonical identity (@user.id) + exp.
1006
1010
  const { idToken } = sessionObj;
1007
1011
  const { exp } = JSON.parse(this.decodeBase64Url(idToken.split(".")[1]));
1008
- if (Date.now() > exp * 1000) {
1012
+ // Treat a MISSING or non-numeric expiry as EXPIRED (fail closed). A bare
1013
+ // `Date.now() > exp * 1000` is NaN -> false when exp is absent/non-numeric,
1014
+ // so a forged or malformed token with no usable expiry would otherwise be
1015
+ // accepted as valid forever.
1016
+ const expired = typeof exp !== "number" || !Number.isFinite(exp) || Date.now() > exp * 1000;
1017
+ if (expired) {
1009
1018
  const { refreshToken } = sessionObj;
1010
1019
  // Expired token with no way to refresh — clear the stale session
1011
1020
  // (mirrors the refresh-failed branch below) so isAuthenticated()
@@ -1111,25 +1120,46 @@ class WebSessionManager {
1111
1120
  /* ------------------------------------------------------------------ */
1112
1121
  /* TOKEN HELPERS */
1113
1122
  /* ------------------------------------------------------------------ */
1123
+ // These reads are DEFENSIVE (try/catch -> treat a corrupt blob as absent).
1124
+ // logout() reads the refresh token + issuer BEFORE the session is cleared; if a
1125
+ // corrupt (non-JSON) blob threw out of the read, logout would abort with the
1126
+ // credential still in storage while isAuthenticated() kept reporting true, so
1127
+ // sign-out could never complete. A corrupt blob is unusable either way, so
1128
+ // reporting "no token" lets the clear path proceed and finish the sign-out.
1114
1129
  static getIdToken() {
1115
1130
  if (typeof window === "undefined")
1116
1131
  return null;
1117
- const session = localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY);
1118
- return session ? JSON.parse(session).idToken : null;
1132
+ try {
1133
+ const session = localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY);
1134
+ return session ? JSON.parse(session).idToken : null;
1135
+ }
1136
+ catch (_a) {
1137
+ return null;
1138
+ }
1119
1139
  }
1120
1140
  static getRefreshToken() {
1121
1141
  if (typeof window === "undefined")
1122
1142
  return null;
1123
- const session = localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY);
1124
- return session ? JSON.parse(session).refreshToken : null;
1143
+ try {
1144
+ const session = localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY);
1145
+ return session ? JSON.parse(session).refreshToken : null;
1146
+ }
1147
+ catch (_a) {
1148
+ return null;
1149
+ }
1125
1150
  }
1126
1151
  /** The issuer base that minted this session (for routing silent refresh). */
1127
1152
  static getIssuer() {
1128
1153
  var _a;
1129
1154
  if (typeof window === "undefined")
1130
1155
  return null;
1131
- const session = localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY);
1132
- return session ? ((_a = JSON.parse(session).issuer) !== null && _a !== void 0 ? _a : null) : null;
1156
+ try {
1157
+ const session = localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY);
1158
+ return session ? ((_a = JSON.parse(session).issuer) !== null && _a !== void 0 ? _a : null) : null;
1159
+ }
1160
+ catch (_b) {
1161
+ return null;
1162
+ }
1133
1163
  }
1134
1164
  /* ------------------------------------------------------------------ */
1135
1165
  /* UPDATE TOKENS */
@@ -1387,7 +1417,12 @@ class ReactNativeSessionManager {
1387
1417
  // logins is opaque, so decoding it threw → the catch wiped a valid session.
1388
1418
  const { idToken } = sessionObj;
1389
1419
  const { exp } = JSON.parse(this.decodeBase64Url(idToken.split(".")[1]));
1390
- if (Date.now() > exp * 1000) {
1420
+ // Treat a MISSING or non-numeric expiry as EXPIRED (fail closed). A bare
1421
+ // `Date.now() > exp * 1000` is NaN -> false when exp is absent/non-numeric,
1422
+ // so a forged or malformed token with no usable expiry would otherwise be
1423
+ // accepted as valid forever.
1424
+ const expired = typeof exp !== "number" || !Number.isFinite(exp) || Date.now() > exp * 1000;
1425
+ if (expired) {
1391
1426
  const { refreshToken } = sessionObj;
1392
1427
  // Expired token with no way to refresh — clear the stale session
1393
1428
  // (mirrors the refresh-failed branch below) so isAuthenticated()
@@ -4010,7 +4045,7 @@ class ServerSessionManager {
4010
4045
  const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
4011
4046
  /* fetch nonce from auth API (bound to the snapshotted app+issuer) */
4012
4047
  const nonce = await genAuthNonce(sessionConfig);
4013
- const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-D_CN0H6r.js'); });
4048
+ const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-Bycv-L5n.js'); });
4014
4049
  const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
4015
4050
  /* sign the message */
4016
4051
  const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
@@ -4102,18 +4137,31 @@ ServerSessionManager.instance = new ServerSessionManager();
4102
4137
  *
4103
4138
  * These are used instead of bare `atob()`/`btoa()` so the core package
4104
4139
  * works in React Native without requiring the consumer to polyfill globals.
4140
+ *
4141
+ * Both directions are UTF-8-safe: bare btoa / Buffer('binary') are LATIN-1 and
4142
+ * throw (surrogate-pair emoji) or silently mangle (accents, CJK) any code point
4143
+ * > 0xFF, corrupting user text - e.g. an AI-query `prompt` - before it is sent.
4144
+ * safeBtoa base64s the UTF-8 bytes; safeAtob is its exact inverse, and the
4145
+ * realtime worker decodes the same bytes as UTF-8 (src/utf8-base64.ts). ASCII is
4146
+ * byte-identical either way, so JWT (decodeBase64Url) decoding is unaffected.
4105
4147
  */
4106
4148
  function safeAtob(input) {
4107
4149
  if (typeof globalThis.atob === 'function') {
4108
- return globalThis.atob(input);
4150
+ const binary = globalThis.atob(input);
4151
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
4152
+ return new TextDecoder().decode(bytes);
4109
4153
  }
4110
- return bufferExports.Buffer.from(input, 'base64').toString('binary');
4154
+ return bufferExports.Buffer.from(input, 'base64').toString('utf-8');
4111
4155
  }
4112
4156
  function safeBtoa(input) {
4157
+ const bytes = new TextEncoder().encode(input);
4113
4158
  if (typeof globalThis.btoa === 'function') {
4114
- return globalThis.btoa(input);
4159
+ let binary = '';
4160
+ for (let i = 0; i < bytes.length; i++)
4161
+ binary += String.fromCharCode(bytes[i]);
4162
+ return globalThis.btoa(binary);
4115
4163
  }
4116
- return bufferExports.Buffer.from(input, 'binary').toString('base64');
4164
+ return bufferExports.Buffer.from(bytes).toString('base64');
4117
4165
  }
4118
4166
  /**
4119
4167
  * Decode a base64url-encoded string (used by JWT payloads).
@@ -6729,7 +6777,7 @@ async function doReconnectWithNewAuth() {
6729
6777
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6730
6778
  }
6731
6779
  try {
6732
- const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-I_bcryPz.js'); });
6780
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-CINyAkPw.js'); });
6733
6781
  await reconnectRealtimeStoreWithNewAuth();
6734
6782
  }
6735
6783
  catch (error) {
@@ -7403,17 +7451,6 @@ function assertEvmTransactionMatchesWrite(transaction, context) {
7403
7451
  }
7404
7452
  }
7405
7453
 
7406
- var __rest$1 = (undefined && undefined.__rest) || function (s, e) {
7407
- var t = {};
7408
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
7409
- t[p] = s[p];
7410
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
7411
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7412
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
7413
- t[p[i]] = s[p[i]];
7414
- }
7415
- return t;
7416
- };
7417
7454
  /**
7418
7455
  * Thrown when a user's wallet doesn't have enough SOL to cover the transaction.
7419
7456
  * Apps can catch this to trigger an onramp/funding flow.
@@ -7991,7 +8028,11 @@ async function get(path, opts = {}) {
7991
8028
  }
7992
8029
  else {
7993
8030
  const path = encodeURIComponent(normalizedPath);
7994
- const promptQueryParam = (opts === null || opts === void 0 ? void 0 : opts.prompt) ? `&prompt=${safeBtoa(opts.prompt)}` : "";
8031
+ // encodeURIComponent the base64: safeBtoa emits standard base64 (with '+' '/' '='),
8032
+ // and a raw '+' in a query string decodes to a space on the server
8033
+ // (url.searchParams.get), corrupting the exact non-ASCII prompts DS2-0463 fixed. The
8034
+ // server percent-decodes back to the original base64 before decodeUtf8Base64. (DS2-0463)
8035
+ const promptQueryParam = (opts === null || opts === void 0 ? void 0 : opts.prompt) ? `&prompt=${encodeURIComponent(safeBtoa(opts.prompt))}` : "";
7995
8036
  const apiPath = `items?path=${path}${promptQueryParam}${filterParam}${sortParam}${includeSubPathsParam}${shapeParam}${limitParam}${cursorParam}`;
7996
8037
  response = await makeApiRequest('GET', apiPath, null, opts._overrides);
7997
8038
  }
@@ -8332,20 +8373,200 @@ async function runExpressionMany(many) {
8332
8373
  throw error;
8333
8374
  }
8334
8375
  }
8376
+ /* ===================== WRITE RECEIPT HELPERS (W4) ======================== */
8377
+ /**
8378
+ * Settle delay before the best-effort mirror read on an on-chain write. It is
8379
+ * an optimization, not a correctness guarantee: the receipt is captured first
8380
+ * and returned regardless of what the mirror read reports.
8381
+ */
8382
+ const MIRROR_SYNC_SETTLE_MS = 1500;
8383
+ const SOLANA_CHAIN_TAGS = new Set(['solana_devnet', 'solana_mainnet']);
8384
+ /**
8385
+ * Read the execution-lane tag the WORKER reported. The SDK never infers it: a
8386
+ * batch touching only unflagged collections in a `realtime_devnet` app commits
8387
+ * offchain, so the app's protocol is not the lane. Several field names are
8388
+ * accepted (batch-level receipt first, then the per-transaction Solana
8389
+ * `network` tag that predates it), but no value is ever manufactured.
8390
+ */
8391
+ function readWorkerChainTag(...sources) {
8392
+ for (const source of sources) {
8393
+ if (!source || typeof source !== 'object')
8394
+ continue;
8395
+ for (const key of ['chain', 'executionChain', 'network']) {
8396
+ const value = source[key];
8397
+ if (typeof value === 'string' && value.trim().length > 0)
8398
+ return value.trim();
8399
+ }
8400
+ }
8401
+ return null;
8402
+ }
8403
+ /** Path-keyed echo of what the caller asked for. NEVER authoritative state. */
8404
+ function buildRequestedDocuments(documents) {
8405
+ return documents.map(entry => {
8406
+ var _a;
8407
+ return ({
8408
+ path: entry.destinationPath,
8409
+ document: ((_a = entry.document) !== null && _a !== void 0 ? _a : null),
8410
+ });
8411
+ });
8412
+ }
8413
+ /** Look a concrete path up in a path-keyed map, tolerating a leading slash. */
8414
+ function pickPathDocument(source, path) {
8415
+ if (Object.prototype.hasOwnProperty.call(source, path))
8416
+ return source[path];
8417
+ const withSlash = path.startsWith('/') ? path : `/${path}`;
8418
+ if (Object.prototype.hasOwnProperty.call(source, withSlash))
8419
+ return source[withSlash];
8420
+ const withoutSlash = path.startsWith('/') ? path.slice(1) : path;
8421
+ if (Object.prototype.hasOwnProperty.call(source, withoutSlash))
8422
+ return source[withoutSlash];
8423
+ return undefined;
8424
+ }
8425
+ /**
8426
+ * `observed: true` only when EVERY requested concrete path was read back as a
8427
+ * real document. The sync endpoint answers `null` for a document that is
8428
+ * absent, lagging, or read-denied, and a deleted path reads back as `null` too
8429
+ * — none of those are evidence of synchronization.
8430
+ */
8431
+ function validatePathKeyedDocuments(paths, source) {
8432
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
8433
+ return { observed: false, reason: 'the synchronization response was not a path-keyed document map' };
8434
+ }
8435
+ const map = source;
8436
+ const documents = [];
8437
+ const unobserved = [];
8438
+ for (const path of paths) {
8439
+ const document = pickPathDocument(map, path);
8440
+ if (document == null || typeof document !== 'object' || Array.isArray(document)) {
8441
+ unobserved.push(path);
8442
+ documents.push({ path, document: null });
8443
+ continue;
8444
+ }
8445
+ documents.push({ path, document: document });
8446
+ }
8447
+ if (unobserved.length > 0) {
8448
+ return {
8449
+ observed: false,
8450
+ reason: `no readable document was returned for ${unobserved.join(', ')} ` +
8451
+ '(absent, deleted, lagging, read-denied, or a passthrough write)',
8452
+ };
8453
+ }
8454
+ return { observed: true, documents };
8455
+ }
8456
+ /**
8457
+ * Best-effort mirror read. A confirmed transaction must NEVER be lost to a
8458
+ * synchronization failure, so this reports its outcome in `mirrorSync` and
8459
+ * never rejects. It reads the CONCRETE destination paths, so a collection
8460
+ * insert with a generated id syncs the path that actually exists.
8461
+ */
8462
+ async function attemptMirrorSync(paths, options, settleMs = MIRROR_SYNC_SETTLE_MS) {
8463
+ try {
8464
+ if (settleMs > 0)
8465
+ await new Promise(resolve => setTimeout(resolve, settleMs));
8466
+ const synced = await syncItems(paths, options);
8467
+ // The sync endpoint answers `{ data: <path-keyed map>, status }`; a document
8468
+ // path always contains a `/`, so a top-level `data` key is never one.
8469
+ const map = (synced && typeof synced === 'object' && !Array.isArray(synced) && 'data' in synced)
8470
+ ? synced.data
8471
+ : synced;
8472
+ return validatePathKeyedDocuments(paths, map);
8473
+ }
8474
+ catch (error) {
8475
+ const message = typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' ? error.message : String(error);
8476
+ return { observed: false, reason: `mirror synchronization failed: ${message}` };
8477
+ }
8478
+ }
8479
+ function normalizePathKey(path) {
8480
+ return path.startsWith('/') ? path.slice(1) : path;
8481
+ }
8482
+ /**
8483
+ * Post-hook observation for a DIRECT commit, taken from the worker's own batch
8484
+ * receipt. The worker is the only party that can read every concrete path back
8485
+ * AFTER hooks have run, so when it reports nothing we say so rather than
8486
+ * echoing the request as if it were server state.
8487
+ *
8488
+ * Unlike the mirror read, a `null` document here IS authoritative: the worker
8489
+ * read its own store, so a deleted path genuinely is absent. The worker's
8490
+ * `observed` verdict is still re-validated against the requested paths — a
8491
+ * receipt that skips one of them cannot be treated as complete.
8492
+ */
8493
+ function observationFromDirectCommit(paths, observation) {
8494
+ var _a;
8495
+ if (!observation || typeof observation !== 'object' || Array.isArray(observation)) {
8496
+ return { observed: false, reason: 'the write receipt carried no post-hook observation' };
8497
+ }
8498
+ const entries = Array.isArray(observation.documents) ? observation.documents : null;
8499
+ if (!entries) {
8500
+ return { observed: false, reason: 'the write receipt carried no path-keyed post-hook documents' };
8501
+ }
8502
+ const byPath = new Map();
8503
+ for (const entry of entries) {
8504
+ if (!entry || typeof entry.path !== 'string')
8505
+ continue;
8506
+ const document = entry.document;
8507
+ byPath.set(normalizePathKey(entry.path), (document && typeof document === 'object' && !Array.isArray(document))
8508
+ ? document
8509
+ : null);
8510
+ }
8511
+ const documents = [];
8512
+ const missing = [];
8513
+ for (const path of paths) {
8514
+ const key = normalizePathKey(path);
8515
+ if (!byPath.has(key)) {
8516
+ missing.push(path);
8517
+ documents.push({ path, document: null });
8518
+ continue;
8519
+ }
8520
+ documents.push({ path, document: (_a = byPath.get(key)) !== null && _a !== void 0 ? _a : null });
8521
+ }
8522
+ if (observation.observed !== true || missing.length > 0) {
8523
+ const unobserved = Array.isArray(observation.unobserved) ? observation.unobserved : [];
8524
+ const reasons = [
8525
+ ...unobserved
8526
+ .filter((entry) => entry && typeof entry.path === 'string')
8527
+ .map((entry) => { var _a; return `${entry.path}: ${(_a = entry.reason) !== null && _a !== void 0 ? _a : 'not observable'}`; }),
8528
+ ...missing.map(path => `${path}: no entry in the receipt observation`),
8529
+ ];
8530
+ return {
8531
+ observed: false,
8532
+ reason: reasons.length > 0
8533
+ ? reasons.join('; ')
8534
+ : 'the worker could not read every requested path back after hooks',
8535
+ };
8536
+ }
8537
+ return { observed: true, documents };
8538
+ }
8335
8539
  /**
8336
8540
  * Write a document at `path`. Sugar for a one-element {@link setMany}.
8337
8541
  *
8338
8542
  * **Delete:** pass `null` as the document to delete it — `set(path, null)` is
8339
8543
  * the delete (there is no separate `del`/`remove`). It is routed through the
8340
- * collection's policy `delete` rule and broadcasts a delete to subscribers.
8544
+ * collection's policy `delete` rule and broadcasts a delete to subscribers, and
8545
+ * the deleted entry surfaces as a `null` document under its path.
8546
+ *
8547
+ * Resolves to a discriminated {@link SetResult}; discriminate on `status`.
8341
8548
  */
8342
8549
  async function set(path, document, options) {
8343
8550
  // setMany already invalidates the read cache for the affected document and
8344
8551
  // collection paths — no second sweep needed here.
8345
8552
  return setMany([{ path, document }], options);
8346
8553
  }
8554
+ /**
8555
+ * Atomically write (or delete, with a `null` document) a batch of documents.
8556
+ *
8557
+ * Resolves to a discriminated {@link SetResult}:
8558
+ * - `committed` — the worker applied the batch directly.
8559
+ * - `confirmed` — a client-signed chain transaction landed successfully.
8560
+ * - `submitted` — broadcast happened but the outcome is unknown. NEVER
8561
+ * resubmit; call `reconcileSetResult`/`waitForSetResult` on the receipt.
8562
+ * - `expired_not_landed` — Solana only, and the one retry-safe terminal state.
8563
+ * - `signed` — `shouldSubmitTx: false`; signed and deliberately not sent.
8564
+ *
8565
+ * `requestedDocuments` is always the caller's own echo; authoritative server
8566
+ * state appears only under `observation`/`mirrorSync` once actually observed.
8567
+ */
8347
8568
  async function setMany(many, options) {
8348
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
8569
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
8349
8570
  // Returns the data that was set, or undefined if the document was already set.
8350
8571
  try {
8351
8572
  // A server WalletClient threads its immutable config snapshot; the default
@@ -8427,8 +8648,17 @@ async function setMany(many, options) {
8427
8648
  (hasSerializedSolanaTarget && hasBuiltSolanaTarget)) {
8428
8649
  throw new Error('Ambiguous on-chain write response mixes EVM and/or Solana transaction target formats');
8429
8650
  }
8430
- let transactionResult;
8431
8651
  const expectedWritePaths = documents.map(d => d.destinationPath);
8652
+ const requestedDocuments = buildRequestedDocuments(documents);
8653
+ const appId = config.appId;
8654
+ // The lane comes verbatim from the worker, and is read BEFORE anything is
8655
+ // signed or submitted: a missing tag fails closed while nothing has been
8656
+ // broadcast, instead of stranding a landed transaction with no receipt.
8657
+ const workerChain = readWorkerChainTag((_k = setResponse.data) === null || _k === void 0 ? void 0 : _k.receipt, setResponse.data, curTx);
8658
+ if (!workerChain) {
8659
+ throw new Error('The write response carried no execution-chain tag, so this receipt could not state where the write ran. ' +
8660
+ 'Nothing was signed or submitted.');
8661
+ }
8432
8662
  // EVM onchain write: the worker returns a PRE-BUILT EIP-1559 tx on
8433
8663
  // `transactions[0].evmTransaction` (the EVM twin of Solana's pre-built
8434
8664
  // `serializedTransaction`). WORKER 202 CONTRACT (field name + shape the
@@ -8443,41 +8673,110 @@ async function setMany(many, options) {
8443
8673
  assertEvmTransactionMatchesWrite(curTx.evmTransaction, {
8444
8674
  appId: config.appId,
8445
8675
  documents,
8446
- policy: (_m = (_l = (_k = options === null || options === void 0 ? void 0 : options._overrides) === null || _k === void 0 ? void 0 : _k._evmTransactionPolicy) !== null && _l !== void 0 ? _l : config.evmWalletConfig) !== null && _m !== void 0 ? _m : null,
8676
+ policy: (_o = (_m = (_l = options === null || options === void 0 ? void 0 : options._overrides) === null || _l === void 0 ? void 0 : _l._evmTransactionPolicy) !== null && _m !== void 0 ? _m : config.evmWalletConfig) !== null && _o !== void 0 ? _o : null,
8447
8677
  });
8448
- transactionResult = await handleEvmTransaction(curTx.evmTransaction, authProvider, options);
8678
+ const evmChain = workerChain;
8679
+ const evmOutcome = await handleEvmTransaction(curTx.evmTransaction, authProvider, options);
8680
+ // CAPTURE THE RECEIPT FIRST. Mirror synchronization comes after, and its
8681
+ // failure can never turn a landed transaction into a thrown error that
8682
+ // invites the caller to submit the same write twice.
8683
+ if (evmOutcome.outcome === 'confirmed') {
8684
+ const mirrorSync = await attemptMirrorSync(expectedWritePaths, options);
8685
+ return {
8686
+ status: 'confirmed', appId, requestedDocuments, chain: evmChain,
8687
+ transactionId: evmOutcome.transactionSignature,
8688
+ blockNumber: evmOutcome.blockNumber,
8689
+ mirrorSync,
8690
+ };
8691
+ }
8692
+ // An unknown confirmation outcome is NOT a mirror question: nothing is
8693
+ // known to have landed, so no synchronization is attempted.
8694
+ if (evmOutcome.transactionSignature === null) {
8695
+ return {
8696
+ status: 'submitted', appId, requestedDocuments, chain: evmChain,
8697
+ transactionId: null,
8698
+ confirmation: { state: 'unknown', reason: evmOutcome.reason, reconcilable: false },
8699
+ };
8700
+ }
8701
+ if ('fence' in evmOutcome && evmOutcome.fence) {
8702
+ return {
8703
+ status: 'submitted', appId, requestedDocuments, chain: evmChain,
8704
+ transactionId: evmOutcome.transactionSignature,
8705
+ fence: evmOutcome.fence,
8706
+ confirmation: { state: 'unknown', reason: evmOutcome.reason },
8707
+ };
8708
+ }
8709
+ return {
8710
+ status: 'submitted', appId, requestedDocuments, chain: evmChain,
8711
+ transactionId: evmOutcome.transactionSignature,
8712
+ rebroadcastable: false,
8713
+ confirmation: { state: 'unknown', reason: evmOutcome.reason },
8714
+ };
8449
8715
  }
8450
8716
  else if (curTx.serializedTransaction) {
8451
- const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-CYpu6EU8.js'); });
8452
- transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
8717
+ if (!SOLANA_CHAIN_TAGS.has(workerChain)) {
8718
+ throw new Error(`The write response reported execution chain "${workerChain}" for a Solana transaction. ` +
8719
+ 'Nothing was signed or submitted.');
8720
+ }
8721
+ const solanaChain = workerChain;
8722
+ const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-Dm_4VnlB.js'); });
8723
+ const solanaOutcome = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
8724
+ // `shouldSubmitTx: false` signed WITHOUT sending — no signature exists,
8725
+ // and there is nothing to mirror, so no synchronization is attempted.
8726
+ if (solanaOutcome.outcome === 'signed') {
8727
+ return {
8728
+ status: 'signed', appId, requestedDocuments, chain: solanaChain,
8729
+ transactionId: null,
8730
+ signedTransaction: solanaOutcome.signedTransaction,
8731
+ fence: solanaOutcome.fence,
8732
+ };
8733
+ }
8734
+ if (solanaOutcome.outcome === 'submitted') {
8735
+ return {
8736
+ status: 'submitted', appId, requestedDocuments, chain: solanaChain,
8737
+ transactionId: solanaOutcome.transactionSignature,
8738
+ signedTransaction: solanaOutcome.signedTransaction,
8739
+ fence: solanaOutcome.fence,
8740
+ confirmation: { state: 'unknown', reason: solanaOutcome.reason },
8741
+ };
8742
+ }
8743
+ // Confirmed: the receipt is already complete, so the mirror read is a
8744
+ // best-effort enrichment reported in `mirrorSync`, never a rejection.
8745
+ const mirrorSync = await attemptMirrorSync(expectedWritePaths, options);
8746
+ return {
8747
+ status: 'confirmed', appId, requestedDocuments, chain: solanaChain,
8748
+ transactionId: solanaOutcome.transactionSignature,
8749
+ signedTransaction: solanaOutcome.signedTransaction,
8750
+ fence: solanaOutcome.fence,
8751
+ confirmationContextSlot: solanaOutcome.confirmationContextSlot,
8752
+ mirrorSync,
8753
+ };
8453
8754
  }
8454
8755
  else {
8455
8756
  throw new Error('Unsupported 202 transaction shape: expected serializedTransaction or evmTransaction');
8456
8757
  }
8457
- // Sync items after all transactions are confirmed
8458
- // Wait for 1.5 seconds to ensure all transactions are confirmed
8459
- await new Promise(resolve => setTimeout(resolve, 1500));
8460
- await syncItems(many.map(m => m.path), options);
8461
- // TODO: Should we wait here or do the optimistic subscription updates like below?
8462
- return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: transactionResult.transactionSignature, signedTransaction: transactionResult.signedTransaction });
8463
8758
  }
8464
8759
  else if (setResponse.status === 200) {
8465
- // This means that the document was set successfully.
8466
- // TODO: If we're able to be smarter about it, we can return the document here, and get it back from the server
8467
- // rather than just using what we set.
8468
- // TODO: We can also trigger subscriptions here maybe? Can even recall all relevant subscriptions here.
8469
- if (setResponse.data === true) {
8470
- return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: null });
8471
- }
8472
- else if (setResponse.data &&
8473
- typeof setResponse.data === 'object' &&
8474
- setResponse.data.success === true) {
8475
- const _o = setResponse.data, { success: _success } = _o, rest = __rest$1(_o, ["success"]);
8476
- return Object.assign(Object.assign(Object.assign({}, documents.map(d => d.document)), rest), { transactionId: null });
8477
- }
8478
- else {
8479
- return Object.assign(Object.assign({}, setResponse.data), { transactionId: null });
8480
- }
8760
+ // Direct commit: the worker applied the batch itself. The receipt is
8761
+ // batch-level — `transactionId` comes from the worker's own receipt and is
8762
+ // NEVER lifted out of a result document (a delete returns the PREVIOUS
8763
+ // document, so its stale `_transaction_hash` would misreport the delete).
8764
+ const body = (setResponse.data && typeof setResponse.data === 'object' && !Array.isArray(setResponse.data))
8765
+ ? setResponse.data
8766
+ : null;
8767
+ const receipt = ((body === null || body === void 0 ? void 0 : body.receipt) && typeof body.receipt === 'object' && !Array.isArray(body.receipt))
8768
+ ? body.receipt
8769
+ : null;
8770
+ const rawTransactionId = receipt === null || receipt === void 0 ? void 0 : receipt.transactionId;
8771
+ return {
8772
+ status: 'committed',
8773
+ requestedDocuments: buildRequestedDocuments(documents),
8774
+ observation: observationFromDirectCommit(documents.map(d => d.destinationPath), receipt === null || receipt === void 0 ? void 0 : receipt.observation),
8775
+ // A worker predating the batch receipt reports no lane. Say so rather
8776
+ // than inventing one — and never discard a committed write's receipt.
8777
+ chain: ((_p = readWorkerChainTag(receipt)) !== null && _p !== void 0 ? _p : 'unknown'),
8778
+ transactionId: typeof rawTransactionId === 'string' && rawTransactionId.length > 0 ? rawTransactionId : null,
8779
+ };
8481
8780
  }
8482
8781
  else {
8483
8782
  throw new Error(`Failed to set document: ${setResponse}`);
@@ -8492,7 +8791,11 @@ async function setMany(many, options) {
8492
8791
  // slot of runTransaction (Solana slot undefined). shouldSubmitTx is honored by
8493
8792
  // the provider itself.
8494
8793
  async function handleEvmTransaction(evmTransaction, authProvider, options) {
8495
- return authProvider.runTransaction(evmTransaction, undefined, options);
8794
+ const result = await authProvider.runTransaction(evmTransaction, undefined, options);
8795
+ if ((result === null || result === void 0 ? void 0 : result.lane) !== 'evm') {
8796
+ throw new Error('The EVM auth provider returned a non-EVM transaction outcome');
8797
+ }
8798
+ return result;
8496
8799
  }
8497
8800
  }
8498
8801
  /**
@@ -8572,8 +8875,27 @@ async function getFiles(path, options) {
8572
8875
  throw error;
8573
8876
  }
8574
8877
  }
8878
+ /** Pull the worker's concrete object path (`token.pathId`) out of a response body. */
8879
+ function readWorkerFilePath(source) {
8880
+ if (!source || typeof source !== 'object')
8881
+ return null;
8882
+ for (const key of ['pathId', 'path', 'destinationPath']) {
8883
+ const value = source[key];
8884
+ if (typeof value === 'string' && value.trim().length > 0)
8885
+ return value.trim();
8886
+ }
8887
+ return null;
8888
+ }
8889
+ function readFileVisibility(source) {
8890
+ const value = (source && typeof source === 'object') ? source.visibility : undefined;
8891
+ if (value === 'public' || value === 'private' || value === 'unknown')
8892
+ return value;
8893
+ // The worker did not state it, so neither do we. `unknown` never means the
8894
+ // object is private — it means the read-rule evaluation was not reported.
8895
+ return 'unknown';
8896
+ }
8575
8897
  async function setFile(path, file, options) {
8576
- var _a;
8898
+ var _a, _b, _c, _d;
8577
8899
  // 1) Get the presigned URL from your backend
8578
8900
  const requestBody = {
8579
8901
  destinationPath: path,
@@ -8590,10 +8912,18 @@ async function setFile(path, file, options) {
8590
8912
  }
8591
8913
  }
8592
8914
  const response = await makeApiRequest('POST', 'storage/url', requestBody, options === null || options === void 0 ? void 0 : options._overrides);
8915
+ const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
8593
8916
  if (file == null) {
8594
- return true;
8917
+ // A deletion is acknowledged only by a non-404 response. `makeApiRequest`
8918
+ // normalizes 404 into `{ data: null, status: 404 }` rather than throwing, so
8919
+ // the status has to be inspected.
8920
+ const deletePath = (_a = readWorkerFilePath(response === null || response === void 0 ? void 0 : response.data)) !== null && _a !== void 0 ? _a : normalizedPath;
8921
+ if ((response === null || response === void 0 ? void 0 : response.status) === 404) {
8922
+ return { path: deletePath, deleted: false, alreadyAbsent: true };
8923
+ }
8924
+ return { path: deletePath, deleted: true };
8595
8925
  }
8596
- if (!((_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.s3Url)) {
8926
+ if (!((_b = response === null || response === void 0 ? void 0 : response.data) === null || _b === void 0 ? void 0 : _b.s3Url)) {
8597
8927
  throw new Error("No s3Url in response");
8598
8928
  }
8599
8929
  // Client-side size guard using the limit returned by the server
@@ -8602,20 +8932,62 @@ async function setFile(path, file, options) {
8602
8932
  throw new Error(`File size ${(file.size / (1024 * 1024)).toFixed(2)} MB exceeds the maximum upload size of ${(maxFileSize / (1024 * 1024)).toFixed(0)} MB.`);
8603
8933
  }
8604
8934
  const presignedUrl = response.data.s3Url;
8605
- // 2) Upload the file directly to S3 with a PUT request
8606
- const uploadResponse = await fetch(presignedUrl, {
8607
- method: 'PUT',
8608
- headers: {
8609
- 'Content-Type': file.type,
8610
- },
8611
- body: file
8612
- });
8613
- // 3) Check if the upload was successful
8935
+ // Retain the CONCRETE path BEFORE the PUT. Storage generates a collection
8936
+ // upload's id server-side, so `setFile("metadata", file)` creates
8937
+ // `metadata/<generated-id>`; if the commit succeeds and only its response is
8938
+ // lost, this is the only identifier the object has.
8939
+ const retainedPath = (_c = readWorkerFilePath(response.data)) !== null && _c !== void 0 ? _c : normalizedPath;
8940
+ // 2) Upload the file directly to storage with a PUT request
8941
+ let uploadResponse;
8942
+ try {
8943
+ uploadResponse = await fetch(presignedUrl, {
8944
+ method: 'PUT',
8945
+ headers: {
8946
+ 'Content-Type': file.type,
8947
+ },
8948
+ body: file
8949
+ });
8950
+ }
8951
+ catch (error) {
8952
+ // The transport itself failed, so we do not know whether the object
8953
+ // committed. This is NOT a definite failure and NOT retry-safe.
8954
+ const message = typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' ? error.message : String(error);
8955
+ return {
8956
+ status: 'unknown',
8957
+ path: retainedPath,
8958
+ reason: `the upload response was lost before it could be read: ${message}`,
8959
+ retrySafe: false,
8960
+ };
8961
+ }
8962
+ // 3) A non-2xx answer is a definite, unambiguous failure — keep throwing.
8614
8963
  if (!uploadResponse.ok) {
8615
- await uploadResponse.text();
8964
+ await uploadResponse.text().catch(() => '');
8616
8965
  throw new Error(`Upload failed with status: ${uploadResponse.status}`);
8617
8966
  }
8618
- return true;
8967
+ // 4) The PUT response IS the finalize response: it carries the durable URL
8968
+ // and the anonymous-read verdict, inside the worker's `{ data, status }`
8969
+ // envelope.
8970
+ let finalizeBody = null;
8971
+ try {
8972
+ const parsed = await uploadResponse.json();
8973
+ finalizeBody = (parsed && typeof parsed === 'object' && parsed.data && typeof parsed.data === 'object')
8974
+ ? parsed.data
8975
+ : parsed;
8976
+ }
8977
+ catch (_e) {
8978
+ // A worker predating the URL-carrying finalize response answers with
8979
+ // something we cannot parse. The object still committed.
8980
+ finalizeBody = null;
8981
+ }
8982
+ const url = (finalizeBody && typeof finalizeBody.url === 'string' && finalizeBody.url.length > 0)
8983
+ ? finalizeBody.url
8984
+ : null;
8985
+ return {
8986
+ status: 'uploaded',
8987
+ path: (_d = readWorkerFilePath(finalizeBody)) !== null && _d !== void 0 ? _d : retainedPath,
8988
+ url,
8989
+ visibility: readFileVisibility(finalizeBody),
8990
+ };
8619
8991
  }
8620
8992
  async function signMessage(message) {
8621
8993
  const config = await getConfig();
@@ -8652,6 +9024,7 @@ var operations = /*#__PURE__*/Object.freeze({
8652
9024
  __proto__: null,
8653
9025
  InsufficientBalanceError: InsufficientBalanceError,
8654
9026
  aggregate: aggregate,
9027
+ attemptMirrorSync: attemptMirrorSync,
8655
9028
  clearReadCacheForAuthChange: clearReadCacheForAuthChange,
8656
9029
  count: count,
8657
9030
  docId: docId,
@@ -8674,6 +9047,341 @@ var operations = /*#__PURE__*/Object.freeze({
8674
9047
  syncItems: syncItems
8675
9048
  });
8676
9049
 
9050
+ const DEFAULT_TIMEOUT_MS = 60000;
9051
+ const DEFAULT_MAX_ATTEMPTS = 20;
9052
+ const DEFAULT_POLL_INTERVAL_MS = 3000;
9053
+ const DEFAULT_MIN_REBROADCAST_INTERVAL_MS = 15000;
9054
+ /** Mirror read after a late confirmation. No settle delay: the tx already landed. */
9055
+ const RECONCILE_MIRROR_SETTLE_MS = 0;
9056
+ function errorText(error) {
9057
+ const message = error === null || error === void 0 ? void 0 : error.message;
9058
+ return typeof message === 'string' && message.length > 0 ? message : String(error);
9059
+ }
9060
+ function isSolanaSubmitted(receipt) {
9061
+ return receipt.status === 'submitted' && 'signedTransaction' in receipt;
9062
+ }
9063
+ function isEvmFencedSubmitted(receipt) {
9064
+ return receipt.status === 'submitted' && 'fence' in receipt && !('signedTransaction' in receipt);
9065
+ }
9066
+ function isEvmHashKnownSubmitted(receipt) {
9067
+ return receipt.status === 'submitted' && receipt.rebroadcastable === false;
9068
+ }
9069
+ /**
9070
+ * The Solana execution lane a client config REQUIRES, or `null` when the config
9071
+ * carries no constraint. Only a positive contradiction rejects a receipt — an
9072
+ * unmapped or unset `chain` is not evidence of a mismatch.
9073
+ */
9074
+ function requiredSolanaChain(config) {
9075
+ switch (config.chain) {
9076
+ case 'realtime_devnet':
9077
+ case 'solana_devnet':
9078
+ return 'solana_devnet';
9079
+ case 'realtime_mainnet':
9080
+ case 'realtime_mainnet_preview':
9081
+ case 'solana_mainnet':
9082
+ return 'solana_mainnet';
9083
+ default:
9084
+ return null;
9085
+ }
9086
+ }
9087
+ /**
9088
+ * Bind the receipt to the app + chain it was issued for BEFORE any RPC or
9089
+ * mirror request. Writes bind to an immutable config, so a bare reconcile after
9090
+ * a global re-`init()` could otherwise poll the wrong app's endpoints entirely.
9091
+ */
9092
+ function assertReceiptMatchesContext(receipt, config) {
9093
+ var _a;
9094
+ if (!receipt.appId || typeof receipt.appId !== 'string') {
9095
+ throw new Error('This write receipt carries no appId and cannot be reconciled.');
9096
+ }
9097
+ if (receipt.appId !== config.appId) {
9098
+ throw new Error(`This write receipt was issued for app "${receipt.appId}" but the bound client targets "${config.appId}". ` +
9099
+ 'Reconcile it through the client it came from.');
9100
+ }
9101
+ if (isSolanaSubmitted(receipt)) {
9102
+ const required = requiredSolanaChain(config);
9103
+ if (required && required !== receipt.chain) {
9104
+ throw new Error(`This write receipt ran on "${receipt.chain}" but the bound client targets "${required}".`);
9105
+ }
9106
+ return;
9107
+ }
9108
+ const fence = receipt.fence;
9109
+ if (fence && typeof fence.chainId === 'number') {
9110
+ const evm = config.evmWalletConfig;
9111
+ const configured = new Set();
9112
+ if (evm && typeof evm.chainId === 'number')
9113
+ configured.add(evm.chainId);
9114
+ for (const id of Object.keys((_a = evm === null || evm === void 0 ? void 0 : evm.runtimeAddresses) !== null && _a !== void 0 ? _a : {})) {
9115
+ const parsed = Number(id);
9116
+ if (Number.isSafeInteger(parsed))
9117
+ configured.add(parsed);
9118
+ }
9119
+ if (configured.size > 0 && !configured.has(fence.chainId)) {
9120
+ throw new Error(`This write receipt ran on EVM chain ${fence.chainId}, which the bound client does not configure.`);
9121
+ }
9122
+ }
9123
+ }
9124
+ function stillSubmitted(receipt, reason) {
9125
+ return Object.assign(Object.assign({}, receipt), { confirmation: Object.assign(Object.assign({}, receipt.confirmation), { reason }) });
9126
+ }
9127
+ async function mirrorFor(receipt, options) {
9128
+ const paths = receipt.requestedDocuments.map(entry => entry.path);
9129
+ if (paths.length === 0)
9130
+ return { observed: false, reason: 'the receipt carried no requested paths' };
9131
+ const setOptions = (options === null || options === void 0 ? void 0 : options._overrides) ? { _overrides: options._overrides } : undefined;
9132
+ return attemptMirrorSync(paths, setOptions, RECONCILE_MIRROR_SETTLE_MS);
9133
+ }
9134
+ async function resolveSolanaRpc(config, options) {
9135
+ var _a, _b;
9136
+ const injected = (_a = options === null || options === void 0 ? void 0 : options._overrides) === null || _a === void 0 ? void 0 : _a._solanaRpc;
9137
+ if (injected)
9138
+ return injected;
9139
+ const rpcUrl = (_b = config.rpcUrl) === null || _b === void 0 ? void 0 : _b.trim();
9140
+ if (!rpcUrl) {
9141
+ throw new Error('Reconciling a Solana write receipt requires init({ rpcUrl }).');
9142
+ }
9143
+ const { Connection } = await import('@solana/web3.js');
9144
+ return new Connection(rpcUrl, 'confirmed');
9145
+ }
9146
+ /**
9147
+ * Solana reconciliation NEVER broadcasts. It reaches `expired_not_landed` only
9148
+ * after the fence is observed expired AND history successfully proves the
9149
+ * signature is absent; a failed or inconclusive lookup stays `submitted`.
9150
+ */
9151
+ async function reconcileSolana(receipt, config, options) {
9152
+ var _a, _b, _c, _d;
9153
+ const rpc = await resolveSolanaRpc(config, options);
9154
+ let statusValue = null;
9155
+ let contextSlot;
9156
+ try {
9157
+ if (typeof rpc.getSignatureStatuses !== 'function') {
9158
+ return stillSubmitted(receipt, 'the configured Solana RPC cannot report signature statuses');
9159
+ }
9160
+ const statuses = await rpc.getSignatureStatuses([receipt.transactionId]);
9161
+ statusValue = (_b = (_a = statuses === null || statuses === void 0 ? void 0 : statuses.value) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null;
9162
+ contextSlot = (_c = statuses === null || statuses === void 0 ? void 0 : statuses.context) === null || _c === void 0 ? void 0 : _c.slot;
9163
+ }
9164
+ catch (error) {
9165
+ return stillSubmitted(receipt, `signature status lookup failed: ${errorText(error)}`);
9166
+ }
9167
+ if (statusValue) {
9168
+ // Landed. A landed-but-failed transaction is a genuine failure, not `submitted`.
9169
+ if (statusValue.err) {
9170
+ throw new Error(`Transaction failed: ${JSON.stringify(statusValue.err)}`);
9171
+ }
9172
+ const confirmed = {
9173
+ status: 'confirmed',
9174
+ appId: receipt.appId,
9175
+ requestedDocuments: receipt.requestedDocuments,
9176
+ chain: receipt.chain,
9177
+ transactionId: receipt.transactionId,
9178
+ signedTransaction: receipt.signedTransaction,
9179
+ fence: receipt.fence,
9180
+ confirmationContextSlot: contextSlot !== null && contextSlot !== void 0 ? contextSlot : 0,
9181
+ mirrorSync: await mirrorFor(receipt, options),
9182
+ };
9183
+ return confirmed;
9184
+ }
9185
+ // Absent from the status window. Expiry has to be OBSERVED before absence can
9186
+ // mean anything: before the fence expires the transaction may still land.
9187
+ let blockHeight;
9188
+ try {
9189
+ blockHeight = typeof rpc.getBlockHeight === 'function' ? await rpc.getBlockHeight('confirmed') : undefined;
9190
+ }
9191
+ catch (error) {
9192
+ return stillSubmitted(receipt, `block height lookup failed: ${errorText(error)}`);
9193
+ }
9194
+ if (typeof blockHeight !== 'number') {
9195
+ return stillSubmitted(receipt, 'expiry could not be observed: the RPC reported no block height');
9196
+ }
9197
+ if (blockHeight <= receipt.fence.lastValidBlockHeight) {
9198
+ return stillSubmitted(receipt, `not yet expired: block height ${blockHeight} is within the fence (lastValidBlockHeight ${receipt.fence.lastValidBlockHeight})`);
9199
+ }
9200
+ // Expired. Prove absence from history before declaring the write retry-safe.
9201
+ let history;
9202
+ try {
9203
+ history = await rpc.getTransaction(receipt.transactionId, {
9204
+ maxSupportedTransactionVersion: 0,
9205
+ commitment: 'confirmed',
9206
+ });
9207
+ }
9208
+ catch (error) {
9209
+ return stillSubmitted(receipt, `history lookup failed after expiry: ${errorText(error)}`);
9210
+ }
9211
+ if (history) {
9212
+ if ((_d = history === null || history === void 0 ? void 0 : history.meta) === null || _d === void 0 ? void 0 : _d.err) {
9213
+ throw new Error(`Transaction failed: ${JSON.stringify(history.meta.err)}`);
9214
+ }
9215
+ const confirmed = {
9216
+ status: 'confirmed',
9217
+ appId: receipt.appId,
9218
+ requestedDocuments: receipt.requestedDocuments,
9219
+ chain: receipt.chain,
9220
+ transactionId: receipt.transactionId,
9221
+ signedTransaction: receipt.signedTransaction,
9222
+ fence: receipt.fence,
9223
+ confirmationContextSlot: typeof history.slot === 'number' ? history.slot : (contextSlot !== null && contextSlot !== void 0 ? contextSlot : 0),
9224
+ mirrorSync: await mirrorFor(receipt, options),
9225
+ };
9226
+ return confirmed;
9227
+ }
9228
+ return {
9229
+ status: 'expired_not_landed',
9230
+ appId: receipt.appId,
9231
+ requestedDocuments: receipt.requestedDocuments,
9232
+ chain: receipt.chain,
9233
+ transactionId: receipt.transactionId,
9234
+ fence: receipt.fence,
9235
+ };
9236
+ }
9237
+ /**
9238
+ * EVM reconciliation runs through the provider's own transport, so a wallet
9239
+ * client reconciles with its own identity and RPC. There is deliberately NO
9240
+ * EVM retry-safe terminal state: nonce advancement is not proof the write did
9241
+ * not execute, and plain JSON-RPC offers no transaction-by-sender-and-nonce
9242
+ * lookup, so a `submitted` EVM receipt either confirms or stays `submitted`.
9243
+ */
9244
+ async function reconcileEvm(receipt, provider, options) {
9245
+ if (!provider || typeof provider.reconcileEvmTransaction !== 'function') {
9246
+ return stillSubmitted(receipt, 'no EVM reconciliation transport is available on the bound auth provider');
9247
+ }
9248
+ const fenced = isEvmFencedSubmitted(receipt) ? receipt.fence : undefined;
9249
+ const allowRebroadcast = Boolean(fenced) && (options === null || options === void 0 ? void 0 : options.allowRebroadcast) !== false;
9250
+ let outcome;
9251
+ try {
9252
+ outcome = await provider.reconcileEvmTransaction({
9253
+ transactionHash: receipt.transactionId,
9254
+ fence: fenced,
9255
+ allowRebroadcast,
9256
+ });
9257
+ }
9258
+ catch (error) {
9259
+ return stillSubmitted(receipt, `EVM reconciliation failed: ${errorText(error)}`);
9260
+ }
9261
+ if (outcome.state === 'failed') {
9262
+ // Landed and reverted: a genuine failure, never `submitted`.
9263
+ throw new Error(outcome.reason);
9264
+ }
9265
+ if (outcome.state === 'confirmed') {
9266
+ if (typeof outcome.blockNumber !== 'number') {
9267
+ return stillSubmitted(receipt, 'the EVM receipt reported success without a block number, so confirmation could not be stated');
9268
+ }
9269
+ const confirmed = {
9270
+ status: 'confirmed',
9271
+ appId: receipt.appId,
9272
+ requestedDocuments: receipt.requestedDocuments,
9273
+ chain: receipt.chain,
9274
+ transactionId: receipt.transactionId,
9275
+ blockNumber: outcome.blockNumber,
9276
+ mirrorSync: await mirrorFor(receipt, options),
9277
+ };
9278
+ return confirmed;
9279
+ }
9280
+ if (outcome.state === 'absent_after_expiry') {
9281
+ // Defensive: no EVM lane may report a retry-safe terminal state.
9282
+ return stillSubmitted(receipt, 'the EVM transport reported absence, which is not proof an EVM write did not execute');
9283
+ }
9284
+ return stillSubmitted(receipt, outcome.reason);
9285
+ }
9286
+ /**
9287
+ * Poll the SAME signature/hash a `submitted` receipt carries and return the
9288
+ * resulting `SetResult`.
9289
+ *
9290
+ * - Terminal receipts (`committed`, `confirmed`, `expired_not_landed`,
9291
+ * `signed`) are returned unchanged; nothing was left open.
9292
+ * - An irreconcilable injected-wallet receipt (`transactionId: null`) is
9293
+ * rejected immediately: no identifier exists to poll.
9294
+ * - A transaction that landed and FAILED throws, exactly as it would have on
9295
+ * the original write.
9296
+ */
9297
+ async function reconcileSetResult(receipt, options) {
9298
+ var _a, _b, _c, _d, _e;
9299
+ if (!receipt || typeof receipt !== 'object' || typeof receipt.status !== 'string') {
9300
+ throw new Error('reconcileSetResult requires a write receipt returned by set/setMany.');
9301
+ }
9302
+ if (receipt.status !== 'submitted')
9303
+ return receipt;
9304
+ if (receipt.transactionId === null) {
9305
+ throw new Error('This receipt is explicitly irreconcilable: the wallet send response was lost, so no transaction hash exists to poll. ' +
9306
+ 'Resolve it out of band — resubmitting could execute the write twice.');
9307
+ }
9308
+ const config = (_b = (_a = options === null || options === void 0 ? void 0 : options._overrides) === null || _a === void 0 ? void 0 : _a._config) !== null && _b !== void 0 ? _b : await getConfig();
9309
+ // Validation happens BEFORE any RPC or mirror request.
9310
+ assertReceiptMatchesContext(receipt, config);
9311
+ if (isSolanaSubmitted(receipt)) {
9312
+ return reconcileSolana(receipt, config, options);
9313
+ }
9314
+ const provider = (_e = (_d = (_c = options === null || options === void 0 ? void 0 : options._overrides) === null || _c === void 0 ? void 0 : _c.authProvider) !== null && _d !== void 0 ? _d : config.authProvider) !== null && _e !== void 0 ? _e : null;
9315
+ if (isEvmFencedSubmitted(receipt) || isEvmHashKnownSubmitted(receipt)) {
9316
+ return reconcileEvm(receipt, provider, options);
9317
+ }
9318
+ return stillSubmitted(receipt, 'this receipt shape has no reconciliation lane');
9319
+ }
9320
+ /**
9321
+ * Reconcile repeatedly until the receipt leaves `submitted` or the bound is hit.
9322
+ *
9323
+ * The bound is mandatory, because some states are permanently non-terminal by
9324
+ * design: an unidentified nonce consumer or a permanently underpriced EVM
9325
+ * transaction never resolves. On exhaustion (or abort) this returns the LATEST
9326
+ * `submitted` receipt with its identifier intact rather than hanging or
9327
+ * throwing the identifier away.
9328
+ */
9329
+ async function waitForSetResult(receipt, options) {
9330
+ var _a, _b, _c, _d, _e;
9331
+ if (receipt && typeof receipt === 'object' && receipt.status === 'submitted' && receipt.transactionId === null) {
9332
+ // Rejected immediately rather than polled: there is nothing to poll.
9333
+ return reconcileSetResult(receipt, options);
9334
+ }
9335
+ const timeoutMs = (_a = options === null || options === void 0 ? void 0 : options.timeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_TIMEOUT_MS;
9336
+ const maxAttempts = (_b = options === null || options === void 0 ? void 0 : options.maxAttempts) !== null && _b !== void 0 ? _b : DEFAULT_MAX_ATTEMPTS;
9337
+ const pollIntervalMs = (_c = options === null || options === void 0 ? void 0 : options.pollIntervalMs) !== null && _c !== void 0 ? _c : DEFAULT_POLL_INTERVAL_MS;
9338
+ const minRebroadcastIntervalMs = (_d = options === null || options === void 0 ? void 0 : options.minRebroadcastIntervalMs) !== null && _d !== void 0 ? _d : DEFAULT_MIN_REBROADCAST_INTERVAL_MS;
9339
+ const rebroadcastPermitted = (options === null || options === void 0 ? void 0 : options.allowRebroadcast) !== false;
9340
+ const startedAt = Date.now();
9341
+ let current = receipt;
9342
+ let attempts = 0;
9343
+ let lastRebroadcastAt = -Infinity;
9344
+ while (attempts < maxAttempts && Date.now() - startedAt < timeoutMs) {
9345
+ if ((_e = options === null || options === void 0 ? void 0 : options.signal) === null || _e === void 0 ? void 0 : _e.aborted)
9346
+ return current;
9347
+ const now = Date.now();
9348
+ const allowRebroadcast = rebroadcastPermitted && (now - lastRebroadcastAt >= minRebroadcastIntervalMs);
9349
+ if (allowRebroadcast)
9350
+ lastRebroadcastAt = now;
9351
+ attempts++;
9352
+ current = await reconcileSetResult(current, Object.assign(Object.assign({}, options), { allowRebroadcast }));
9353
+ if (current.status !== 'submitted')
9354
+ return current;
9355
+ if (attempts >= maxAttempts)
9356
+ break;
9357
+ const remaining = timeoutMs - (Date.now() - startedAt);
9358
+ if (remaining <= 0)
9359
+ break;
9360
+ const slept = await sleepUnlessAborted(Math.min(pollIntervalMs, remaining), options === null || options === void 0 ? void 0 : options.signal);
9361
+ if (!slept)
9362
+ return current;
9363
+ }
9364
+ return current;
9365
+ }
9366
+ /** Resolves `false` when the signal aborted during (or before) the sleep. */
9367
+ function sleepUnlessAborted(ms, signal) {
9368
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
9369
+ return Promise.resolve(false);
9370
+ if (!signal)
9371
+ return new Promise(resolve => setTimeout(() => resolve(true), ms));
9372
+ return new Promise(resolve => {
9373
+ const timer = setTimeout(() => {
9374
+ signal.removeEventListener('abort', onAbort);
9375
+ resolve(true);
9376
+ }, ms);
9377
+ const onAbort = () => {
9378
+ clearTimeout(timer);
9379
+ resolve(false);
9380
+ };
9381
+ signal.addEventListener('abort', onAbort, { once: true });
9382
+ });
9383
+ }
9384
+
8677
9385
  /**
8678
9386
  * WebSocket Subscription Module
8679
9387
  *
@@ -9515,7 +10223,7 @@ function toMillis(seconds) {
9515
10223
 
9516
10224
  /** SIWS message creation is async already, so its onchain module can be lazy. */
9517
10225
  async function genSolanaMessage(address, nonce) {
9518
- const implementation = await Promise.resolve().then(function () { return require('./solana-message-D_CN0H6r.js'); });
10226
+ const implementation = await Promise.resolve().then(function () { return require('./solana-message-Bycv-L5n.js'); });
9519
10227
  return implementation.genSolanaMessage(address, nonce);
9520
10228
  }
9521
10229
 
@@ -10044,6 +10752,7 @@ exports.normalizeEvmAddress = normalizeEvmAddress;
10044
10752
  exports.now = now;
10045
10753
  exports.onSessionCleared = onSessionCleared;
10046
10754
  exports.queryAggregate = queryAggregate;
10755
+ exports.reconcileSetResult = reconcileSetResult;
10047
10756
  exports.reconnectWithNewAuth = reconnectWithNewAuth;
10048
10757
  exports.refreshSession = refreshSession;
10049
10758
  exports.revokeSession = revokeSession;
@@ -10067,5 +10776,6 @@ exports.subscribeView = subscribeView;
10067
10776
  exports.toLowerEvmAddress = toLowerEvmAddress;
10068
10777
  exports.toMillis = toMillis;
10069
10778
  exports.toSeconds = toSeconds;
10779
+ exports.waitForSetResult = waitForSetResult;
10070
10780
  exports.withEffects = withEffects;
10071
- //# sourceMappingURL=index-C8ocsC_W.js.map
10781
+ //# sourceMappingURL=index-DNZifL5j.js.map