@routedock/routedock 0.3.1 → 0.4.0

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 (58) hide show
  1. package/README.md +113 -12
  2. package/dist/{MppSessionHandler-1EL8QBAn.d.cts → MppSessionHandler-3QfCHI5x.d.ts} +40 -2
  3. package/dist/{MppSessionHandler-Bq4S-JTE.d.ts → MppSessionHandler-xu4Kxu6N.d.cts} +40 -2
  4. package/dist/{ProviderRegistry-w4_hkI_1.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
  5. package/dist/{ProviderRegistry-CyZ5eryO.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
  6. package/dist/{RouteDockClient-CjM2VyOd.d.ts → RouteDockClient-6rmgkbYb.d.cts} +124 -144
  7. package/dist/{RouteDockClient-CXvWXix5.d.cts → RouteDockClient-CnAkxwrW.d.ts} +124 -144
  8. package/dist/{SessionReconciler-Bw-PoBVl.d.ts → SessionReconciler-Cce9MXO1.d.ts} +10 -3
  9. package/dist/{SessionReconciler-Cz2nuOlJ.d.cts → SessionReconciler-Dpe9qXdD.d.cts} +10 -3
  10. package/dist/{SessionStore-BaYiegP0.d.ts → SessionStore-DkLBxFQu.d.cts} +28 -3
  11. package/dist/{SessionStore-B4-Hi4H8.d.cts → SessionStore-_ZkoBzFY.d.ts} +28 -3
  12. package/dist/SpendStore-BUclV02i.d.ts +73 -0
  13. package/dist/SpendStore-s_dfcemj.d.cts +73 -0
  14. package/dist/client.cjs +767 -482
  15. package/dist/client.d.cts +80 -10
  16. package/dist/client.d.ts +80 -10
  17. package/dist/client.js +733 -463
  18. package/dist/index.cjs +1295 -585
  19. package/dist/index.d.cts +13 -10
  20. package/dist/index.d.ts +13 -10
  21. package/dist/index.js +1247 -554
  22. package/dist/logger-DVa7VT4c.d.cts +41 -0
  23. package/dist/logger-DVa7VT4c.d.ts +41 -0
  24. package/dist/network.cjs +57 -0
  25. package/dist/network.d.cts +72 -0
  26. package/dist/network.d.ts +72 -0
  27. package/dist/network.js +49 -0
  28. package/dist/node.cjs +166 -0
  29. package/dist/node.d.cts +77 -0
  30. package/dist/node.d.ts +77 -0
  31. package/dist/node.js +159 -0
  32. package/dist/provider/fastify.cjs +847 -62
  33. package/dist/provider/fastify.d.cts +10 -3
  34. package/dist/provider/fastify.d.ts +10 -3
  35. package/dist/provider/fastify.js +848 -63
  36. package/dist/provider/hono.cjs +936 -60
  37. package/dist/provider/hono.d.cts +23 -8
  38. package/dist/provider/hono.d.ts +23 -8
  39. package/dist/provider/hono.js +937 -61
  40. package/dist/provider.cjs +894 -70
  41. package/dist/provider.d.cts +9 -8
  42. package/dist/provider.d.ts +9 -8
  43. package/dist/provider.js +895 -72
  44. package/dist/react.cjs +736 -431
  45. package/dist/react.d.cts +7 -5
  46. package/dist/react.d.ts +7 -5
  47. package/dist/react.js +725 -420
  48. package/dist/{routedockMiddleware-57gUaEMf.d.ts → routedockMiddleware-Cq9HOJEa.d.ts} +9 -2
  49. package/dist/{routedockMiddleware-DlyLrxp7.d.cts → routedockMiddleware-CxsWkSKa.d.cts} +9 -2
  50. package/dist/schema.cjs +7 -0
  51. package/dist/schema.js +7 -0
  52. package/dist/{sign-AKH67sft.d.cts → sign-Bq08SeFq.d.ts} +2 -2
  53. package/dist/{sign-BEq-OsSq.d.ts → sign-DqGE0Plv.d.cts} +2 -2
  54. package/dist/testing.d.cts +4 -3
  55. package/dist/testing.d.ts +4 -3
  56. package/dist/{logger-D8_Q2ytp.d.cts → types-DXaS5Wip.d.cts} +85 -47
  57. package/dist/{logger-D8_Q2ytp.d.ts → types-DXaS5Wip.d.ts} +85 -47
  58. package/package.json +50 -4
package/dist/index.cjs CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  var nulthSdk = require('@routedock/nulth-sdk');
4
4
  var stellarSdk = require('@stellar/stellar-sdk');
5
+ var sha2_js = require('@noble/hashes/sha2.js');
5
6
  var jsonSchema = require('@cfworker/json-schema');
6
- var crypto$1 = require('crypto');
7
7
  var stellar = require('@x402/stellar');
8
- var client = require('@x402/stellar/exact/client');
9
- var client$1 = require('@x402/core/client');
8
+ var client = require('@x402/core/client');
10
9
  var http = require('@x402/core/http');
11
- var client$3 = require('@stellar/mpp/charge/client');
12
- var client$2 = require('mppx/client');
13
- var mpp = require('@stellar/mpp');
14
- var client$4 = require('@stellar/mpp/channel/client');
10
+ var client$2 = require('@stellar/mpp/charge/client');
11
+ var client$1 = require('mppx/client');
15
12
  var mppx = require('mppx');
13
+ var mpp = require('@stellar/mpp');
14
+ var client$3 = require('@stellar/mpp/channel/client');
15
+ var crypto$1 = require('crypto');
16
16
  var fs = require('fs/promises');
17
17
  var path = require('path');
18
18
  var facilitator = require('@x402/stellar/exact/facilitator');
@@ -123,6 +123,20 @@ var RouteDockPolicyRejectError = class extends RouteDockError {
123
123
  this.reason = reason;
124
124
  }
125
125
  };
126
+ var RouteDockPaymentRejectedError = class extends RouteDockError {
127
+ status;
128
+ /** The provider's `reason` field, if its response body was JSON and had one. */
129
+ reason;
130
+ /** The provider's `error` field (short summary), if present. */
131
+ providerError;
132
+ constructor(message, status, details = {}, options = {}) {
133
+ super(message, "PAYMENT_REJECTED", false, options);
134
+ this.name = "RouteDockPaymentRejectedError";
135
+ this.status = status;
136
+ if (details.reason !== void 0) this.reason = details.reason;
137
+ if (details.providerError !== void 0) this.providerError = details.providerError;
138
+ }
139
+ };
126
140
  var RouteDockChannelStateError = class extends RouteDockError {
127
141
  constructor(message, options = {}) {
128
142
  super(message, "CHANNEL_STATE", false, options);
@@ -167,6 +181,59 @@ function httpStatusToError(message, status, response) {
167
181
  }
168
182
  return new RouteDockNetworkError(message, opts);
169
183
  }
184
+ var MAX_REJECTION_BODY_BYTES = 64 * 1024;
185
+ var MAX_REJECTION_FIELD_CHARS = 500;
186
+ async function readBoundedText(response, limit) {
187
+ const body = response.body;
188
+ if (!body) return null;
189
+ const reader = body.getReader();
190
+ const chunks = [];
191
+ let total = 0;
192
+ try {
193
+ for (; ; ) {
194
+ const { done, value } = await reader.read();
195
+ if (done) break;
196
+ total += value.byteLength;
197
+ if (total > limit) {
198
+ await reader.cancel().catch(() => void 0);
199
+ return null;
200
+ }
201
+ chunks.push(value);
202
+ }
203
+ } catch {
204
+ return null;
205
+ }
206
+ const bytes = new Uint8Array(total);
207
+ let offset = 0;
208
+ for (const chunk of chunks) {
209
+ bytes.set(chunk, offset);
210
+ offset += chunk.byteLength;
211
+ }
212
+ return new TextDecoder().decode(bytes);
213
+ }
214
+ async function paymentRejectedError(label, status, response) {
215
+ let reason;
216
+ let providerError;
217
+ try {
218
+ const text = await readBoundedText(response, MAX_REJECTION_BODY_BYTES);
219
+ const body = text === null ? null : JSON.parse(text);
220
+ if (body !== null && typeof body === "object") {
221
+ const { reason: r, error: e } = body;
222
+ if (typeof r === "string" && r.length > 0) reason = r.slice(0, MAX_REJECTION_FIELD_CHARS);
223
+ if (typeof e === "string" && e.length > 0) providerError = e.slice(0, MAX_REJECTION_FIELD_CHARS);
224
+ }
225
+ } catch {
226
+ }
227
+ const detail = reason ?? providerError;
228
+ return new RouteDockPaymentRejectedError(
229
+ `${label}: HTTP ${status}${detail ? ` \u2014 ${detail}` : ""}`,
230
+ status,
231
+ {
232
+ ...reason !== void 0 ? { reason } : {},
233
+ ...providerError !== void 0 ? { providerError } : {}
234
+ }
235
+ );
236
+ }
170
237
  function wrapFetchError(err, context) {
171
238
  if (err instanceof RouteDockError) return err;
172
239
  return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
@@ -231,23 +298,34 @@ async function withRetry(fn, policy = {}) {
231
298
  if (!err.retryable || attempt >= maxAttempts - 1) {
232
299
  throw err;
233
300
  }
234
- const delay = err.retryAfterMs !== void 0 ? err.retryAfterMs : backoffDelayMs(attempt, baseDelayMs, maxDelayMs);
301
+ const delay2 = Math.min(
302
+ err.retryAfterMs ?? backoffDelayMs(attempt, baseDelayMs, maxDelayMs),
303
+ maxDelayMs
304
+ );
235
305
  if (onRetry) {
236
306
  try {
237
- await onRetry(attempt, err, delay);
307
+ await onRetry(attempt, err, delay2);
238
308
  } catch (cbErr) {
239
309
  log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
240
310
  }
241
311
  }
242
- await sleep(delay);
312
+ await sleep(delay2);
243
313
  }
244
314
  }
245
315
  throw lastError ?? new RouteDockError("Retry exhausted", "NETWORK", false);
246
316
  }
317
+
318
+ // src/network.ts
247
319
  var USDC_ISSUERS = {
248
320
  testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
249
321
  mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
250
322
  };
323
+ var HORIZON_URLS = {
324
+ testnet: "https://horizon-testnet.stellar.org",
325
+ mainnet: "https://horizon.stellar.org"
326
+ };
327
+
328
+ // src/internal/usdc.ts
251
329
  var usdcToUnits = nulthSdk.usdcToStroops;
252
330
 
253
331
  // src/internal/assetUtils.ts
@@ -328,6 +406,138 @@ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
328
406
  );
329
407
  }
330
408
 
409
+ // package.json
410
+ var package_default = {
411
+ version: "0.4.0"};
412
+
413
+ // src/internal/encoding.ts
414
+ var encoder = new TextEncoder();
415
+ var decoder = new TextDecoder();
416
+ function utf8ToBytes(text) {
417
+ return encoder.encode(text);
418
+ }
419
+ function bytesToUtf8(bytes) {
420
+ return decoder.decode(bytes);
421
+ }
422
+ function bytesToBase64(bytes) {
423
+ const chunk = 32768;
424
+ let binary = "";
425
+ for (let i = 0; i < bytes.length; i += chunk) {
426
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
427
+ }
428
+ return btoa(binary);
429
+ }
430
+ function base64ToBytes(b64) {
431
+ const end = b64.indexOf("=");
432
+ let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
433
+ if (clean.length % 4 === 1) clean = clean.slice(0, -1);
434
+ const binary = atob(clean);
435
+ const bytes = new Uint8Array(binary.length);
436
+ for (let i = 0; i < binary.length; i++) {
437
+ bytes[i] = binary.charCodeAt(i);
438
+ }
439
+ return bytes;
440
+ }
441
+ function base64ToUtf8(b64) {
442
+ return bytesToUtf8(base64ToBytes(b64));
443
+ }
444
+ function hexToBytes(hex) {
445
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
446
+ if (clean.length % 2 !== 0) {
447
+ throw new Error("hexToBytes: hex string must have an even length");
448
+ }
449
+ const bytes = new Uint8Array(clean.length / 2);
450
+ for (let i = 0; i < bytes.length; i++) {
451
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
452
+ }
453
+ return bytes;
454
+ }
455
+ function bytesToHex(bytes) {
456
+ let out = "";
457
+ for (let i = 0; i < bytes.length; i++) {
458
+ out += bytes[i].toString(16).padStart(2, "0");
459
+ }
460
+ return out;
461
+ }
462
+ function sha256(data) {
463
+ return sha2_js.sha256(typeof data === "string" ? utf8ToBytes(data) : data);
464
+ }
465
+
466
+ // src/manifest/sign.ts
467
+ var MANIFEST_SIGNATURE_VERSION = "2";
468
+ function canonicalize(value) {
469
+ if (Array.isArray(value)) {
470
+ return value.map(canonicalize);
471
+ }
472
+ if (value !== null && typeof value === "object") {
473
+ return Object.fromEntries(
474
+ Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
475
+ );
476
+ }
477
+ return value;
478
+ }
479
+ function manifestDigest(manifest) {
480
+ const { signature: _omit, ...rest } = manifest;
481
+ const canonical = JSON.stringify(canonicalize(rest));
482
+ return sha256(utf8ToBytes(canonical));
483
+ }
484
+ function signManifest(manifest, payeeSecretKey) {
485
+ const keypair = stellarSdk.Keypair.fromSecret(payeeSecretKey);
486
+ const versionedManifest = {
487
+ ...manifest,
488
+ signature_version: MANIFEST_SIGNATURE_VERSION
489
+ };
490
+ const digest = manifestDigest(versionedManifest);
491
+ const sig = keypair.sign(digest);
492
+ return { ...versionedManifest, signature: bytesToBase64(sig) };
493
+ }
494
+ function verifyManifestSignature(manifest, expectedPayee) {
495
+ if (!manifest.signature) {
496
+ throw new RouteDockSignatureError(
497
+ "Manifest is missing a signature field \u2014 cannot verify payee authenticity"
498
+ );
499
+ }
500
+ if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
501
+ throw new RouteDockSignatureError(
502
+ `Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
503
+ );
504
+ }
505
+ if (expectedPayee !== void 0 && manifest.payee !== expectedPayee) {
506
+ throw new RouteDockSignatureError(
507
+ `Manifest payee ${manifest.payee} does not match expected payee ${expectedPayee}`
508
+ );
509
+ }
510
+ let keypair;
511
+ try {
512
+ keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
513
+ } catch (err) {
514
+ throw new RouteDockSignatureError(
515
+ `Manifest payee ${manifest.payee} is not a valid Stellar public key`,
516
+ { cause: err }
517
+ );
518
+ }
519
+ const digest = manifestDigest(manifest);
520
+ let sigBytes;
521
+ try {
522
+ sigBytes = base64ToBytes(manifest.signature);
523
+ } catch {
524
+ throw new RouteDockSignatureError("Manifest signature is not valid base64");
525
+ }
526
+ let valid = false;
527
+ try {
528
+ valid = keypair.verify(digest, sigBytes);
529
+ } catch {
530
+ throw new RouteDockSignatureError(
531
+ `Manifest signature verification failed for payee ${manifest.payee}`
532
+ );
533
+ }
534
+ if (!valid) {
535
+ throw new RouteDockSignatureError(
536
+ `Manifest signature verification failed for payee ${manifest.payee}`
537
+ );
538
+ }
539
+ }
540
+
331
541
  // src/schemas/routedock.schema.json
332
542
  var routedock_schema_default = {
333
543
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -405,6 +615,7 @@ var routedock_schema_default = {
405
615
  },
406
616
  asset_contract: {
407
617
  type: "string",
618
+ pattern: "^C[A-Z2-7]{55}$",
408
619
  description: "Stellar Asset Contract (SAC) address for the payment asset"
409
620
  },
410
621
  assets: {
@@ -417,6 +628,7 @@ var routedock_schema_default = {
417
628
  },
418
629
  payee: {
419
630
  type: "string",
631
+ pattern: "^G[A-Z2-7]{55}$",
420
632
  description: "Stellar address (G...) that receives payments"
421
633
  },
422
634
  pricing: {
@@ -587,6 +799,7 @@ var routedock_schema_default = {
587
799
  properties: {
588
800
  amount: {
589
801
  type: "string",
802
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
590
803
  description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
591
804
  },
592
805
  per: {
@@ -600,6 +813,7 @@ var routedock_schema_default = {
600
813
  },
601
814
  payee: {
602
815
  type: "string",
816
+ pattern: "^G[A-Z2-7]{55}$",
603
817
  description: "Optional per-mode payee override (Stellar G... address). Directs settlements for this mode to a separate account for treasury separation. Falls back to the top-level 'payee' when omitted."
604
818
  }
605
819
  }
@@ -612,6 +826,7 @@ var routedock_schema_default = {
612
826
  properties: {
613
827
  rate: {
614
828
  type: "string",
829
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
615
830
  description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
616
831
  },
617
832
  per: {
@@ -621,10 +836,12 @@ var routedock_schema_default = {
621
836
  },
622
837
  channel_factory: {
623
838
  type: "string",
839
+ pattern: "^C[A-Z2-7]{55}$",
624
840
  description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
625
841
  },
626
842
  min_deposit: {
627
843
  type: "string",
844
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
628
845
  description: "Minimum initial deposit to open a channel (decimal string)"
629
846
  },
630
847
  refund_waiting_period_ledgers: {
@@ -722,78 +939,110 @@ var routedock_schema_default = {
722
939
  }
723
940
  };
724
941
 
725
- // package.json
726
- var package_default = {
727
- version: "0.3.1"};
728
- var MANIFEST_SIGNATURE_VERSION = "2";
729
- function canonicalize(value) {
730
- if (Array.isArray(value)) {
731
- return value.map(canonicalize);
732
- }
733
- if (value !== null && typeof value === "object") {
734
- return Object.fromEntries(
735
- Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
736
- );
942
+ // src/manifest/validate.ts
943
+ var validator = new jsonSchema.Validator(routedock_schema_default, "7");
944
+ function validateManifestSchema(manifest, source) {
945
+ const result = validator.validate(manifest);
946
+ if (!result.valid) {
947
+ const msgs = result.errors.map((err) => err.error).join("; ");
948
+ throw new RouteDockManifestError(`Invalid manifest${source ? ` at ${source}` : ""}: ${msgs}`);
737
949
  }
738
- return value;
739
950
  }
740
- function manifestDigest(manifest) {
741
- const { signature: _omit, ...rest } = manifest;
742
- const canonical = JSON.stringify(canonicalize(rest));
743
- return crypto$1.createHash("sha256").update(canonical, "utf8").digest();
744
- }
745
- function signManifest(manifest, payeeSecretKey) {
746
- const keypair = stellarSdk.Keypair.fromSecret(payeeSecretKey);
747
- const versionedManifest = {
748
- ...manifest,
749
- signature_version: MANIFEST_SIGNATURE_VERSION
750
- };
751
- const digest = manifestDigest(versionedManifest);
752
- const sig = keypair.sign(digest);
753
- return { ...versionedManifest, signature: Buffer.from(sig).toString("base64") };
754
- }
755
- function verifyManifestSignature(manifest, expectedPayee) {
756
- if (!manifest.signature) {
757
- throw new RouteDockSignatureError(
758
- "Manifest is missing a signature field \u2014 cannot verify payee authenticity"
759
- );
760
- }
761
- if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
762
- throw new RouteDockSignatureError(
763
- `Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
764
- );
951
+ function assertManifestValid(manifest, baseUrl) {
952
+ const context = baseUrl ? ` at ${baseUrl}` : "";
953
+ if (manifest.latency_hints !== void 0) {
954
+ if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
955
+ throw new RouteDockManifestError(
956
+ `Invalid manifest${context}: latency_hints defined without declaring regions`
957
+ );
958
+ }
959
+ const regionSet = new Set(manifest.regions);
960
+ for (const region of Object.keys(manifest.latency_hints)) {
961
+ if (!regionSet.has(region)) {
962
+ throw new RouteDockManifestError(
963
+ `Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
964
+ );
965
+ }
966
+ }
765
967
  }
766
- if (expectedPayee !== void 0 && manifest.payee !== expectedPayee) {
767
- throw new RouteDockSignatureError(
768
- `Manifest payee ${manifest.payee} does not match expected payee ${expectedPayee}`
769
- );
968
+ if (manifest.assets !== void 0) {
969
+ if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
970
+ throw new RouteDockManifestError(
971
+ `Invalid manifest${context}: assets must be a non-empty array when defined`
972
+ );
973
+ }
974
+ for (let i = 0; i < manifest.assets.length; i++) {
975
+ const a = manifest.assets[i];
976
+ if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
977
+ throw new RouteDockManifestError(
978
+ `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
979
+ );
980
+ }
981
+ }
982
+ const first = manifest.assets[0];
983
+ if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
984
+ throw new RouteDockManifestError(
985
+ `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
986
+ );
987
+ }
770
988
  }
771
- const keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
772
- const digest = manifestDigest(manifest);
773
- let sigBytes;
774
- try {
775
- sigBytes = Buffer.from(manifest.signature, "base64");
776
- } catch {
777
- throw new RouteDockSignatureError("Manifest signature is not valid base64");
989
+ assertPricingMatchesModes(manifest, context);
990
+ assertDeprecatedModesListed(manifest, context);
991
+ assertPricesParse(manifest, context);
992
+ }
993
+ function assertPricingMatchesModes(manifest, context) {
994
+ const pricing = manifest.pricing ?? {};
995
+ for (const mode of manifest.modes ?? []) {
996
+ if (pricing[mode] === void 0 || pricing[mode] === null) {
997
+ throw new RouteDockManifestError(
998
+ `Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
999
+ );
1000
+ }
778
1001
  }
779
- let valid = false;
780
- try {
781
- valid = keypair.verify(digest, sigBytes);
782
- } catch {
783
- throw new RouteDockSignatureError(
784
- `Manifest signature verification failed for payee ${manifest.payee}`
785
- );
1002
+ }
1003
+ function assertDeprecatedModesListed(manifest, context) {
1004
+ if (manifest.deprecated_modes === void 0) return;
1005
+ const modes = new Set(manifest.modes ?? []);
1006
+ for (const mode of manifest.deprecated_modes) {
1007
+ if (!modes.has(mode)) {
1008
+ throw new RouteDockManifestError(
1009
+ `Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
1010
+ );
1011
+ }
786
1012
  }
787
- if (!valid) {
788
- throw new RouteDockSignatureError(
789
- `Manifest signature verification failed for payee ${manifest.payee}`
790
- );
1013
+ }
1014
+ var PRICE_FIELDS = {
1015
+ x402: ["amount"],
1016
+ "mpp-charge": ["amount"],
1017
+ "mpp-session": ["rate", "min_deposit"],
1018
+ "mpp-session-ws": ["rate", "min_deposit"]
1019
+ };
1020
+ function assertPricesParse(manifest, context) {
1021
+ const pricing = manifest.pricing ?? {};
1022
+ for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
1023
+ const entry = pricing[mode];
1024
+ if (entry === void 0 || entry === null) continue;
1025
+ for (const field of fields) {
1026
+ const value = entry[field];
1027
+ try {
1028
+ if (typeof value !== "string") {
1029
+ throw new RangeError(`expected a decimal string, got ${typeof value}`);
1030
+ }
1031
+ nulthSdk.usdcToStroops(value);
1032
+ } catch (err) {
1033
+ const detail = err instanceof Error ? err.message : String(err);
1034
+ throw new RouteDockManifestError(
1035
+ `Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
1036
+ { cause: err }
1037
+ );
1038
+ }
1039
+ }
791
1040
  }
792
1041
  }
793
1042
 
794
1043
  // src/client/ModeRouter.ts
795
- var validator = new jsonSchema.Validator(routedock_schema_default, "7");
796
1044
  var SDK_VERSION = package_default.version;
1045
+ var SUPPORTED_MANIFEST_MAJOR = 1;
797
1046
  function parseMajorMinor(version) {
798
1047
  const parts = version.split(".");
799
1048
  return [Number.parseInt(parts[0] ?? "0", 10), Number.parseInt(parts[1] ?? "0", 10)];
@@ -811,6 +1060,14 @@ function assertClientVersionSupported(manifest, baseUrl) {
811
1060
  );
812
1061
  }
813
1062
  }
1063
+ function assertManifestVersionSupported(manifest, baseUrl) {
1064
+ const [major] = parseMajorMinor(manifest.routedock);
1065
+ if (major > SUPPORTED_MANIFEST_MAJOR) {
1066
+ throw new RouteDockClientVersionError(
1067
+ `Manifest version ${manifest.routedock} from provider at ${baseUrl} has schema major version ${major}, which is newer than the highest version this SDK supports (${SUPPORTED_MANIFEST_MAJOR}.x). Please upgrade the SDK.`
1068
+ );
1069
+ }
1070
+ }
814
1071
  function assertManifestActive(manifest, baseUrl) {
815
1072
  const sunsetAt = manifest.sunset_at;
816
1073
  if (!sunsetAt) return;
@@ -826,11 +1083,16 @@ function assertManifestActive(manifest, baseUrl) {
826
1083
  );
827
1084
  }
828
1085
  }
829
- function assertEndpointActive(manifest, url, logger) {
1086
+ function assertEndpointActive(manifest, url, logger, method) {
830
1087
  const pathname = new URL(url).pathname;
831
- const matches = Object.entries(manifest.endpoints).filter(
1088
+ const pathMatches = Object.entries(manifest.endpoints).filter(
832
1089
  ([, descriptor]) => descriptor.path === pathname
833
1090
  );
1091
+ if (pathMatches.length === 0) return;
1092
+ const requestedMethod = method?.toUpperCase();
1093
+ const matches = requestedMethod ? pathMatches.filter(
1094
+ ([, descriptor]) => descriptor.method.toUpperCase() === requestedMethod
1095
+ ) : pathMatches;
834
1096
  if (matches.length === 0) return;
835
1097
  const now = Date.now();
836
1098
  const allSunset = matches.every(([, descriptor]) => {
@@ -853,45 +1115,6 @@ function assertEndpointActive(manifest, url, logger) {
853
1115
  );
854
1116
  }
855
1117
  }
856
- function assertManifestValid(manifest, baseUrl) {
857
- const context = baseUrl ? ` at ${baseUrl}` : "";
858
- if (manifest.latency_hints !== void 0) {
859
- if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
860
- throw new RouteDockManifestError(
861
- `Invalid manifest${context}: latency_hints defined without declaring regions`
862
- );
863
- }
864
- const regionSet = new Set(manifest.regions);
865
- for (const region of Object.keys(manifest.latency_hints)) {
866
- if (!regionSet.has(region)) {
867
- throw new RouteDockManifestError(
868
- `Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
869
- );
870
- }
871
- }
872
- }
873
- if (manifest.assets !== void 0) {
874
- if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
875
- throw new RouteDockManifestError(
876
- `Invalid manifest${context}: assets must be a non-empty array when defined`
877
- );
878
- }
879
- for (let i = 0; i < manifest.assets.length; i++) {
880
- const a = manifest.assets[i];
881
- if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
882
- throw new RouteDockManifestError(
883
- `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
884
- );
885
- }
886
- }
887
- const first = manifest.assets[0];
888
- if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
889
- throw new RouteDockManifestError(
890
- `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
891
- );
892
- }
893
- }
894
- }
895
1118
  var CACHE_TTL_MS = 6e4;
896
1119
  var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
897
1120
  var DEFAULT_MANIFEST_TIMEOUT_MS = 5e3;
@@ -932,6 +1155,7 @@ var LruCache = class {
932
1155
  }
933
1156
  };
934
1157
  var manifestCache = new LruCache(DEFAULT_MANIFEST_CACHE_MAX_SIZE);
1158
+ var inflightManifestFetches = /* @__PURE__ */ new Map();
935
1159
  function configureManifestCache(maxSize) {
936
1160
  if (!Number.isInteger(maxSize) || maxSize <= 0) {
937
1161
  throw new RouteDockManifestError("Invalid manifest cache size: " + maxSize);
@@ -967,10 +1191,27 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
967
1191
  if (cached && Date.now() < cached.expiresAt) {
968
1192
  verifyManifestSignature(cached.manifest, expectedPayee);
969
1193
  assertClientVersionSupported(cached.manifest, baseUrl);
1194
+ assertManifestVersionSupported(cached.manifest, baseUrl);
970
1195
  assertManifestValid(cached.manifest, baseUrl);
971
1196
  assertManifestActive(cached.manifest, baseUrl);
972
1197
  return cached.manifest;
973
1198
  }
1199
+ let inflight = inflightManifestFetches.get(baseUrl);
1200
+ if (!inflight) {
1201
+ inflight = fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs).finally(() => {
1202
+ inflightManifestFetches.delete(baseUrl);
1203
+ });
1204
+ inflightManifestFetches.set(baseUrl, inflight);
1205
+ }
1206
+ const manifest = await inflight;
1207
+ verifyManifestSignature(manifest, expectedPayee);
1208
+ assertClientVersionSupported(manifest, baseUrl);
1209
+ assertManifestVersionSupported(manifest, baseUrl);
1210
+ assertManifestValid(manifest, baseUrl);
1211
+ assertManifestActive(manifest, baseUrl);
1212
+ return manifest;
1213
+ }
1214
+ async function fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_MANIFEST_TIMEOUT_MS) {
974
1215
  const url = baseUrl.replace(/\/$/, "") + "/.well-known/routedock.json";
975
1216
  return withRetry(async () => {
976
1217
  let raw;
@@ -1007,15 +1248,12 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
1007
1248
  } catch (err) {
1008
1249
  throw new RouteDockManifestError(`Manifest at ${url} is not valid JSON`, { cause: err });
1009
1250
  }
1010
- const result = validator.validate(raw);
1011
- if (!result.valid) {
1012
- const msgs = result.errors.map((err) => err.error).join("; ");
1013
- throw new RouteDockManifestError(`Invalid manifest at ${url}: ${msgs}`);
1014
- }
1251
+ validateManifestSchema(raw, url);
1015
1252
  const manifest = raw;
1016
1253
  assertManifestValid(manifest, baseUrl);
1017
- verifyManifestSignature(manifest, expectedPayee);
1254
+ verifyManifestSignature(manifest);
1018
1255
  assertClientVersionSupported(manifest, baseUrl);
1256
+ assertManifestVersionSupported(manifest, baseUrl);
1019
1257
  assertManifestActive(manifest, baseUrl);
1020
1258
  manifestCache.set(baseUrl, { manifest, fetchedAt: Date.now(), expiresAt });
1021
1259
  return manifest;
@@ -1130,6 +1368,48 @@ function rankProvidersByLatency(manifests, targetRegion) {
1130
1368
  });
1131
1369
  }
1132
1370
 
1371
+ // src/internal/settleResult.ts
1372
+ function readSettleResult(result) {
1373
+ const settle = result;
1374
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
1375
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
1376
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
1377
+ return failed(settle.errorReason);
1378
+ }
1379
+ return { ok: true, txHash: settle.transaction };
1380
+ }
1381
+
1382
+ // src/internal/request.ts
1383
+ var DEFAULT_METHOD = "GET";
1384
+ var BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
1385
+ function isReplayable(body) {
1386
+ return typeof body === "string" || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || typeof Blob !== "undefined" && body instanceof Blob || typeof FormData !== "undefined" && body instanceof FormData || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams;
1387
+ }
1388
+ async function bufferBody(body) {
1389
+ if (isReplayable(body)) return body;
1390
+ return new Response(body).arrayBuffer();
1391
+ }
1392
+ async function prepareRequest(request) {
1393
+ const method = (request?.method ?? DEFAULT_METHOD).toUpperCase();
1394
+ const headers = new Headers(request?.headers);
1395
+ const body = request?.body;
1396
+ if (body === void 0 || BODYLESS_METHODS.has(method)) {
1397
+ return { method, headers };
1398
+ }
1399
+ return { method, headers, body: await bufferBody(body) };
1400
+ }
1401
+ function toRequestInit(prepared, overrides) {
1402
+ const headers = new Headers(prepared.headers);
1403
+ if (overrides) {
1404
+ new Headers(overrides).forEach((value, name) => headers.set(name, value));
1405
+ }
1406
+ const init = { method: prepared.method, headers };
1407
+ if (prepared.body !== void 0) {
1408
+ init.body = prepared.body;
1409
+ }
1410
+ return init;
1411
+ }
1412
+
1133
1413
  // src/internal/payee.ts
1134
1414
  function resolvePayee(manifest, mode) {
1135
1415
  const override = manifest.pricing[mode]?.payee;
@@ -1190,6 +1470,102 @@ function checkChargeChallenge(request, manifest, network, maxAmount) {
1190
1470
  }
1191
1471
  return checkAmount(request["amount"], maxAmount);
1192
1472
  }
1473
+ var defaultIo = {
1474
+ async expirationLedger(network, maxTimeoutSeconds) {
1475
+ const { sequence } = await stellar.getRpcClient(network).getLatestLedger();
1476
+ const secondsPerLedger = await stellar.getEstimatedLedgerCloseTimeSeconds(network);
1477
+ return sequence + Math.ceil(maxTimeoutSeconds / secondsPerLedger);
1478
+ },
1479
+ async assemble({ asset, from, payTo, amount, networkPassphrase, rpcUrl }) {
1480
+ return stellarSdk.contract.AssembledTransaction.build({
1481
+ contractId: asset,
1482
+ method: "transfer",
1483
+ // SEP-41: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface
1484
+ args: [
1485
+ stellarSdk.nativeToScVal(from, { type: "address" }),
1486
+ stellarSdk.nativeToScVal(payTo, { type: "address" }),
1487
+ stellarSdk.nativeToScVal(amount, { type: "i128" })
1488
+ ],
1489
+ networkPassphrase,
1490
+ rpcUrl,
1491
+ parseResultXdr: (result) => result
1492
+ });
1493
+ }
1494
+ };
1495
+ var NulthStellarScheme = class {
1496
+ constructor(signer, io = defaultIo) {
1497
+ this.signer = signer;
1498
+ this.io = io;
1499
+ }
1500
+ signer;
1501
+ io;
1502
+ scheme = "exact";
1503
+ async createPaymentPayload(x402Version, paymentRequirements) {
1504
+ try {
1505
+ this.validateRequirements(paymentRequirements);
1506
+ } catch (err) {
1507
+ throw new Error(
1508
+ `Invalid input parameters for creating Nulth Stellar payment, cause: ${String(err)}`
1509
+ );
1510
+ }
1511
+ const from = this.signer.address;
1512
+ const { network, payTo, asset, amount, maxTimeoutSeconds } = paymentRequirements;
1513
+ if (!paymentRequirements.extra.areFeesSponsored) {
1514
+ throw new Error("Nulth exact scheme requires areFeesSponsored to be true");
1515
+ }
1516
+ const maxLedger = await this.io.expirationLedger(network, maxTimeoutSeconds);
1517
+ const tx = await this.io.assemble({
1518
+ asset,
1519
+ from,
1520
+ payTo,
1521
+ amount,
1522
+ networkPassphrase: stellar.getNetworkPassphrase(network),
1523
+ rpcUrl: stellar.getRpcUrl(network)
1524
+ });
1525
+ stellar.handleSimulationResult(tx.simulation);
1526
+ const signers = tx.needsNonInvokerSigningBy();
1527
+ if (!signers.includes(from) || signers.length > 1) {
1528
+ throw new Error(`Expected to sign with [${from}], but got [${signers.join(", ")}]`);
1529
+ }
1530
+ await tx.signAuthEntries({
1531
+ address: from,
1532
+ signAuthEntry: this.signer.signAuthEntry,
1533
+ expiration: maxLedger,
1534
+ authorizeEntry: nulthSdk.nulthAuthorizeEntry
1535
+ });
1536
+ await tx.simulate();
1537
+ stellar.handleSimulationResult(tx.simulation);
1538
+ const remaining = tx.needsNonInvokerSigningBy();
1539
+ if (remaining.length > 0) {
1540
+ throw new Error(`unexpected signer(s) required: [${remaining.join(", ")}]`);
1541
+ }
1542
+ if (!tx.built) {
1543
+ throw new Error("Nulth payment transaction has no assembled envelope to send");
1544
+ }
1545
+ return { x402Version, payload: { transaction: tx.built.toXDR() } };
1546
+ }
1547
+ validateRequirements(requirements) {
1548
+ const { scheme, network, payTo, asset, amount } = requirements;
1549
+ if (typeof amount !== "string" || !Number.isInteger(Number(amount)) || Number(amount) <= 0) {
1550
+ throw new Error(`Invalid amount: ${amount}. Amount must be a positive integer.`);
1551
+ }
1552
+ if (scheme !== this.scheme) {
1553
+ throw new Error(`Unsupported scheme: ${scheme}`);
1554
+ }
1555
+ if (!stellar.isStellarNetwork(network)) {
1556
+ throw new Error(`Unsupported Stellar network: ${network}`);
1557
+ }
1558
+ if (!stellar.validateStellarDestinationAddress(payTo)) {
1559
+ throw new Error(`Invalid Stellar destination address: ${payTo}`);
1560
+ }
1561
+ if (!stellar.validateStellarAssetAddress(asset)) {
1562
+ throw new Error(`Invalid Stellar asset address: ${asset}`);
1563
+ }
1564
+ }
1565
+ };
1566
+ function selectStellarScheme(signer) {
1567
+ return nulthSdk.isNulthStellarSigner(signer) ? new NulthStellarScheme(signer) : new stellar.ExactStellarScheme(signer);
1568
+ }
1193
1569
 
1194
1570
  // src/client/x402Client.ts
1195
1571
  var CAIP2 = {
@@ -1197,25 +1573,29 @@ var CAIP2 = {
1197
1573
  mainnet: "stellar:pubnet"
1198
1574
  };
1199
1575
  var X402Client = class _X402Client {
1200
- constructor(secretKeyOrSigner, network, retryPolicy) {
1576
+ constructor(secretKeyOrSigner, network, retryPolicy, preSignHook, logger) {
1201
1577
  this.network = network;
1202
1578
  this.retryPolicy = retryPolicy;
1579
+ this.preSignHook = preSignHook;
1203
1580
  const caip2 = CAIP2[network];
1204
1581
  this.signer = typeof secretKeyOrSigner === "string" ? stellar.createEd25519Signer(secretKeyOrSigner, caip2) : secretKeyOrSigner;
1205
- const scheme = new client.ExactStellarScheme(this.signer);
1206
- const core = new client$1.x402Client();
1582
+ const scheme = selectStellarScheme(this.signer);
1583
+ const core = new client.x402Client();
1207
1584
  core.register(caip2, scheme);
1208
- this.httpClient = new client$1.x402HTTPClient(core);
1585
+ this.httpClient = new client.x402HTTPClient(core);
1586
+ this.logger = logger;
1209
1587
  }
1210
1588
  network;
1211
1589
  retryPolicy;
1590
+ preSignHook;
1212
1591
  httpClient;
1213
1592
  signer;
1593
+ logger;
1214
1594
  /** Replace signer (e.g. swap to Nulth ZK account payer before pay) */
1215
1595
  withSigner(signer) {
1216
- return new _X402Client(signer, this.network, this.retryPolicy);
1596
+ return new _X402Client(signer, this.network, this.retryPolicy, this.preSignHook, this.logger);
1217
1597
  }
1218
- async pay(url, manifest) {
1598
+ async pay(url, manifest, request) {
1219
1599
  const pricing = manifest.pricing.x402;
1220
1600
  if (!pricing) {
1221
1601
  throw new RouteDockManifestError("manifest.pricing.x402 missing");
@@ -1224,13 +1604,15 @@ var X402Client = class _X402Client {
1224
1604
  throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
1225
1605
  }
1226
1606
  const caip2 = CAIP2[this.network];
1607
+ const prepared = await prepareRequest(request);
1227
1608
  const probe = await withRetry(async () => {
1228
1609
  let init2;
1229
1610
  try {
1230
- init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
1611
+ init2 = await fetch(url, toRequestInit(prepared, { "X-Preferred-Mode": "x402" }));
1231
1612
  } catch (err) {
1232
1613
  throw wrapFetchError(err, "x402 initial request");
1233
1614
  }
1615
+ this.logger?.("debug", "[x402] unpaid probe response", { status: init2.status });
1234
1616
  if (init2.status !== 402) {
1235
1617
  if (!init2.ok) {
1236
1618
  throw httpStatusToError(
@@ -1247,6 +1629,11 @@ var X402Client = class _X402Client {
1247
1629
  `Failed to parse JSON from non-402 response (HTTP ${init2.status})`
1248
1630
  );
1249
1631
  }
1632
+ this.logger?.(
1633
+ "debug",
1634
+ "[x402] unpaid probe returned non-402 success \u2014 serving as free response",
1635
+ { status: init2.status }
1636
+ );
1250
1637
  return { kind: "free", data: data2 };
1251
1638
  }
1252
1639
  return { kind: "payment-required", init: init2 };
@@ -1275,6 +1662,18 @@ var X402Client = class _X402Client {
1275
1662
  if (allowed.length === 0) {
1276
1663
  throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
1277
1664
  }
1665
+ if (this.preSignHook) {
1666
+ let decision;
1667
+ try {
1668
+ decision = await this.preSignHook({ url, manifest, accepts: allowed });
1669
+ } catch (err) {
1670
+ throw new RouteDockPolicyRejectError("pre_sign_hook_error", { cause: err });
1671
+ }
1672
+ if (!decision || decision.approve !== true) {
1673
+ const reason = decision && decision.approve === false ? decision.reason : void 0;
1674
+ throw new RouteDockPolicyRejectError(reason ?? "pre_sign_rejected");
1675
+ }
1676
+ }
1278
1677
  let paymentPayload;
1279
1678
  try {
1280
1679
  paymentPayload = await this.httpClient.createPaymentPayload({
@@ -1288,13 +1687,15 @@ var X402Client = class _X402Client {
1288
1687
  }
1289
1688
  const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1290
1689
  const signedAmount = nulthSdk.stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
1690
+ this.logger?.("debug", "[x402] sending paid request with payment header");
1291
1691
  const settled = await withRetry(async () => {
1292
1692
  let response;
1293
1693
  try {
1294
- response = await fetch(url, { headers: paymentHeaders });
1694
+ response = await fetch(url, toRequestInit(prepared, paymentHeaders));
1295
1695
  } catch (err) {
1296
1696
  throw wrapFetchError(err, "x402 settlement request");
1297
1697
  }
1698
+ this.logger?.("debug", "[x402] paid request response", { status: response.status });
1298
1699
  if (!response.ok) {
1299
1700
  if (response.status >= 500 || response.status === 429 || response.status === 503) {
1300
1701
  throw httpStatusToError(
@@ -1303,17 +1704,28 @@ var X402Client = class _X402Client {
1303
1704
  response
1304
1705
  );
1305
1706
  }
1306
- throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
1707
+ throw await paymentRejectedError("x402 payment failed", response.status, response);
1307
1708
  }
1308
1709
  return response;
1309
1710
  }, this.retryPolicy);
1310
1711
  let txHash = null;
1311
1712
  const responseHeader = settled.headers.get("X-Payment-Response");
1312
- if (responseHeader) {
1713
+ if (!responseHeader) {
1714
+ this.logger?.("debug", "[x402] settled 2xx carried no X-Payment-Response header");
1715
+ } else {
1313
1716
  try {
1314
- const settleResponse = http.decodePaymentResponseHeader(responseHeader);
1315
- txHash = settleResponse.transaction ?? null;
1316
- } catch {
1717
+ const outcome = readSettleResult(http.decodePaymentResponseHeader(responseHeader));
1718
+ if (outcome.ok) {
1719
+ txHash = outcome.txHash;
1720
+ } else {
1721
+ this.logger?.("debug", "[x402] X-Payment-Response reports an unsettled payment", {
1722
+ ...outcome.reason !== void 0 ? { reason: outcome.reason } : {}
1723
+ });
1724
+ }
1725
+ } catch (err) {
1726
+ this.logger?.("debug", "[x402] X-Payment-Response header present but undecodable", {
1727
+ error: err
1728
+ });
1317
1729
  }
1318
1730
  }
1319
1731
  let data;
@@ -1334,33 +1746,28 @@ var MppChargeClient = class {
1334
1746
  keypair;
1335
1747
  network;
1336
1748
  retryPolicy;
1337
- async pay(url, manifest) {
1749
+ async pay(url, manifest, request) {
1338
1750
  const pricing = manifest.pricing["mpp-charge"];
1339
1751
  if (!pricing) {
1340
1752
  throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
1341
1753
  }
1342
1754
  const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
1343
- let txHash = null;
1755
+ const prepared = await prepareRequest(request);
1344
1756
  let signedAmount;
1345
1757
  let credential;
1346
- const mppx = client$2.Mppx.create({
1758
+ const mppx = client$1.Mppx.create({
1347
1759
  polyfill: false,
1348
1760
  methods: [
1349
- client$3.stellar.charge({
1761
+ client$2.stellar.charge({
1350
1762
  keypair: this.keypair,
1351
- mode: "pull",
1352
- onProgress(event) {
1353
- if (event.type === "paid") {
1354
- txHash = event.hash;
1355
- }
1356
- }
1763
+ mode: "pull"
1357
1764
  })
1358
1765
  ],
1359
1766
  onChallenge: async (challenge, { createCredential }) => {
1360
- const request = challenge.request;
1361
- const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
1767
+ const request2 = challenge.request;
1768
+ const rejection = checkChargeChallenge(request2, manifest, caip2, pricing.amount);
1362
1769
  if (rejection) throw rejection;
1363
- const amount = request["amount"];
1770
+ const amount = request2["amount"];
1364
1771
  if (typeof amount !== "string") {
1365
1772
  throw new RouteDockPolicyRejectError("challenge_amount_invalid");
1366
1773
  }
@@ -1372,7 +1779,7 @@ var MppChargeClient = class {
1372
1779
  return withRetry(async () => {
1373
1780
  let response;
1374
1781
  try {
1375
- response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
1782
+ response = credential ? await mppx.rawFetch(url, toRequestInit(prepared, { Authorization: credential })) : await mppx.fetch(url, toRequestInit(prepared));
1376
1783
  } catch (err) {
1377
1784
  throw wrapMppError(err, "MPP charge request");
1378
1785
  }
@@ -1384,8 +1791,9 @@ var MppChargeClient = class {
1384
1791
  response
1385
1792
  );
1386
1793
  }
1387
- throw new RouteDockManifestError(`MPP charge failed: HTTP ${response.status}`);
1794
+ throw await paymentRejectedError("MPP charge failed", response.status, response);
1388
1795
  }
1796
+ const txHash = readSettlementHash(response);
1389
1797
  let data;
1390
1798
  try {
1391
1799
  data = await response.json();
@@ -1405,8 +1813,40 @@ var MppChargeClient = class {
1405
1813
  }, this.retryPolicy);
1406
1814
  }
1407
1815
  };
1816
+ function readSettlementHash(response) {
1817
+ try {
1818
+ return mppx.Receipt.fromResponse(response).reference;
1819
+ } catch {
1820
+ return null;
1821
+ }
1822
+ }
1408
1823
  var MIN_REFUND_WAITING_PERIOD = 17280;
1409
1824
  var STREAM_CLEANUP_TIMEOUT_MS = 3e3;
1825
+ var TX_CONFIRM_INTERVAL_MS = 1e3;
1826
+ var TX_CONFIRM_TIMEOUT_MS = 3e4;
1827
+ async function confirmTransaction(server, hash, label, timeoutMs = TX_CONFIRM_TIMEOUT_MS, intervalMs = TX_CONFIRM_INTERVAL_MS) {
1828
+ const deadline = Date.now() + timeoutMs;
1829
+ while (true) {
1830
+ const txResponse = await server.getTransaction(hash);
1831
+ const status = txResponse?.status;
1832
+ if (status === "SUCCESS") {
1833
+ return hash;
1834
+ }
1835
+ if (status === "FAILED") {
1836
+ throw new RouteDockDisputeError(
1837
+ `${label} transaction failed on-chain: status FAILED (resultXdr: ${txResponse.resultXdr.toXDR("base64")})`
1838
+ );
1839
+ }
1840
+ if (Date.now() >= deadline) {
1841
+ break;
1842
+ }
1843
+ const remaining = deadline - Date.now();
1844
+ await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(1, remaining))));
1845
+ }
1846
+ throw new RouteDockDisputeError(
1847
+ `${label} transaction confirmation timed out after ${timeoutMs}ms (hash: ${hash})`
1848
+ );
1849
+ }
1410
1850
  var WS_CONNECTING = 0;
1411
1851
  var WS_OPEN = 1;
1412
1852
  function defaultWebSocketFactory(url, init, handlers) {
@@ -1420,9 +1860,9 @@ function defaultWebSocketFactory(url, init, handlers) {
1420
1860
  if (typeof data === "string") {
1421
1861
  handlers.onMessage(data);
1422
1862
  } else if (data instanceof ArrayBuffer) {
1423
- handlers.onMessage(Buffer.from(data).toString("utf8"));
1863
+ handlers.onMessage(bytesToUtf8(new Uint8Array(data)));
1424
1864
  } else {
1425
- void data.arrayBuffer().then((buffer) => handlers.onMessage(Buffer.from(buffer).toString("utf8"))).catch(() => {
1865
+ void data.arrayBuffer().then((buffer) => handlers.onMessage(bytesToUtf8(new Uint8Array(buffer)))).catch(() => {
1426
1866
  });
1427
1867
  }
1428
1868
  };
@@ -1575,11 +2015,11 @@ var MppSessionClient = class {
1575
2015
  throw err;
1576
2016
  }
1577
2017
  };
1578
- const streamWs = (u, m, createCredential, onSigned) => this.streamWebSocket(u, m, createCredential, onSigned);
1579
- const mppx = client$2.Mppx.create({
2018
+ const streamWs = (u, m, createCredential, onSigned, request) => this.streamWebSocket(u, m, createCredential, onSigned, request);
2019
+ const mppx = client$1.Mppx.create({
1580
2020
  polyfill: false,
1581
2021
  methods: [
1582
- client$4.stellar.channel({
2022
+ client$3.stellar.channel({
1583
2023
  commitmentKey,
1584
2024
  sourceAccount: this.keypair.publicKey(),
1585
2025
  ...store ? { store } : {},
@@ -1647,6 +2087,7 @@ var MppSessionClient = class {
1647
2087
  };
1648
2088
  },
1649
2089
  async *stream(options2) {
2090
+ const prepared = await prepareRequest(options2?.request);
1650
2091
  const checkSpend = () => {
1651
2092
  if (!onSpend) return Promise.resolve();
1652
2093
  return onSpend(pricing.rate);
@@ -1656,7 +2097,13 @@ var MppSessionClient = class {
1656
2097
  const onSigned = () => {
1657
2098
  vouchersIssued++;
1658
2099
  };
1659
- for await (const item of streamWs(url, mppx, createGuardedCredential, onSigned)) {
2100
+ for await (const item of streamWs(
2101
+ url,
2102
+ mppx,
2103
+ createGuardedCredential,
2104
+ onSigned,
2105
+ prepared
2106
+ )) {
1660
2107
  yield item;
1661
2108
  }
1662
2109
  return;
@@ -1671,7 +2118,10 @@ var MppSessionClient = class {
1671
2118
  const doFetch = () => withRetry(async () => {
1672
2119
  let resp;
1673
2120
  try {
1674
- resp = await mppx.fetch(url, { signal: abortController.signal });
2121
+ resp = await mppx.fetch(url, {
2122
+ ...toRequestInit(prepared),
2123
+ signal: abortController.signal
2124
+ });
1675
2125
  } catch (err) {
1676
2126
  if (abortController.signal.aborted) {
1677
2127
  throw err;
@@ -1756,10 +2206,10 @@ var MppSessionClient = class {
1756
2206
  async close() {
1757
2207
  clearSessionTimer();
1758
2208
  closed = true;
1759
- const { rpc: rpcMod, Contract: Contract2, nativeToScVal: nativeToScVal2, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
2209
+ const { rpc: rpcMod, Contract: Contract2, nativeToScVal: nativeToScVal3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1760
2210
  const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
1761
2211
  const server = new rpcMod.Server(rpcUrl);
1762
- const contract = new Contract2(channelFactory);
2212
+ const contract2 = new Contract2(channelFactory);
1763
2213
  const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
1764
2214
  const account = await withRetry(async () => {
1765
2215
  try {
@@ -1774,9 +2224,9 @@ var MppSessionClient = class {
1774
2224
  fee: BASE_FEE2,
1775
2225
  networkPassphrase: passphrase
1776
2226
  }).addOperation(
1777
- contract.call(
2227
+ contract2.call(
1778
2228
  "prepare_commitment",
1779
- nativeToScVal2(currentCumulative, { type: "i128" })
2229
+ nativeToScVal3(currentCumulative, { type: "i128" })
1780
2230
  )
1781
2231
  ).setTimeout(30).build();
1782
2232
  return await server.simulateTransaction(simTx);
@@ -1795,7 +2245,7 @@ var MppSessionClient = class {
1795
2245
  }
1796
2246
  let signature;
1797
2247
  try {
1798
- signature = commitmentKey.sign(Buffer.from(commitmentBytes));
2248
+ signature = commitmentKey.sign(commitmentBytes);
1799
2249
  } catch (err) {
1800
2250
  throw new RouteDockSignatureError("Channel close commitment signing failed", {
1801
2251
  cause: err
@@ -1809,7 +2259,7 @@ var MppSessionClient = class {
1809
2259
  headers: { "Content-Type": "application/json" },
1810
2260
  body: JSON.stringify({
1811
2261
  amount: currentCumulative.toString(),
1812
- signature: Buffer.from(signature).toString("hex")
2262
+ signature: bytesToHex(signature)
1813
2263
  })
1814
2264
  });
1815
2265
  } catch (err) {
@@ -1851,15 +2301,19 @@ var MppSessionClient = class {
1851
2301
  vouchersIssued
1852
2302
  };
1853
2303
  },
1854
- async requestRefund() {
2304
+ /**
2305
+ * Request refund from the channel contract (initiates dispute).
2306
+ * Returns the transaction hash only after on-chain confirmation (SUCCESS).
2307
+ */
2308
+ async requestRefund(options2) {
1855
2309
  const { rpc: rpcMod, Contract: Contract2, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1856
2310
  const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
1857
2311
  const server = new rpcMod.Server(rpcUrl);
1858
- const contract = new Contract2(channelFactory);
2312
+ const contract2 = new Contract2(channelFactory);
1859
2313
  const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
1860
2314
  try {
1861
2315
  const account = await server.getAccount(agentPublicKey);
1862
- const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract.call("request_refund")).setTimeout(30).build();
2316
+ const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract2.call("request_refund")).setTimeout(30).build();
1863
2317
  const simResult = await server.simulateTransaction(tx);
1864
2318
  if (rpcMod.Api.isSimulationError(simResult)) {
1865
2319
  throw new RouteDockDisputeError(`request_refund simulation failed: ${simResult.error}`);
@@ -1871,37 +2325,50 @@ var MppSessionClient = class {
1871
2325
  const errDetail = result.errorResult ? JSON.stringify(result.errorResult) : "status ERROR";
1872
2326
  throw new RouteDockDisputeError(`Refund request transaction failed: ${errDetail}`);
1873
2327
  }
2328
+ if (result.status === "TRY_AGAIN_LATER") {
2329
+ throw new RouteDockDisputeError("Refund request transaction rejected with TRY_AGAIN_LATER: transaction not queued");
2330
+ }
1874
2331
  if (!result.hash) {
1875
2332
  throw new RouteDockDisputeError("Refund request transaction not sent");
1876
2333
  }
1877
- return result.hash;
2334
+ return await confirmTransaction(
2335
+ server,
2336
+ result.hash,
2337
+ "Refund request",
2338
+ options2?.timeoutMs,
2339
+ options2?.intervalMs
2340
+ );
1878
2341
  } catch (err) {
1879
2342
  if (err instanceof RouteDockDisputeError) throw err;
1880
2343
  throw new RouteDockDisputeError(`Failed to request refund: ${err instanceof Error ? err.message : String(err)}`);
1881
2344
  }
1882
2345
  },
1883
- async settleWithLatestVoucher() {
1884
- const { rpc: rpcMod, Contract: Contract2, nativeToScVal: nativeToScVal2, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
2346
+ /**
2347
+ * Server-side counter-mechanism to settle with latest voucher before refund window expires.
2348
+ * Returns the transaction hash only after on-chain confirmation (SUCCESS).
2349
+ */
2350
+ async settleWithLatestVoucher(options2) {
2351
+ const { rpc: rpcMod, Contract: Contract2, nativeToScVal: nativeToScVal3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1885
2352
  const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
1886
2353
  const server = new rpcMod.Server(rpcUrl);
1887
- const contract = new Contract2(channelFactory);
2354
+ const contract2 = new Contract2(channelFactory);
1888
2355
  const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
1889
2356
  try {
1890
2357
  const account = await server.getAccount(agentPublicKey);
1891
- const simTx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract.call("prepare_commitment", nativeToScVal2(currentCumulative, { type: "i128" }))).setTimeout(30).build();
2358
+ const simTx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract2.call("prepare_commitment", nativeToScVal3(currentCumulative, { type: "i128" }))).setTimeout(30).build();
1892
2359
  const simResult = await server.simulateTransaction(simTx);
1893
2360
  if (rpcMod.Api.isSimulationError(simResult)) {
1894
2361
  throw new RouteDockDisputeError(`prepare_commitment simulation failed: ${simResult.error}`);
1895
2362
  }
1896
2363
  const commitmentBytes = simResult.result?.retval?.bytes();
1897
2364
  if (!commitmentBytes) throw new RouteDockDisputeError("prepare_commitment returned no bytes");
1898
- const signature = commitmentKey.sign(Buffer.from(commitmentBytes));
2365
+ const signature = commitmentKey.sign(commitmentBytes);
1899
2366
  const settleAccount = await server.getAccount(agentPublicKey);
1900
2367
  const settleTx = new TransactionBuilder2(settleAccount, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(
1901
- contract.call(
2368
+ contract2.call(
1902
2369
  "settle_with_signature",
1903
- nativeToScVal2(currentCumulative, { type: "i128" }),
1904
- nativeToScVal2(Buffer.from(signature))
2370
+ nativeToScVal3(currentCumulative, { type: "i128" }),
2371
+ nativeToScVal3(signature)
1905
2372
  )
1906
2373
  ).setTimeout(30).build();
1907
2374
  const preparedSettleTx = await server.prepareTransaction(settleTx);
@@ -1911,10 +2378,19 @@ var MppSessionClient = class {
1911
2378
  const errDetail = settleResult.errorResult ? JSON.stringify(settleResult.errorResult) : "status ERROR";
1912
2379
  throw new RouteDockDisputeError(`Settlement transaction failed: ${errDetail}`);
1913
2380
  }
2381
+ if (settleResult.status === "TRY_AGAIN_LATER") {
2382
+ throw new RouteDockDisputeError("Settlement transaction rejected with TRY_AGAIN_LATER: transaction not queued");
2383
+ }
1914
2384
  if (!settleResult.hash) {
1915
2385
  throw new RouteDockDisputeError("Settlement transaction not sent");
1916
2386
  }
1917
- return settleResult.hash;
2387
+ return await confirmTransaction(
2388
+ server,
2389
+ settleResult.hash,
2390
+ "Settlement",
2391
+ options2?.timeoutMs,
2392
+ options2?.intervalMs
2393
+ );
1918
2394
  } catch (err) {
1919
2395
  if (err instanceof RouteDockDisputeError) throw err;
1920
2396
  throw new RouteDockDisputeError(`Failed to settle with latest voucher: ${err instanceof Error ? err.message : String(err)}`);
@@ -1924,11 +2400,11 @@ var MppSessionClient = class {
1924
2400
  const { rpc: rpcMod, Contract: Contract2, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1925
2401
  const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
1926
2402
  const server = new rpcMod.Server(rpcUrl);
1927
- const contract = new Contract2(channelFactory);
2403
+ const contract2 = new Contract2(channelFactory);
1928
2404
  const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
1929
2405
  try {
1930
2406
  const account = await server.getAccount(agentPublicKey);
1931
- const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract.call("get_channel_state")).setTimeout(30).build();
2407
+ const tx = new TransactionBuilder2(account, { fee: BASE_FEE2, networkPassphrase: passphrase }).addOperation(contract2.call("get_channel_state")).setTimeout(30).build();
1932
2408
  const simResult = await server.simulateTransaction(tx);
1933
2409
  if (rpcMod.Api.isSimulationError(simResult)) {
1934
2410
  throw new RouteDockChannelStateError(`Failed to query channel state: ${simResult.error}`);
@@ -1994,11 +2470,11 @@ var MppSessionClient = class {
1994
2470
  * close code (1000/1001); abnormal closes and connection failures surface as
1995
2471
  * RouteDockChannelStateError.
1996
2472
  */
1997
- async *streamWebSocket(url, mppx$1, createCredential, onSigned) {
1998
- const request = { method: "GET" };
2473
+ async *streamWebSocket(url, mppx$1, createCredential, onSigned, request) {
2474
+ const probeInit = toRequestInit(request);
1999
2475
  let probe;
2000
2476
  try {
2001
- probe = await mppx$1.rawFetch(url, request);
2477
+ probe = await mppx$1.rawFetch(url, probeInit);
2002
2478
  } catch (err) {
2003
2479
  throw wrapFetchError(err, "Voucher request");
2004
2480
  }
@@ -2102,230 +2578,19 @@ var MppSessionClient = class {
2102
2578
  }
2103
2579
  }
2104
2580
  };
2105
- var NulthPolicyError = class extends Error {
2106
- code;
2107
- constructor(code, message) {
2108
- super(message);
2109
- this.name = "NulthPolicyError";
2110
- this.code = code;
2111
- }
2112
- };
2113
- function sha256Hex(data) {
2114
- const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
2115
- return crypto$1.createHash("sha256").update(buf).digest("hex");
2116
- }
2117
- function commitDailyCap(dailyCapStroops, witnessSecret) {
2118
- return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
2119
- }
2120
- function commitAllowlist(payees, witnessSecret) {
2121
- const sorted = [...payees].sort().join(",");
2122
- return sha256Hex(`${witnessSecret}:allowlist:${sorted}`);
2123
- }
2124
- function hashPayee(payee) {
2125
- return sha256Hex(`payee:${payee}`);
2126
- }
2127
- function dayBucketFromLedger(ledgerSequence) {
2128
- return Math.floor(ledgerSequence / 17280);
2129
- }
2130
- function authDigestFromEntry(authEntryBase64) {
2131
- const entryHash = stellarSdk.hash(Buffer.from(authEntryBase64, "base64"));
2132
- return sha256Hex(entryHash);
2133
- }
2134
- function assertAuthEntryMatchesContext(authEntry, from, ctx) {
2135
- let preimage;
2581
+ function createNulthSigner(config) {
2582
+ const sharedConfig = { ...config, logger: resolveLogger(config.logger) };
2583
+ Object.defineProperty(sharedConfig, "paymentContext", {
2584
+ get: () => config.paymentContext
2585
+ });
2136
2586
  try {
2137
- preimage = stellarSdk.xdr.HashIdPreimage.fromXDR(authEntry, "base64");
2138
- } catch {
2139
- throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not decode as a HashIdPreimage");
2140
- }
2141
- if (preimage.switch() !== stellarSdk.xdr.EnvelopeType.envelopeTypeSorobanAuthorization()) {
2142
- throw new NulthPolicyError(
2143
- "auth_entry_mismatch",
2144
- "authEntry is not a Soroban authorization preimage"
2145
- );
2146
- }
2147
- const inv = preimage.sorobanAuthorization().invocation();
2148
- if (inv.subInvocations().length !== 0) {
2149
- throw new NulthPolicyError("auth_entry_mismatch", "authEntry has unexpected sub-invocations");
2150
- }
2151
- if (inv.function().switch() !== stellarSdk.xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()) {
2152
- throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not invoke a contract function");
2153
- }
2154
- const fn = inv.function().contractFn();
2155
- const contractAddress = stellarSdk.Address.fromScAddress(fn.contractAddress()).toString();
2156
- if (contractAddress !== ctx.assetContract) {
2157
- throw new NulthPolicyError(
2158
- "auth_entry_mismatch",
2159
- `authEntry contract ${contractAddress} does not match assetContract ${ctx.assetContract}`
2160
- );
2161
- }
2162
- const functionName = fn.functionName().toString();
2163
- if (functionName !== "transfer") {
2164
- throw new NulthPolicyError(
2165
- "auth_entry_mismatch",
2166
- `authEntry function ${functionName} does not match expected transfer`
2167
- );
2168
- }
2169
- const args = fn.args();
2170
- if (args.length !== 3) {
2171
- throw new NulthPolicyError("auth_entry_mismatch", `authEntry transfer has ${args.length} args, expected 3`);
2172
- }
2173
- const entryFrom = stellarSdk.scValToNative(args[0]);
2174
- if (entryFrom !== from) {
2175
- throw new NulthPolicyError(
2176
- "auth_entry_mismatch",
2177
- `authEntry from ${entryFrom} does not match nulthAccount ${from}`
2178
- );
2179
- }
2180
- const entryTo = stellarSdk.scValToNative(args[1]);
2181
- if (entryTo !== ctx.payee) {
2182
- throw new NulthPolicyError(
2183
- "auth_entry_mismatch",
2184
- `authEntry to ${entryTo} does not match paymentContext.payee ${ctx.payee}`
2185
- );
2186
- }
2187
- const entryAmount = stellarSdk.scValToNative(args[2]);
2188
- if (entryAmount !== ctx.amountStroops) {
2189
- throw new NulthPolicyError(
2190
- "auth_entry_mismatch",
2191
- `authEntry amount ${entryAmount} does not match paymentContext.amountStroops ${ctx.amountStroops}`
2192
- );
2193
- }
2194
- }
2195
- function insecureMockProof(preimage) {
2196
- return crypto$1.createHash("sha512").update(`nulth:${preimage}`).digest("base64");
2197
- }
2198
- function encodeAuthSignature(proof) {
2199
- const payload = { nulth: "zk-v1", proof };
2200
- return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
2201
- }
2202
- var NulthClient = class {
2203
- constructor(config) {
2204
- this.config = config;
2205
- this.policy = { ...config.policy };
2206
- this.prover = config.prover ?? "mock";
2207
- if (this.prover !== "mock") {
2208
- throw new RouteDockManifestError(
2209
- `NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
2210
- );
2211
- }
2212
- if (config.network !== "testnet") {
2213
- throw new RouteDockManifestError(
2214
- `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
2215
- );
2216
- }
2217
- resolveLogger(config.logger)(
2218
- "warn",
2219
- "NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
2220
- );
2221
- }
2222
- config;
2223
- policy;
2224
- prover;
2225
- get proverBackend() {
2226
- return this.prover;
2227
- }
2228
- get nulthAccount() {
2229
- return this.config.nulthAccount;
2230
- }
2231
- get allowlistCommitment() {
2232
- return this.policy.allowlistCommitment;
2233
- }
2234
- get capCommitment() {
2235
- return commitDailyCap(this.policy.dailyCapStroops, this.policy.witnessSecret);
2236
- }
2237
- buildPaymentAuthProof(context) {
2238
- this.enforcePolicy(context);
2239
- const bucket = dayBucketFromLedger(context.ledgerSequence);
2240
- if (bucket !== this.policy.dayBucket) {
2241
- this.policy.dayBucket = bucket;
2242
- this.policy.dailySpendStroops = 0n;
2243
- }
2244
- const authDigest = authDigestFromEntry(context.authEntry);
2245
- const capCommitment = this.capCommitment;
2246
- const payeeHash = hashPayee(context.payee);
2247
- const preimage = [
2248
- authDigest,
2249
- this.policy.allowlistCommitment,
2250
- capCommitment,
2251
- payeeHash,
2252
- context.amountStroops.toString(),
2253
- bucket.toString(),
2254
- this.config.nulthAccount
2255
- ].join("|");
2256
- const proof = {
2257
- version: 1,
2258
- proof: insecureMockProof(preimage),
2259
- publicInputs: {
2260
- authDigest,
2261
- allowlistCommitment: this.policy.allowlistCommitment,
2262
- capCommitment,
2263
- payeeHash,
2264
- amountStroops: context.amountStroops.toString(),
2265
- dayBucket: bucket.toString()
2266
- }
2267
- };
2268
- this.policy.dailySpendStroops += context.amountStroops;
2269
- return proof;
2270
- }
2271
- enforcePolicy(context) {
2272
- if (context.amountStroops < 0n) {
2273
- throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
2274
- }
2275
- if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
2276
- throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
2277
- }
2278
- if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
2279
- throw new NulthPolicyError("session_expired", "Nulth session expired");
2280
- }
2281
- if (!this.policy.allowedPayees.includes(context.payee)) {
2282
- throw new NulthPolicyError("payee_not_allowed", `Payee ${context.payee} not in off-chain allowlist`);
2283
- }
2284
- const bucket = dayBucketFromLedger(context.ledgerSequence);
2285
- const spend = bucket === this.policy.dayBucket ? this.policy.dailySpendStroops : 0n;
2286
- if (spend + context.amountStroops > this.policy.dailyCapStroops) {
2287
- throw new NulthPolicyError("daily_cap_exceeded", "Nulth daily cap exceeded");
2587
+ return nulthSdk.createNulthSigner(sharedConfig);
2588
+ } catch (error) {
2589
+ if (error instanceof nulthSdk.NulthConfigError) {
2590
+ throw new RouteDockManifestError(error.message, { cause: error });
2288
2591
  }
2592
+ throw error;
2289
2593
  }
2290
- };
2291
- function createNulthSigner(config) {
2292
- const client = new NulthClient(config);
2293
- const nulthAccount = config.nulthAccount;
2294
- return {
2295
- address: nulthAccount,
2296
- signAuthEntry: async (authEntry) => {
2297
- const ctx = config.paymentContext;
2298
- if (!ctx) throw new Error("Nulth paymentContext must be set before signing");
2299
- assertAuthEntryMatchesContext(authEntry, nulthAccount, ctx);
2300
- const proof = client.buildPaymentAuthProof({ authEntry, ...ctx });
2301
- return { signedAuthEntry: encodeAuthSignature(proof), signerAddress: nulthAccount };
2302
- }
2303
- };
2304
- }
2305
- function setNulthPaymentContext(config, context) {
2306
- config.paymentContext = context;
2307
- }
2308
- function paymentContextFromManifest(manifest, mode, ledgerSequence) {
2309
- const pricing = manifest.pricing[mode];
2310
- if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
2311
- return {
2312
- payee: pricing.payee ?? manifest.payee,
2313
- amountStroops: nulthSdk.usdcToStroops(pricing.amount),
2314
- assetContract: manifest.asset_contract,
2315
- ledgerSequence
2316
- };
2317
- }
2318
- function createPolicyState(input) {
2319
- const dailyCapStroops = nulthSdk.usdcToStroops(input.dailyCapUsdc);
2320
- return {
2321
- dailyCapStroops,
2322
- allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
2323
- allowedPayees: input.allowedPayees,
2324
- dailySpendStroops: 0n,
2325
- dayBucket: dayBucketFromLedger(input.ledgerSequence ?? 0),
2326
- witnessSecret: input.witnessSecret,
2327
- ...input.expiryLedger !== void 0 ? { expiryLedger: input.expiryLedger } : {}
2328
- };
2329
2594
  }
2330
2595
  function assertNulthVaultManifest(manifest, nulthAccount) {
2331
2596
  if (manifest.vault !== void 0 && manifest.vault !== "nulth") {
@@ -2349,69 +2614,29 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
2349
2614
  "nulth vault uses the insecure mock prover and cannot be used on mainnet"
2350
2615
  );
2351
2616
  }
2352
- const ledgerSequence = ledgerSequenceOverride ?? await fetchLedgerSequence(network);
2353
- const policy = createPolicyState({
2354
- dailyCapUsdc: vault.dailyCapUsdc,
2355
- allowedPayees: vault.allowedPayees,
2356
- witnessSecret: vault.witnessSecret,
2357
- ledgerSequence,
2358
- ...vault.expiryLedger !== void 0 ? { expiryLedger: vault.expiryLedger } : {}
2359
- });
2360
- const config = {
2361
- nulthAccount: vault.nulthAccount,
2362
- network,
2363
- prover,
2364
- policy,
2365
- ...vault.verifierContract !== void 0 ? { verifierContract: vault.verifierContract } : {}
2366
- };
2367
- if (mode === "x402" || mode === "mpp-charge") {
2368
- setNulthPaymentContext(config, paymentContextFromManifest(manifest, mode, ledgerSequence));
2369
- } else {
2370
- throw new RouteDockManifestError(`Nulth ZK vault does not support payment mode: ${mode}`);
2371
- }
2372
- const signer = createNulthSigner(config);
2373
- return { signer, config };
2374
- }
2375
- function decodeAuthSignature(encoded) {
2376
- const raw = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
2377
- if (typeof raw !== "object" || raw === null || raw.nulth !== "zk-v1") {
2378
- throw new Error("Invalid Nulth ZK auth signature");
2379
- }
2380
- return raw;
2381
- }
2382
- var FileSpendStore = class {
2383
- filePath;
2384
- constructor(filePath) {
2385
- this.filePath = filePath;
2386
- }
2387
- async read() {
2388
- let data;
2389
- try {
2390
- data = await fs__default.default.readFile(this.filePath, "utf-8");
2391
- } catch (err) {
2392
- if (err.code === "ENOENT") {
2393
- return null;
2394
- }
2395
- throw new Error(
2396
- `[FileSpendStore] Failed to read spend file at ${this.filePath}: ${err.message}`
2397
- );
2398
- }
2399
- try {
2400
- return JSON.parse(data);
2401
- } catch {
2402
- throw new Error(
2403
- `[FileSpendStore] Spend file at ${this.filePath} contains invalid JSON. Delete or repair it before resuming. Raw content: ${data.slice(0, 120)}`
2404
- );
2405
- }
2406
- }
2407
- async write(state) {
2408
- const dir = path__default.default.dirname(this.filePath);
2409
- await fs__default.default.mkdir(dir, { recursive: true });
2410
- const tmpPath = `${this.filePath}.tmp`;
2411
- await fs__default.default.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
2412
- await fs__default.default.rename(tmpPath, this.filePath);
2617
+ const ledgerSequence = ledgerSequenceOverride ?? await fetchLedgerSequence(network);
2618
+ const policy = nulthSdk.createPolicyState({
2619
+ dailyCapUsdc: vault.dailyCapUsdc,
2620
+ allowedPayees: vault.allowedPayees,
2621
+ witnessSecret: vault.witnessSecret,
2622
+ ledgerSequence,
2623
+ ...vault.expiryLedger !== void 0 ? { expiryLedger: vault.expiryLedger } : {}
2624
+ });
2625
+ const config = {
2626
+ nulthAccount: vault.nulthAccount,
2627
+ network,
2628
+ prover,
2629
+ policy,
2630
+ ...vault.verifierContract !== void 0 ? { verifierContract: vault.verifierContract } : {}
2631
+ };
2632
+ if (mode === "x402" || mode === "mpp-charge") {
2633
+ nulthSdk.setNulthPaymentContext(config, nulthSdk.paymentContextFromManifest(manifest, mode, ledgerSequence));
2634
+ } else {
2635
+ throw new RouteDockManifestError(`Nulth ZK vault does not support payment mode: ${mode}`);
2413
2636
  }
2414
- };
2637
+ const signer = createNulthSigner(config);
2638
+ return { signer, config };
2639
+ }
2415
2640
 
2416
2641
  // src/store/SpendStore.ts
2417
2642
  var InMemorySpendStore = class {
@@ -2525,7 +2750,7 @@ var RouteDockClient = class _RouteDockClient {
2525
2750
  }
2526
2751
  const secretKey = this.keypair.secret();
2527
2752
  const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
2528
- this.x402 = new X402Client(secretKey, this.network, retryPolicy);
2753
+ this.x402 = new X402Client(secretKey, this.network, retryPolicy, config.preSignHook, this.logger);
2529
2754
  this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
2530
2755
  this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
2531
2756
  }
@@ -2533,7 +2758,7 @@ var RouteDockClient = class _RouteDockClient {
2533
2758
  async _resolveManifest(url, options) {
2534
2759
  const baseUrl = new URL(url).origin;
2535
2760
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2536
- assertEndpointActive(manifest, url, this.logger);
2761
+ assertEndpointActive(manifest, url, this.logger, options?.request?.method);
2537
2762
  const mode = selectMode(manifest, options);
2538
2763
  return { manifest, mode };
2539
2764
  }
@@ -2550,13 +2775,9 @@ var RouteDockClient = class _RouteDockClient {
2550
2775
  });
2551
2776
  return next;
2552
2777
  }
2553
- /**
2554
- * Check that the payer has a trustline for the payment asset and surface
2555
- * manifest metadata (regions, latency hints, capabilities). Safe to
2556
- * call before committing to a payment — for approval gates and manual
2557
- * trustline remediation.
2558
- */
2559
- async preflight(manifest, mode, endpoint) {
2778
+ async preflight(target, mode, endpoint) {
2779
+ if (typeof target === "string") return this._preflightUrl(target);
2780
+ const manifest = target;
2560
2781
  assertManifestValid(manifest);
2561
2782
  const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
2562
2783
  await this._checkTrustline(manifest, selectedAsset);
@@ -2569,6 +2790,42 @@ var RouteDockClient = class _RouteDockClient {
2569
2790
  ...manifest.capabilities && { capabilities: manifest.capabilities }
2570
2791
  };
2571
2792
  }
2793
+ /** Manifest-only preflight for a URL — no trustline check, no payment. */
2794
+ async _preflightUrl(url) {
2795
+ const baseUrl = new URL(url).origin;
2796
+ const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2797
+ assertEndpointActive(manifest, url);
2798
+ const costs = manifest.modes.map((mode) => {
2799
+ const asset = selectAsset(manifest, mode, url).asset;
2800
+ switch (mode) {
2801
+ case "x402":
2802
+ case "mpp-charge": {
2803
+ const pricing = manifest.pricing[mode];
2804
+ return { mode, amount: pricing.amount, per: "request", asset };
2805
+ }
2806
+ case "mpp-session":
2807
+ case "mpp-session-ws": {
2808
+ const pricing = manifest.pricing[mode];
2809
+ return { mode, amount: pricing.rate, per: "voucher", asset, min_deposit: pricing.min_deposit };
2810
+ }
2811
+ default:
2812
+ throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2813
+ }
2814
+ });
2815
+ const pathname = new URL(url).pathname;
2816
+ const matches = Object.values(manifest.endpoints).filter((descriptor) => descriptor.path === pathname);
2817
+ const endpointSunsets = matches.map((descriptor) => descriptor.sunset_at).filter((value) => typeof value === "string").sort((a, b) => Date.parse(a) - Date.parse(b));
2818
+ return {
2819
+ modes: manifest.modes,
2820
+ costs,
2821
+ compliance: {
2822
+ deprecated_modes: manifest.deprecated_modes ?? [],
2823
+ ...manifest.sunset_at && { sunset_at: manifest.sunset_at },
2824
+ endpoint_deprecated: matches.some((descriptor) => descriptor.deprecated === true),
2825
+ ...endpointSunsets[0] && { endpoint_sunset_at: endpointSunsets[0] }
2826
+ }
2827
+ };
2828
+ }
2572
2829
  /**
2573
2830
  * Verify via Horizon that the payer account has a trustline for the
2574
2831
  * payment asset declared in the manifest. Throws RouteDockTrustlineError
@@ -2580,8 +2837,7 @@ var RouteDockClient = class _RouteDockClient {
2580
2837
  const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
2581
2838
  const cached = _RouteDockClient._trustlineCache.get(cacheKey);
2582
2839
  if (cached && Date.now() < cached.expiresAt) return;
2583
- const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
2584
- const server = new stellarSdk.Horizon.Server(horizonUrl);
2840
+ const server = new stellarSdk.Horizon.Server(HORIZON_URLS[this.network]);
2585
2841
  try {
2586
2842
  const account = await server.loadAccount(this.keypair.publicKey());
2587
2843
  const balances = account.balances;
@@ -2617,9 +2873,11 @@ var RouteDockClient = class _RouteDockClient {
2617
2873
  async pay(url, options) {
2618
2874
  const baseUrl = new URL(url).origin;
2619
2875
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2620
- assertEndpointActive(manifest, url, this.logger);
2876
+ const request = options?.request;
2877
+ assertEndpointActive(manifest, url, this.logger, request?.method);
2621
2878
  const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
2622
2879
  const selectedAsset = selectAsset(manifest, mode, url).asset;
2880
+ this._assertCapAsset(selectedAsset);
2623
2881
  await this._checkTrustline(manifest, selectedAsset);
2624
2882
  let amount;
2625
2883
  switch (mode) {
@@ -2637,18 +2895,18 @@ var RouteDockClient = class _RouteDockClient {
2637
2895
  default:
2638
2896
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2639
2897
  }
2640
- const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
2898
+ const reserveId = await this._checkAndReserveSpend(amount, baseUrl, selectedAsset);
2641
2899
  let result;
2642
2900
  try {
2643
2901
  if (this.vault?.mode === "nulth") {
2644
- result = await this._payWithNulthVault(url, manifest, mode);
2902
+ result = await this._payWithNulthVault(url, manifest, mode, request);
2645
2903
  } else {
2646
2904
  switch (mode) {
2647
2905
  case "x402":
2648
- result = await this.x402.pay(url, manifest);
2906
+ result = await this.x402.pay(url, manifest, request);
2649
2907
  break;
2650
2908
  case "mpp-charge":
2651
- result = await this.charge.pay(url, manifest);
2909
+ result = await this.charge.pay(url, manifest, request);
2652
2910
  break;
2653
2911
  default:
2654
2912
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
@@ -2667,7 +2925,7 @@ var RouteDockClient = class _RouteDockClient {
2667
2925
  return result;
2668
2926
  }
2669
2927
  /** Nulth ZK vault path — proof built off-chain, attached as auth signature */
2670
- async _payWithNulthVault(url, manifest, mode) {
2928
+ async _payWithNulthVault(url, manifest, mode, request) {
2671
2929
  const prover = this.vault?.prover ?? "mock";
2672
2930
  if (this.network === "mainnet" && prover === "mock") {
2673
2931
  throw new RouteDockManifestError("nulth vault uses a MOCK Groth16 prover and cannot be used on mainnet");
@@ -2680,10 +2938,10 @@ var RouteDockClient = class _RouteDockClient {
2680
2938
  try {
2681
2939
  const { signer } = await prepareNulthSigner(this.vault, manifest, mode, this.network);
2682
2940
  const x402 = this.x402.withSigner(signer);
2683
- const result = await x402.pay(url, manifest);
2941
+ const result = await x402.pay(url, manifest, request);
2684
2942
  return result;
2685
2943
  } catch (err) {
2686
- if (err instanceof NulthPolicyError) {
2944
+ if (err instanceof nulthSdk.NulthPolicyError) {
2687
2945
  throw new RouteDockPolicyRejectError(err.code);
2688
2946
  }
2689
2947
  throw err;
@@ -2749,17 +3007,35 @@ var RouteDockClient = class _RouteDockClient {
2749
3007
  );
2750
3008
  }
2751
3009
  const endpointKey = new URL(url).origin;
2752
- const onSpend = this.spendCap ? (amount) => this._recordSessionSpend(amount, endpointKey) : void 0;
3010
+ let onSpend;
3011
+ if (this.spendCap) {
3012
+ const sessionAsset = selectAsset(manifest, mode, url).asset;
3013
+ this._assertCapAsset(sessionAsset);
3014
+ onSpend = (amount) => this._recordSessionSpend(amount, endpointKey, sessionAsset);
3015
+ }
2753
3016
  return this.session.openSession(url, manifest, secret, options, onSpend);
2754
3017
  }
3018
+ /**
3019
+ * The local cap accumulates one USDC total. Refuse to count any other asset
3020
+ * against it (or let it through uncounted) when a cap is configured.
3021
+ */
3022
+ _assertCapAsset(asset) {
3023
+ if (this.spendCap && asset !== this.spendCap.asset) {
3024
+ throw new RouteDockPolicyRejectError("local_cap_asset_mismatch", {
3025
+ cause: new Error(
3026
+ `spendCap is denominated in ${this.spendCap.asset} but the selected payment asset is ${asset}`
3027
+ )
3028
+ });
3029
+ }
3030
+ }
2755
3031
  /**
2756
3032
  * Record spend for an MPP session voucher. Called before each voucher
2757
3033
  * fetch in stream(). Enforces the same local daily and per-endpoint caps
2758
3034
  * as discrete pay() calls. Returns a commit function to clean up the
2759
3035
  * reservation after the voucher fetch succeeds.
2760
3036
  */
2761
- async _recordSessionSpend(amount, endpointKey) {
2762
- const reserveId = await this._checkAndReserveSpend(amount, endpointKey);
3037
+ async _recordSessionSpend(amount, endpointKey, asset) {
3038
+ const reserveId = await this._checkAndReserveSpend(amount, endpointKey, asset);
2763
3039
  if (reserveId) {
2764
3040
  await this._commitSpend(reserveId);
2765
3041
  }
@@ -2798,15 +3074,16 @@ var RouteDockClient = class _RouteDockClient {
2798
3074
  * Both limits are independent: a payment is only reserved after **both** pass.
2799
3075
  * All spend (regardless of endpoint) counts toward the global accumulator.
2800
3076
  */
2801
- async _checkAndReserveSpend(amount, endpointKey) {
3077
+ async _checkAndReserveSpend(amount, endpointKey, asset) {
2802
3078
  if (!this.spendCap) return "";
2803
- return this._withSpendMutex(async () => {
3079
+ this._assertCapAsset(asset);
3080
+ const spendCap = this.spendCap;
3081
+ const amountMicros = nulthSdk.usdcToStroops(amount);
3082
+ const reserve = (prev) => {
2804
3083
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2805
- const persisted = await this.spendStore.read();
2806
- const current = persisted && persisted.date === today ? persisted : { date: today, totalMicros: "0", endpoints: {} };
2807
- const amountMicros = nulthSdk.usdcToStroops(amount);
3084
+ const current = prev && prev.date === today ? { ...prev, endpoints: { ...prev.endpoints } } : { date: today, totalMicros: "0", endpoints: {} };
2808
3085
  const total = BigInt(current.totalMicros);
2809
- const endpointCapStr = this.spendCap.endpointCaps?.[endpointKey];
3086
+ const endpointCapStr = spendCap.endpointCaps?.[endpointKey];
2810
3087
  if (endpointCapStr !== void 0) {
2811
3088
  const endpointCapMicros = nulthSdk.usdcToStroops(endpointCapStr);
2812
3089
  const endpointTotal = BigInt(current.endpoints[endpointKey] ?? "0");
@@ -2814,17 +3091,25 @@ var RouteDockClient = class _RouteDockClient {
2814
3091
  throw new RouteDockPolicyRejectError("local_endpoint_cap_exceeded");
2815
3092
  }
2816
3093
  }
2817
- const globalCapMicros = nulthSdk.usdcToStroops(this.spendCap.daily);
3094
+ const globalCapMicros = nulthSdk.usdcToStroops(spendCap.daily);
2818
3095
  if (total + amountMicros > globalCapMicros) {
2819
3096
  throw new RouteDockPolicyRejectError("local_daily_cap_exceeded");
2820
3097
  }
2821
- const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
2822
- this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
2823
3098
  current.totalMicros = (total + amountMicros).toString();
2824
3099
  if (endpointCapStr !== void 0) {
2825
3100
  current.endpoints[endpointKey] = (BigInt(current.endpoints[endpointKey] ?? "0") + amountMicros).toString();
2826
3101
  }
2827
- await this.spendStore.write(current);
3102
+ return current;
3103
+ };
3104
+ return this._withSpendMutex(async () => {
3105
+ const store = this.spendStore;
3106
+ if (store.update) {
3107
+ await store.update(reserve);
3108
+ } else {
3109
+ await store.write(reserve(await store.read()));
3110
+ }
3111
+ const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3112
+ this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
2828
3113
  return reserveId;
2829
3114
  });
2830
3115
  }
@@ -2846,22 +3131,29 @@ var RouteDockClient = class _RouteDockClient {
2846
3131
  if (!reserve) return;
2847
3132
  this._pendingReserves.delete(reserveId);
2848
3133
  const { endpointKey, amountMicros } = reserve;
2849
- await this._withSpendMutex(async () => {
3134
+ const release = (prev) => {
2850
3135
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2851
- const persisted = await this.spendStore.read();
2852
- if (!persisted || persisted.date !== today) return;
2853
- const currentTotal = BigInt(persisted.totalMicros);
2854
- persisted.totalMicros = (currentTotal - amountMicros).toString();
3136
+ if (!prev || prev.date !== today) return void 0;
3137
+ const persisted = { ...prev, endpoints: { ...prev.endpoints } };
3138
+ persisted.totalMicros = (BigInt(persisted.totalMicros) - amountMicros).toString();
2855
3139
  if (persisted.endpoints[endpointKey]) {
2856
- const endpointTotal = BigInt(persisted.endpoints[endpointKey]);
2857
- const newEndpointTotal = endpointTotal - amountMicros;
3140
+ const newEndpointTotal = BigInt(persisted.endpoints[endpointKey]) - amountMicros;
2858
3141
  if (newEndpointTotal <= 0n) {
2859
3142
  delete persisted.endpoints[endpointKey];
2860
3143
  } else {
2861
3144
  persisted.endpoints[endpointKey] = newEndpointTotal.toString();
2862
3145
  }
2863
3146
  }
2864
- await this.spendStore.write(persisted);
3147
+ return persisted;
3148
+ };
3149
+ await this._withSpendMutex(async () => {
3150
+ const store = this.spendStore;
3151
+ if (store.update) {
3152
+ await store.update(release);
3153
+ } else {
3154
+ const next = release(await store.read());
3155
+ if (next) await store.write(next);
3156
+ }
2865
3157
  });
2866
3158
  }
2867
3159
  };
@@ -2945,6 +3237,158 @@ var SupabaseSessionStore = class {
2945
3237
  await this.setStatus(channelId, "closed");
2946
3238
  }
2947
3239
  };
3240
+ var DEFAULT_LOCK_STALE_MS = 1e4;
3241
+ var DEFAULT_LOCK_TIMEOUT_MS = 15e3;
3242
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3243
+ var FileSpendStore = class {
3244
+ filePath;
3245
+ lockPath;
3246
+ lockStaleMs;
3247
+ lockTimeoutMs;
3248
+ constructor(filePath, options = {}) {
3249
+ this.filePath = filePath;
3250
+ this.lockPath = `${filePath}.lock`;
3251
+ this.lockStaleMs = options.lockStaleMs ?? DEFAULT_LOCK_STALE_MS;
3252
+ this.lockTimeoutMs = options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
3253
+ }
3254
+ async read() {
3255
+ let data;
3256
+ try {
3257
+ data = await fs__default.default.readFile(this.filePath, "utf-8");
3258
+ } catch (err) {
3259
+ if (err.code === "ENOENT") {
3260
+ return null;
3261
+ }
3262
+ throw new Error(
3263
+ `[FileSpendStore] Failed to read spend file at ${this.filePath}: ${err.message}`
3264
+ );
3265
+ }
3266
+ try {
3267
+ return JSON.parse(data);
3268
+ } catch {
3269
+ throw new Error(
3270
+ `[FileSpendStore] Spend file at ${this.filePath} contains invalid JSON. Delete or repair it before resuming. Raw content: ${data.slice(0, 120)}`
3271
+ );
3272
+ }
3273
+ }
3274
+ async write(state) {
3275
+ await this.withLock(async (assertHeld) => {
3276
+ await this.writeFile(state, assertHeld);
3277
+ });
3278
+ }
3279
+ /**
3280
+ * Atomically read-modify-write the accumulator under the cross-process lock.
3281
+ * `fn` receives the current state (or null) and returns the state to persist,
3282
+ * or `undefined` to leave the file untouched. If `fn` throws, nothing is
3283
+ * written and the lock is released.
3284
+ */
3285
+ async update(fn) {
3286
+ return this.withLock(async (assertHeld) => {
3287
+ const prev = await this.read();
3288
+ const next = fn(prev);
3289
+ if (next === void 0) return prev;
3290
+ await this.writeFile(next, assertHeld);
3291
+ return next;
3292
+ });
3293
+ }
3294
+ // ── internals ──────────────────────────────────────────────────────────────
3295
+ async writeFile(state, assertHeld) {
3296
+ const tmpPath = `${this.filePath}.${process.pid}.${crypto$1.randomBytes(6).toString("hex")}.tmp`;
3297
+ try {
3298
+ await fs__default.default.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
3299
+ await assertHeld();
3300
+ await fs__default.default.rename(tmpPath, this.filePath);
3301
+ } catch (err) {
3302
+ await fs__default.default.rm(tmpPath, { force: true }).catch(() => {
3303
+ });
3304
+ throw err;
3305
+ }
3306
+ }
3307
+ /** Run `fn` while holding the lock file; always releases it. */
3308
+ async withLock(fn) {
3309
+ await fs__default.default.mkdir(path__default.default.dirname(this.filePath), { recursive: true });
3310
+ const token = `${process.pid}:${crypto$1.randomBytes(8).toString("hex")}`;
3311
+ await this.acquireLock(token);
3312
+ const heartbeat = setInterval(() => {
3313
+ const now = /* @__PURE__ */ new Date();
3314
+ fs__default.default.utimes(this.lockPath, now, now).catch(() => {
3315
+ });
3316
+ }, Math.max(10, Math.floor(this.lockStaleMs / 3)));
3317
+ heartbeat.unref();
3318
+ const assertHeld = async () => {
3319
+ let current;
3320
+ try {
3321
+ current = await fs__default.default.readFile(this.lockPath, "utf-8");
3322
+ } catch {
3323
+ current = "";
3324
+ }
3325
+ if (current !== token) {
3326
+ throw new Error(
3327
+ `[FileSpendStore] Lost the lock on ${this.lockPath} before committing; refusing to overwrite the spend file. Retry the operation.`
3328
+ );
3329
+ }
3330
+ };
3331
+ try {
3332
+ return await fn(assertHeld);
3333
+ } finally {
3334
+ clearInterval(heartbeat);
3335
+ await this.releaseLock(token);
3336
+ }
3337
+ }
3338
+ async acquireLock(token) {
3339
+ const deadline = Date.now() + this.lockTimeoutMs;
3340
+ let delay2 = 5;
3341
+ for (; ; ) {
3342
+ try {
3343
+ const handle = await fs__default.default.open(this.lockPath, "wx");
3344
+ try {
3345
+ await handle.writeFile(token, "utf-8");
3346
+ } finally {
3347
+ await handle.close();
3348
+ }
3349
+ return;
3350
+ } catch (err) {
3351
+ if (err.code !== "EEXIST") {
3352
+ throw new Error(
3353
+ `[FileSpendStore] Failed to create lock file ${this.lockPath}: ${err.message}`
3354
+ );
3355
+ }
3356
+ }
3357
+ await this.breakIfStale();
3358
+ if (Date.now() >= deadline) {
3359
+ throw new Error(
3360
+ `[FileSpendStore] Timed out after ${this.lockTimeoutMs}ms waiting for ${this.lockPath}. Another process is holding the spend file lock; if none is running, delete the lock file.`
3361
+ );
3362
+ }
3363
+ await sleep2(delay2 + Math.floor(Math.random() * delay2));
3364
+ delay2 = Math.min(delay2 * 2, 50);
3365
+ }
3366
+ }
3367
+ /** Remove the lock if its holder stopped refreshing it (crashed process). */
3368
+ async breakIfStale() {
3369
+ let before;
3370
+ try {
3371
+ before = await fs__default.default.stat(this.lockPath);
3372
+ } catch {
3373
+ return;
3374
+ }
3375
+ if (Date.now() - before.mtimeMs <= this.lockStaleMs) return;
3376
+ try {
3377
+ const after = await fs__default.default.stat(this.lockPath);
3378
+ if (after.ino !== before.ino || after.mtimeMs !== before.mtimeMs) return;
3379
+ await fs__default.default.unlink(this.lockPath);
3380
+ } catch {
3381
+ }
3382
+ }
3383
+ async releaseLock(token) {
3384
+ try {
3385
+ if (await fs__default.default.readFile(this.lockPath, "utf-8") === token) {
3386
+ await fs__default.default.unlink(this.lockPath);
3387
+ }
3388
+ } catch {
3389
+ }
3390
+ }
3391
+ };
2948
3392
 
2949
3393
  // src/store/SupabaseSeenTxStore.ts
2950
3394
  var SupabaseSeenTxStore = class {
@@ -2996,6 +3440,107 @@ var SupabaseSeenTxStore = class {
2996
3440
  return data;
2997
3441
  }
2998
3442
  };
3443
+ var FLAT_MODES = ["x402", "mpp-charge"];
3444
+ var SESSION_MODES = ["mpp-session", "mpp-session-ws"];
3445
+ function sameAmount(adapter, label, optionValue, manifestValue) {
3446
+ const parse = (where, value) => {
3447
+ try {
3448
+ return nulthSdk.usdcToStroops(value);
3449
+ } catch {
3450
+ throw new RouteDockManifestError(
3451
+ `${adapter}: ${label} has an unparseable ${where} amount "${value}"`
3452
+ );
3453
+ }
3454
+ };
3455
+ return parse("option", optionValue) === parse("manifest", manifestValue);
3456
+ }
3457
+ function assertProviderConfig(opts, adapterName) {
3458
+ const { manifest } = opts;
3459
+ if (manifest === null || typeof manifest !== "object") {
3460
+ throw new RouteDockManifestError(`${adapterName}: opts.manifest is required and must be an object`);
3461
+ }
3462
+ try {
3463
+ validateManifestSchema(manifest);
3464
+ assertManifestValid(manifest);
3465
+ } catch (err) {
3466
+ if (err instanceof RouteDockManifestError) {
3467
+ throw new RouteDockManifestError(`${adapterName}: ${err.message}`, { cause: err });
3468
+ }
3469
+ throw err;
3470
+ }
3471
+ const served = new Set(opts.modes.filter((mode) => Boolean(opts.pricing[mode])));
3472
+ const advertised = new Set(manifest.modes);
3473
+ const notAdvertised = [...served].filter((mode) => !advertised.has(mode));
3474
+ const notServed = [...advertised].filter((mode) => !served.has(mode));
3475
+ if (notAdvertised.length > 0 || notServed.length > 0) {
3476
+ const parts = [];
3477
+ if (notAdvertised.length > 0) parts.push(`served but not in manifest.modes: ${notAdvertised.join(", ")}`);
3478
+ if (notServed.length > 0) parts.push(`in manifest.modes but not served (missing from modes or pricing): ${notServed.join(", ")}`);
3479
+ throw new RouteDockManifestError(`${adapterName}: modes do not match manifest.modes (${parts.join("; ")})`);
3480
+ }
3481
+ for (const mode of FLAT_MODES) {
3482
+ if (!served.has(mode)) continue;
3483
+ const optionValue = opts.pricing[mode];
3484
+ const manifestValue = manifest.pricing[mode]?.amount;
3485
+ if (manifestValue === void 0) {
3486
+ throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode}.amount is missing but ${mode} is served`);
3487
+ }
3488
+ if (!sameAmount(adapterName, `pricing.${mode}`, optionValue, manifestValue)) {
3489
+ throw new RouteDockManifestError(
3490
+ `${adapterName}: pricing.${mode} "${optionValue}" does not match manifest.pricing.${mode}.amount "${manifestValue}"`
3491
+ );
3492
+ }
3493
+ }
3494
+ for (const mode of SESSION_MODES) {
3495
+ if (!served.has(mode)) continue;
3496
+ const optionValue = opts.pricing[mode];
3497
+ const manifestValue = manifest.pricing[mode];
3498
+ if (!manifestValue) {
3499
+ throw new RouteDockManifestError(`${adapterName}: manifest.pricing.${mode} is missing but ${mode} is served`);
3500
+ }
3501
+ if (!sameAmount(adapterName, `pricing.${mode}.rate`, optionValue.rate, manifestValue.rate)) {
3502
+ throw new RouteDockManifestError(
3503
+ `${adapterName}: pricing.${mode}.rate "${optionValue.rate}" does not match manifest.pricing.${mode}.rate "${manifestValue.rate}"`
3504
+ );
3505
+ }
3506
+ if (optionValue.channelFactory !== manifestValue.channel_factory) {
3507
+ throw new RouteDockManifestError(
3508
+ `${adapterName}: pricing.${mode}.channelFactory "${optionValue.channelFactory}" does not match manifest.pricing.${mode}.channel_factory "${manifestValue.channel_factory}"`
3509
+ );
3510
+ }
3511
+ }
3512
+ if (opts.assetContract !== void 0 && opts.assetContract !== manifest.asset_contract) {
3513
+ throw new RouteDockManifestError(
3514
+ `${adapterName}: assetContract "${opts.assetContract}" does not match manifest.asset_contract "${manifest.asset_contract}"`
3515
+ );
3516
+ }
3517
+ if (opts.asset !== void 0 && opts.asset !== manifest.asset) {
3518
+ throw new RouteDockManifestError(
3519
+ `${adapterName}: asset "${opts.asset}" does not match manifest.asset "${manifest.asset}"`
3520
+ );
3521
+ }
3522
+ if (opts.network !== manifest.network) {
3523
+ throw new RouteDockManifestError(
3524
+ `${adapterName}: network "${opts.network}" does not match manifest.network "${manifest.network}"`
3525
+ );
3526
+ }
3527
+ let signerPublicKey;
3528
+ try {
3529
+ signerPublicKey = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey).publicKey();
3530
+ } catch (err) {
3531
+ throw new RouteDockManifestError(`${adapterName}: payeeSecretKey is not a valid Stellar secret key`, { cause: err });
3532
+ }
3533
+ if (signerPublicKey !== manifest.payee) {
3534
+ throw new RouteDockManifestError(
3535
+ `${adapterName}: payeeSecretKey belongs to ${signerPublicKey}, not manifest.payee ${manifest.payee}; clients would reject the signed manifest`
3536
+ );
3537
+ }
3538
+ if (opts.payee !== manifest.payee) {
3539
+ throw new RouteDockManifestError(
3540
+ `${adapterName}: payee "${opts.payee}" does not match manifest.payee "${manifest.payee}"`
3541
+ );
3542
+ }
3543
+ }
2999
3544
  function extractPayerAddress(key) {
3000
3545
  if (typeof key !== "string") return null;
3001
3546
  return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
@@ -3004,6 +3549,18 @@ function extractPayerAddress(key) {
3004
3549
  // src/provider/SeenTxStore.ts
3005
3550
  var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
3006
3551
  var MAX_SETTLEMENT_REPLAYS = 1;
3552
+ async function recordSettlement(store, key, record, ctx) {
3553
+ try {
3554
+ await store.set(key, record);
3555
+ } catch (err) {
3556
+ ctx.logger("error", `${ctx.label} seenTxStore.set failed after settlement`, { error: err });
3557
+ try {
3558
+ ctx.onCallbackError?.(err, "seenTxStore.set");
3559
+ } catch (cbErr) {
3560
+ ctx.logger("error", `${ctx.label} onCallbackError threw`, { error: cbErr });
3561
+ }
3562
+ }
3563
+ }
3007
3564
  async function checkSettlementReplay(store, key, now = Date.now()) {
3008
3565
  const record = await store.get(key);
3009
3566
  if (!record) return { kind: "miss" };
@@ -3058,7 +3615,7 @@ var InMemorySeenTxStore = class {
3058
3615
  return next;
3059
3616
  }
3060
3617
  };
3061
- async function sha256Hex2(input) {
3618
+ async function sha256Hex(input) {
3062
3619
  const buf = new TextEncoder().encode(input);
3063
3620
  const digest = await crypto.subtle.digest("SHA-256", buf);
3064
3621
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -3066,22 +3623,11 @@ async function sha256Hex2(input) {
3066
3623
  async function paymentIdempotencyKey(getHeader, scope) {
3067
3624
  const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
3068
3625
  if (!material) return null;
3069
- return sha256Hex2(
3626
+ return sha256Hex(
3070
3627
  [scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
3071
3628
  );
3072
3629
  }
3073
3630
 
3074
- // src/provider/settleResult.ts
3075
- function readSettleResult(result) {
3076
- const settle = result;
3077
- const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
3078
- if (!settle || settle.success !== true) return failed(settle?.errorReason);
3079
- if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
3080
- return failed(settle.errorReason);
3081
- }
3082
- return { ok: true, txHash: settle.transaction };
3083
- }
3084
-
3085
3631
  // src/provider/x402Handler.ts
3086
3632
  var CAIP22 = {
3087
3633
  testnet: "stellar:testnet",
@@ -3221,10 +3767,10 @@ function createX402Handler(opts) {
3221
3767
  requirements
3222
3768
  );
3223
3769
  if (!verifyResult.isValid) {
3224
- res.status(401).json({
3225
- error: "Payment verification failed",
3226
- reason: verifyResult.invalidReason
3227
- });
3770
+ await respondPaymentRequired(
3771
+ "Payment verification failed",
3772
+ verifyResult.invalidReason
3773
+ );
3228
3774
  return;
3229
3775
  }
3230
3776
  const settleResult = await localFacilitator.settle(
@@ -3250,7 +3796,12 @@ function createX402Handler(opts) {
3250
3796
  if (typeof paymentResponse === "string") {
3251
3797
  headers["X-Payment-Response"] = paymentResponse;
3252
3798
  }
3253
- await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
3799
+ await recordSettlement(
3800
+ seenTxStore,
3801
+ idempotencyKey,
3802
+ { txHash, headers, createdAt: Date.now() },
3803
+ { logger, label: "[x402]", onCallbackError: opts.onCallbackError }
3804
+ );
3254
3805
  }
3255
3806
  if (txHash && opts.onSettled) {
3256
3807
  Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
@@ -3280,21 +3831,21 @@ function createMppChargeHandler(opts) {
3280
3831
  const logger = resolveLogger(opts.logger);
3281
3832
  const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
3282
3833
  const mppxInstances = /* @__PURE__ */ new Map();
3283
- function getMppx(contract) {
3284
- let instance = mppxInstances.get(contract);
3834
+ function getMppx(contract2) {
3835
+ let instance = mppxInstances.get(contract2);
3285
3836
  if (!instance) {
3286
3837
  instance = server$2.Mppx.create({
3287
3838
  secretKey: opts.payeeSecretKey,
3288
3839
  methods: [
3289
3840
  server$4.stellar.charge({
3290
3841
  recipient,
3291
- currency: contract,
3842
+ currency: contract2,
3292
3843
  network: networkId,
3293
3844
  feePayer: { envelopeSigner: opts.payeeSecretKey }
3294
3845
  })
3295
3846
  ]
3296
3847
  });
3297
- mppxInstances.set(contract, instance);
3848
+ mppxInstances.set(contract2, instance);
3298
3849
  }
3299
3850
  return instance;
3300
3851
  }
@@ -3315,7 +3866,7 @@ function createMppChargeHandler(opts) {
3315
3866
  const credPart = authHeader.replace(/^Payment\s+/, "").split(",").find((p) => p.trim().startsWith("credential="));
3316
3867
  if (credPart) {
3317
3868
  const b64 = credPart.split("=").slice(1).join("=").replace(/^"|"$/g, "");
3318
- const credJson = Buffer.from(b64, "base64").toString("utf8");
3869
+ const credJson = base64ToUtf8(b64);
3319
3870
  const cred = JSON.parse(credJson);
3320
3871
  const key = cred.sender ?? cred.payload?.sender ?? cred.payload?.from;
3321
3872
  payerAddress = extractPayerAddress(key);
@@ -3377,18 +3928,19 @@ function createMppChargeHandler(opts) {
3377
3928
  if (receiptHeader) {
3378
3929
  try {
3379
3930
  const parsed = JSON.parse(
3380
- Buffer.from(receiptHeader, "base64").toString("utf8")
3931
+ base64ToUtf8(receiptHeader)
3381
3932
  );
3382
3933
  reference = parsed.reference;
3383
3934
  } catch {
3384
3935
  }
3385
3936
  }
3386
3937
  if (idempotencyKey) {
3387
- await seenTxStore.set(idempotencyKey, {
3388
- txHash: reference ?? null,
3389
- headers: receiptHeaders,
3390
- createdAt: Date.now()
3391
- });
3938
+ await recordSettlement(
3939
+ seenTxStore,
3940
+ idempotencyKey,
3941
+ { txHash: reference ?? null, headers: receiptHeaders, createdAt: Date.now() },
3942
+ { logger, label: "[mpp-charge]", onCallbackError: opts.onCallbackError }
3943
+ );
3392
3944
  }
3393
3945
  if (reference && opts.onSettled) {
3394
3946
  Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
@@ -3402,6 +3954,14 @@ function createMppChargeHandler(opts) {
3402
3954
  }
3403
3955
  };
3404
3956
  }
3957
+
3958
+ // src/provider/internal/vaultRpc.ts
3959
+ async function createSorobanRpcServer(rpcUrl) {
3960
+ const { Server } = await import('@stellar/stellar-sdk/rpc');
3961
+ return new Server(rpcUrl);
3962
+ }
3963
+
3964
+ // src/provider/internal/vaultSettlement.ts
3405
3965
  function resolveVaultSettlementAddresses(payer, payee) {
3406
3966
  if (!payer) return null;
3407
3967
  return { payer, payee };
@@ -3421,6 +3981,111 @@ function buildSessionSettlementTransaction(source, vaultContractId, channelId, a
3421
3981
  networkPassphrase
3422
3982
  }).addOperation(op).setTimeout(30).build();
3423
3983
  }
3984
+ var DEFAULT_RPC_URLS = {
3985
+ testnet: "https://soroban-testnet.stellar.org",
3986
+ mainnet: "https://mainnet.sorobanrpc.com"
3987
+ };
3988
+ var CONFIRM_TIMEOUT_MS = 12e4;
3989
+ var CONFIRM_POLL_MS = 1e3;
3990
+ function delay(ms) {
3991
+ return new Promise((resolve) => setTimeout(resolve, ms));
3992
+ }
3993
+ function describeSendError(sent) {
3994
+ const errorResult = sent.errorResult;
3995
+ try {
3996
+ const name = errorResult?.result?.().switch?.().name;
3997
+ if (name) return `sendTransaction returned ERROR: ${name}`;
3998
+ } catch {
3999
+ }
4000
+ return `sendTransaction returned status ${sent.status}`;
4001
+ }
4002
+ async function waitForConfirmation(server, txHash) {
4003
+ const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
4004
+ for (; ; ) {
4005
+ const tx = await server.getTransaction(txHash);
4006
+ if (tx.status === "SUCCESS") return "success";
4007
+ if (tx.status === "FAILED") return "failed";
4008
+ if (Date.now() >= deadline) return "timeout";
4009
+ await delay(CONFIRM_POLL_MS);
4010
+ }
4011
+ }
4012
+ async function recordSessionSettlement(request, { logger, onCallbackError }) {
4013
+ const reportFailure = (fields) => {
4014
+ logger("error", "[vault] failed to record session_settled", {
4015
+ channelId: request.channelId,
4016
+ ...fields.txHash ? { txHash: fields.txHash } : {},
4017
+ reason: fields.reason,
4018
+ ...fields.error !== void 0 ? { error: fields.error } : {}
4019
+ });
4020
+ if (onCallbackError) {
4021
+ const err = fields.error ?? new Error(`session_settled vault record failed: ${fields.reason}`);
4022
+ onCallbackError(err, "vaultSettlement");
4023
+ }
4024
+ return { ok: false, reason: fields.reason, ...fields.txHash ? { txHash: fields.txHash } : {} };
4025
+ };
4026
+ try {
4027
+ const payeeKeypair = stellarSdk.Keypair.fromSecret(request.payeeSecretKey);
4028
+ const addresses = resolveVaultSettlementAddresses(request.payer, payeeKeypair.publicKey());
4029
+ if (!addresses) {
4030
+ logger("warn", "[vault] skipped session_settled record: payer address unavailable", {
4031
+ channelId: request.channelId
4032
+ });
4033
+ return { ok: false, reason: "payer address unavailable" };
4034
+ }
4035
+ const networkPassphrase = request.network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
4036
+ const rpcUrl = request.config.rpcUrl ?? DEFAULT_RPC_URLS[request.network];
4037
+ const server = await createSorobanRpcServer(rpcUrl);
4038
+ const sourceAccount = await server.getAccount(payeeKeypair.publicKey());
4039
+ const tx = buildSessionSettlementTransaction(
4040
+ sourceAccount,
4041
+ request.config.contract,
4042
+ request.channelId,
4043
+ addresses,
4044
+ request.amount,
4045
+ request.voucherCount,
4046
+ networkPassphrase
4047
+ );
4048
+ const preparedTx = await server.prepareTransaction(tx);
4049
+ preparedTx.sign(payeeKeypair);
4050
+ const sent = await server.sendTransaction(preparedTx);
4051
+ if (sent.status === "ERROR") {
4052
+ return reportFailure({
4053
+ txHash: sent.hash || preparedTx.hash().toString("hex"),
4054
+ reason: describeSendError(sent)
4055
+ });
4056
+ }
4057
+ const txHash = sent.hash || preparedTx.hash().toString("hex");
4058
+ const confirmation = await waitForConfirmation(server, txHash);
4059
+ if (confirmation === "success") {
4060
+ logger("debug", "[vault] recorded session_settled", {
4061
+ channelId: request.channelId,
4062
+ txHash
4063
+ });
4064
+ return { ok: true, txHash };
4065
+ }
4066
+ return reportFailure({
4067
+ txHash,
4068
+ reason: confirmation === "failed" ? "transaction failed on-chain" : `confirmation timed out after ${CONFIRM_TIMEOUT_MS}ms`
4069
+ });
4070
+ } catch (err) {
4071
+ const reason = err instanceof Error ? err.message : String(err);
4072
+ return reportFailure({ reason, error: err });
4073
+ }
4074
+ }
4075
+
4076
+ // src/internal/vaultConfig.ts
4077
+ function resolveVaultSettlementConfig(option, logger) {
4078
+ if (option) return option;
4079
+ if (typeof process === "undefined") return void 0;
4080
+ const fromEnv = process.env.AGENT_VAULT_CONTRACT ?? process.env.AGENT_VAULT_CONTRACT_ID;
4081
+ if (!fromEnv) return void 0;
4082
+ logger(
4083
+ "warn",
4084
+ "[vault] configuring the vault contract via the AGENT_VAULT_CONTRACT/AGENT_VAULT_CONTRACT_ID environment variables is deprecated and will be removed in the next minor release \u2014 pass the vaultSettlement option instead",
4085
+ {}
4086
+ );
4087
+ return { contract: fromEnv };
4088
+ }
3424
4089
  function channelAuthorizer(envelopeSigner) {
3425
4090
  return { envelopeSigner };
3426
4091
  }
@@ -3499,6 +4164,7 @@ function createMppSessionHandler(opts) {
3499
4164
  let record = null;
3500
4165
  let voucherCount = 0;
3501
4166
  let sessionOpened = false;
4167
+ let orphanFlagged = false;
3502
4168
  let idleTimer = null;
3503
4169
  let settledCleanly = false;
3504
4170
  let closeListenerArmed = false;
@@ -3524,6 +4190,14 @@ function createMppSessionHandler(opts) {
3524
4190
  return null;
3525
4191
  }
3526
4192
  record = { amount: BigInt(stored.amount), signature: stored.signature, payer: stored.payer ?? null };
4193
+ if (typeof stored.voucherCount === "number") {
4194
+ voucherCount = stored.voucherCount;
4195
+ }
4196
+ if (typeof stored.sessionOpened === "boolean") {
4197
+ sessionOpened = stored.sessionOpened;
4198
+ } else if (record !== null) {
4199
+ sessionOpened = true;
4200
+ }
3527
4201
  } catch {
3528
4202
  return null;
3529
4203
  }
@@ -3531,8 +4205,8 @@ function createMppSessionHandler(opts) {
3531
4205
  }
3532
4206
  async function flagOrphan(reason) {
3533
4207
  await loadPersistedRecord();
3534
- if (!sessionOpened || settledCleanly) return;
3535
- sessionOpened = false;
4208
+ if (!sessionOpened || settledCleanly || orphanFlagged) return;
4209
+ orphanFlagged = true;
3536
4210
  closeListenerArmed = false;
3537
4211
  clearIdleTimer();
3538
4212
  const cumulativeAmount = record ? (Number(record.amount) / 1e7).toFixed(7) : "0.0000000";
@@ -3580,16 +4254,20 @@ function createMppSessionHandler(opts) {
3580
4254
  if (record && amount < record.amount) return;
3581
4255
  const payer = record?.payer ?? extractPayerAddress(credential.source);
3582
4256
  record = { amount, signature: payload.signature, payer };
4257
+ voucherCount++;
4258
+ settledCleanly = false;
4259
+ orphanFlagged = false;
4260
+ armIdleTimer();
4261
+ const wasOpened = sessionOpened;
4262
+ sessionOpened = true;
3583
4263
  await innerStore.put(voucherRecordKey, {
3584
4264
  amount: record.amount.toString(),
3585
4265
  signature: record.signature,
3586
- payer: record.payer
4266
+ payer: record.payer,
4267
+ voucherCount,
4268
+ sessionOpened: true
3587
4269
  });
3588
- voucherCount++;
3589
- settledCleanly = false;
3590
- armIdleTimer();
3591
- if (!sessionOpened) {
3592
- sessionOpened = true;
4270
+ if (!wasOpened) {
3593
4271
  if (opts.onSessionOpen) {
3594
4272
  Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
3595
4273
  logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
@@ -3641,10 +4319,11 @@ function createMppSessionHandler(opts) {
3641
4319
  }
3642
4320
  if (closeAmount > 0n && closeSig) {
3643
4321
  const closePayer = record?.payer ?? null;
4322
+ const vouchersClosed = voucherCount;
3644
4323
  const closeTxHash = await server$3.close({
3645
4324
  channel: opts.channelFactory,
3646
4325
  amount: closeAmount,
3647
- signature: Buffer.from(closeSig, "hex"),
4326
+ signature: hexToBytes(closeSig),
3648
4327
  feePayer: channelAuthorizer(payeeKeypair),
3649
4328
  network: networkId
3650
4329
  });
@@ -3657,46 +4336,28 @@ function createMppSessionHandler(opts) {
3657
4336
  opts.onCallbackError?.(err, "onSettled");
3658
4337
  });
3659
4338
  }
3660
- const vaultContract = process.env.AGENT_VAULT_CONTRACT;
3661
- if (vaultContract) {
3662
- const settlementAddresses = resolveVaultSettlementAddresses(
3663
- closePayer,
3664
- payeeKeypair.publicKey()
4339
+ res.json({ closeTxHash });
4340
+ const vaultConfig = resolveVaultSettlementConfig(opts.vaultSettlement, logger);
4341
+ if (vaultConfig) {
4342
+ await recordSessionSettlement(
4343
+ {
4344
+ config: vaultConfig,
4345
+ network: opts.network,
4346
+ payeeSecretKey: opts.payeeSecretKey,
4347
+ channelId: opts.channelFactory,
4348
+ payer: closePayer,
4349
+ amount: closeAmount,
4350
+ voucherCount: vouchersClosed
4351
+ },
4352
+ { logger, onCallbackError: opts.onCallbackError }
3665
4353
  );
3666
- if (!settlementAddresses) {
3667
- logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
3668
- } else {
3669
- try {
3670
- const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
3671
- const { Server } = await import('@stellar/stellar-sdk/rpc');
3672
- const networkPassphrase = opts.network === "mainnet" ? Networks2.PUBLIC : Networks2.TESTNET;
3673
- const rpcUrl = opts.network === "mainnet" ? "https://mainnet.sorobanrpc.com" : "https://soroban-testnet.stellar.org";
3674
- const server = new Server(rpcUrl);
3675
- const payeePublicKey = payeeKeypair.publicKey();
3676
- const sourceAccount = await server.getAccount(payeePublicKey);
3677
- const tx = buildSessionSettlementTransaction(
3678
- sourceAccount,
3679
- vaultContract,
3680
- opts.channelFactory,
3681
- settlementAddresses,
3682
- closeAmount,
3683
- voucherCount,
3684
- networkPassphrase
3685
- );
3686
- const preparedTx = await server.prepareTransaction(tx);
3687
- preparedTx.sign(payeeKeypair);
3688
- await server.sendTransaction(preparedTx);
3689
- } catch (recordErr) {
3690
- logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
3691
- }
3692
- }
3693
4354
  }
3694
- res.json({ closeTxHash });
3695
4355
  } else {
3696
4356
  res.json({ closeTxHash: null, message: "no vouchers received" });
3697
4357
  }
3698
4358
  sessionOpened = false;
3699
4359
  voucherCount = 0;
4360
+ orphanFlagged = false;
3700
4361
  record = null;
3701
4362
  await innerStore.delete(voucherRecordKey);
3702
4363
  closeListenerArmed = false;
@@ -3733,6 +4394,7 @@ function createMppSessionHandler(opts) {
3733
4394
  var MPP_MODES = ["mpp-session", "mpp-charge"];
3734
4395
  function routedock(opts) {
3735
4396
  const startTime = Date.now();
4397
+ assertProviderConfig(opts, "routedock");
3736
4398
  const signedManifest = signManifest(opts.manifest, opts.payeeSecretKey);
3737
4399
  const handlerMap = /* @__PURE__ */ new Map();
3738
4400
  if (opts.modes.includes("x402")) {
@@ -3796,7 +4458,8 @@ function routedock(opts) {
3796
4458
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3797
4459
  ...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
3798
4460
  ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
3799
- ...opts.logger ? { logger: opts.logger } : {}
4461
+ ...opts.logger ? { logger: opts.logger } : {},
4462
+ ...opts.vaultSettlement ? { vaultSettlement: opts.vaultSettlement } : {}
3800
4463
  })
3801
4464
  );
3802
4465
  }
@@ -3886,6 +4549,8 @@ var MPP_NETWORK3 = {
3886
4549
  testnet: "stellar:testnet",
3887
4550
  mainnet: "stellar:pubnet"
3888
4551
  };
4552
+ var DEFAULT_MAX_RECONCILE_ATTEMPTS = 5;
4553
+ var ALREADY_CLOSED_PATTERN = /channel has been closed|channel is closed|already closed|finalized/i;
3889
4554
  function decimalToStroops(amountStr) {
3890
4555
  return nulthSdk.usdcToStroops(amountStr);
3891
4556
  }
@@ -3895,12 +4560,15 @@ async function reconcileAbandonedSessions(opts) {
3895
4560
  recoveredCount: 0,
3896
4561
  skippedCount: 0,
3897
4562
  failedCount: 0,
4563
+ alreadyClosedCount: 0,
4564
+ terminalCount: 0,
3898
4565
  errors: []
3899
4566
  };
4567
+ const maxAttempts = opts.maxReconcileAttempts ?? DEFAULT_MAX_RECONCILE_ATTEMPTS;
3900
4568
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
3901
4569
  const networkId = MPP_NETWORK3[opts.network];
3902
4570
  try {
3903
- const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
4571
+ const { data: closingSessions, error } = await opts.supabase.from("sessions").select("channel_id, channel_contract, cumulative_amount, last_signature, settlement_tx_hash, reconcile_attempts").eq("status", "closing").eq("network", opts.network).eq("payee", payeeKeypair.publicKey()).is("settlement_tx_hash", null).not("last_signature", "is", null).order("updated_at", { ascending: true }).limit(100);
3904
4572
  if (error) {
3905
4573
  throw new Error(`Failed to query sessions: ${error.message}`);
3906
4574
  }
@@ -3922,7 +4590,7 @@ async function reconcileAbandonedSessions(opts) {
3922
4590
  continue;
3923
4591
  }
3924
4592
  const closeAmount = decimalToStroops(cumulative_amount);
3925
- const closeSig = Buffer.from(last_signature, "hex");
4593
+ const closeSig = hexToBytes(last_signature);
3926
4594
  const closeFn = opts.channelClose ?? (await import('@stellar/mpp/channel/server')).close;
3927
4595
  const closeTxHash = await closeFn({
3928
4596
  channel: channelId,
@@ -3945,11 +4613,36 @@ async function reconcileAbandonedSessions(opts) {
3945
4613
  await opts.onRecovered(channelId, closeTxHash, totalPaid);
3946
4614
  }
3947
4615
  } catch (err) {
4616
+ const reason = formatMppError(err);
4617
+ if (ALREADY_CLOSED_PATTERN.test(reason)) {
4618
+ const { error: closeError } = await opts.supabase.from("sessions").update({
4619
+ status: "closed",
4620
+ last_reconcile_error: `channel already closed on-chain: ${reason}`,
4621
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
4622
+ }).eq("channel_id", session.channel_id);
4623
+ if (!closeError) {
4624
+ stats.alreadyClosedCount++;
4625
+ continue;
4626
+ }
4627
+ }
3948
4628
  stats.failedCount++;
3949
- stats.errors.push({
3950
- channelId: session.channel_id,
3951
- reason: formatMppError(err)
3952
- });
4629
+ stats.errors.push({ channelId: session.channel_id, reason });
4630
+ const attempts = (Number(session.reconcile_attempts) || 0) + 1;
4631
+ const terminal = attempts >= maxAttempts;
4632
+ const { error: bookkeepingError } = await opts.supabase.from("sessions").update({
4633
+ reconcile_attempts: attempts,
4634
+ last_reconcile_error: reason,
4635
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
4636
+ ...terminal ? { status: "failed" } : {}
4637
+ }).eq("channel_id", session.channel_id);
4638
+ if (bookkeepingError) {
4639
+ stats.errors.push({
4640
+ channelId: session.channel_id,
4641
+ reason: `Failed to record reconcile failure: ${bookkeepingError.message}`
4642
+ });
4643
+ } else if (terminal) {
4644
+ stats.terminalCount++;
4645
+ }
3953
4646
  }
3954
4647
  }
3955
4648
  return stats;
@@ -3970,7 +4663,7 @@ async function runStartupReconciliation(opts, logger) {
3970
4663
  }
3971
4664
  log(
3972
4665
  "info",
3973
- `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
4666
+ `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}, Already closed: ${stats.alreadyClosedCount}, Moved to failed: ${stats.terminalCount}`
3974
4667
  );
3975
4668
  if (stats.errors.length > 0) {
3976
4669
  log("warn", "[SessionReconciler] Reconciliation errors:");
@@ -3987,7 +4680,7 @@ function tryDecodeBase64(value) {
3987
4680
  return value;
3988
4681
  }
3989
4682
  try {
3990
- const decoded = typeof Buffer !== "undefined" ? Buffer.from(value, "base64").toString("utf-8") : atob(value);
4683
+ const decoded = base64ToUtf8(value);
3991
4684
  if (decoded.length > 0 && decoded !== value) return decoded;
3992
4685
  } catch {
3993
4686
  }
@@ -4124,10 +4817,30 @@ var ProviderRegistry = class {
4124
4817
  }
4125
4818
  };
4126
4819
 
4820
+ Object.defineProperty(exports, "NulthPolicyError", {
4821
+ enumerable: true,
4822
+ get: function () { return nulthSdk.NulthPolicyError; }
4823
+ });
4127
4824
  Object.defineProperty(exports, "USDC_DECIMALS", {
4128
4825
  enumerable: true,
4129
4826
  get: function () { return nulthSdk.USDC_DECIMALS; }
4130
4827
  });
4828
+ Object.defineProperty(exports, "createPolicyState", {
4829
+ enumerable: true,
4830
+ get: function () { return nulthSdk.createPolicyState; }
4831
+ });
4832
+ Object.defineProperty(exports, "decodeAuthSignature", {
4833
+ enumerable: true,
4834
+ get: function () { return nulthSdk.decodeAuthSignature; }
4835
+ });
4836
+ Object.defineProperty(exports, "paymentContextFromManifest", {
4837
+ enumerable: true,
4838
+ get: function () { return nulthSdk.paymentContextFromManifest; }
4839
+ });
4840
+ Object.defineProperty(exports, "setNulthPaymentContext", {
4841
+ enumerable: true,
4842
+ get: function () { return nulthSdk.setNulthPaymentContext; }
4843
+ });
4131
4844
  Object.defineProperty(exports, "usdcToStroops", {
4132
4845
  enumerable: true,
4133
4846
  get: function () { return nulthSdk.usdcToStroops; }
@@ -4137,7 +4850,6 @@ exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
4137
4850
  exports.FileSpendStore = FileSpendStore;
4138
4851
  exports.InMemorySpendStore = InMemorySpendStore;
4139
4852
  exports.MppChargeClient = MppChargeClient;
4140
- exports.NulthPolicyError = NulthPolicyError;
4141
4853
  exports.OnChainRegistry = OnChainRegistry;
4142
4854
  exports.ProviderRegistry = ProviderRegistry;
4143
4855
  exports.RouteDockChannelStateError = RouteDockChannelStateError;
@@ -4151,6 +4863,7 @@ exports.RouteDockManifestSunsetError = RouteDockManifestSunsetError;
4151
4863
  exports.RouteDockManifestTimeoutError = RouteDockManifestTimeoutError;
4152
4864
  exports.RouteDockNetworkError = RouteDockNetworkError;
4153
4865
  exports.RouteDockNoSupportedModeError = RouteDockNoSupportedModeError;
4866
+ exports.RouteDockPaymentRejectedError = RouteDockPaymentRejectedError;
4154
4867
  exports.RouteDockPolicyRejectError = RouteDockPolicyRejectError;
4155
4868
  exports.RouteDockPolicyRejectedError = RouteDockPolicyRejectedError;
4156
4869
  exports.RouteDockRefundWindowError = RouteDockRefundWindowError;
@@ -4169,9 +4882,7 @@ exports.configureManifestCache = configureManifestCache;
4169
4882
  exports.consoleLogger = consoleLogger;
4170
4883
  exports.createConsoleLogger = createConsoleLogger;
4171
4884
  exports.createNulthSigner = createNulthSigner;
4172
- exports.createPolicyState = createPolicyState;
4173
4885
  exports.decimalToStroops = decimalToStroops;
4174
- exports.decodeAuthSignature = decodeAuthSignature;
4175
4886
  exports.fetchLedgerSequence = fetchLedgerSequence;
4176
4887
  exports.fetchManifest = fetchManifest;
4177
4888
  exports.getEligibleAssets = getEligibleAssets;
@@ -4182,7 +4893,7 @@ exports.manifestDigest = manifestDigest;
4182
4893
  exports.noopLogger = noopLogger;
4183
4894
  exports.normalizeManifestAssets = normalizeManifestAssets;
4184
4895
  exports.parseRetryAfterMs = parseRetryAfterMs;
4185
- exports.paymentContextFromManifest = paymentContextFromManifest;
4896
+ exports.paymentRejectedError = paymentRejectedError;
4186
4897
  exports.prepareNulthSigner = prepareNulthSigner;
4187
4898
  exports.rankProvidersByLatency = rankProvidersByLatency;
4188
4899
  exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
@@ -4193,7 +4904,6 @@ exports.routedock = routedock;
4193
4904
  exports.runStartupReconciliation = runStartupReconciliation;
4194
4905
  exports.selectAsset = selectAsset;
4195
4906
  exports.selectMode = selectMode;
4196
- exports.setNulthPaymentContext = setNulthPaymentContext;
4197
4907
  exports.signManifest = signManifest;
4198
4908
  exports.usdcToMicros = usdcToMicros;
4199
4909
  exports.usdcToUnits = usdcToUnits;