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