@routedock/routedock 0.3.0 → 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-CA_TdREG.d.ts → ProviderRegistry-D9KqyuH-.d.cts} +16 -36
  5. package/dist/{ProviderRegistry-D-0rqK0N.d.cts → ProviderRegistry-DVi2uzgk.d.ts} +16 -36
  6. package/dist/{RouteDockClient-go9aopfF.d.cts → RouteDockClient-6rmgkbYb.d.cts} +124 -142
  7. package/dist/{RouteDockClient-Bh86bVly.d.ts → RouteDockClient-CnAkxwrW.d.ts} +124 -142
  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 -487
  15. package/dist/client.d.cts +80 -10
  16. package/dist/client.d.ts +80 -10
  17. package/dist/client.js +733 -468
  18. package/dist/index.cjs +1295 -590
  19. package/dist/index.d.cts +13 -10
  20. package/dist/index.d.ts +13 -10
  21. package/dist/index.js +1247 -559
  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 -436
  45. package/dist/react.d.cts +7 -5
  46. package/dist/react.d.ts +7 -5
  47. package/dist/react.js +725 -425
  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/react.cjs CHANGED
@@ -3,20 +3,17 @@
3
3
  var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var stellarSdk = require('@stellar/stellar-sdk');
6
- var jsonSchema = require('@cfworker/json-schema');
7
6
  var nulthSdk = require('@routedock/nulth-sdk');
8
- var crypto = require('crypto');
7
+ var sha2_js = require('@noble/hashes/sha2.js');
8
+ var jsonSchema = require('@cfworker/json-schema');
9
9
  var stellar = require('@x402/stellar');
10
- var client = require('@x402/stellar/exact/client');
11
- var client$1 = require('@x402/core/client');
10
+ var client = require('@x402/core/client');
12
11
  var http = require('@x402/core/http');
13
- var client$3 = require('@stellar/mpp/charge/client');
14
- var client$2 = require('mppx/client');
15
- var mpp = require('@stellar/mpp');
16
- var client$4 = require('@stellar/mpp/channel/client');
12
+ var client$2 = require('@stellar/mpp/charge/client');
13
+ var client$1 = require('mppx/client');
17
14
  var mppx = require('mppx');
18
- require('fs/promises');
19
- require('path');
15
+ var mpp = require('@stellar/mpp');
16
+ var client$3 = require('@stellar/mpp/channel/client');
20
17
 
21
18
  // src/react/context.tsx
22
19
  var RouteDockContext = react.createContext(null);
@@ -123,6 +120,20 @@ var RouteDockPolicyRejectError = class extends RouteDockError {
123
120
  this.reason = reason;
124
121
  }
125
122
  };
123
+ var RouteDockPaymentRejectedError = class extends RouteDockError {
124
+ status;
125
+ /** The provider's `reason` field, if its response body was JSON and had one. */
126
+ reason;
127
+ /** The provider's `error` field (short summary), if present. */
128
+ providerError;
129
+ constructor(message, status, details = {}, options = {}) {
130
+ super(message, "PAYMENT_REJECTED", false, options);
131
+ this.name = "RouteDockPaymentRejectedError";
132
+ this.status = status;
133
+ if (details.reason !== void 0) this.reason = details.reason;
134
+ if (details.providerError !== void 0) this.providerError = details.providerError;
135
+ }
136
+ };
126
137
  var RouteDockChannelStateError = class extends RouteDockError {
127
138
  constructor(message, options = {}) {
128
139
  super(message, "CHANNEL_STATE", false, options);
@@ -159,6 +170,59 @@ function httpStatusToError(message, status, response) {
159
170
  }
160
171
  return new RouteDockNetworkError(message, opts);
161
172
  }
173
+ var MAX_REJECTION_BODY_BYTES = 64 * 1024;
174
+ var MAX_REJECTION_FIELD_CHARS = 500;
175
+ async function readBoundedText(response, limit) {
176
+ const body = response.body;
177
+ if (!body) return null;
178
+ const reader = body.getReader();
179
+ const chunks = [];
180
+ let total = 0;
181
+ try {
182
+ for (; ; ) {
183
+ const { done, value } = await reader.read();
184
+ if (done) break;
185
+ total += value.byteLength;
186
+ if (total > limit) {
187
+ await reader.cancel().catch(() => void 0);
188
+ return null;
189
+ }
190
+ chunks.push(value);
191
+ }
192
+ } catch {
193
+ return null;
194
+ }
195
+ const bytes = new Uint8Array(total);
196
+ let offset = 0;
197
+ for (const chunk of chunks) {
198
+ bytes.set(chunk, offset);
199
+ offset += chunk.byteLength;
200
+ }
201
+ return new TextDecoder().decode(bytes);
202
+ }
203
+ async function paymentRejectedError(label, status, response) {
204
+ let reason;
205
+ let providerError;
206
+ try {
207
+ const text = await readBoundedText(response, MAX_REJECTION_BODY_BYTES);
208
+ const body = text === null ? null : JSON.parse(text);
209
+ if (body !== null && typeof body === "object") {
210
+ const { reason: r, error: e } = body;
211
+ if (typeof r === "string" && r.length > 0) reason = r.slice(0, MAX_REJECTION_FIELD_CHARS);
212
+ if (typeof e === "string" && e.length > 0) providerError = e.slice(0, MAX_REJECTION_FIELD_CHARS);
213
+ }
214
+ } catch {
215
+ }
216
+ const detail = reason ?? providerError;
217
+ return new RouteDockPaymentRejectedError(
218
+ `${label}: HTTP ${status}${detail ? ` \u2014 ${detail}` : ""}`,
219
+ status,
220
+ {
221
+ ...reason !== void 0 ? { reason } : {},
222
+ ...providerError !== void 0 ? { providerError } : {}
223
+ }
224
+ );
225
+ }
162
226
  function wrapFetchError(err, context) {
163
227
  if (err instanceof RouteDockError) return err;
164
228
  return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
@@ -220,7 +284,10 @@ async function withRetry(fn, policy = {}) {
220
284
  if (!err.retryable || attempt >= maxAttempts - 1) {
221
285
  throw err;
222
286
  }
223
- const delay = err.retryAfterMs !== void 0 ? err.retryAfterMs : backoffDelayMs(attempt, baseDelayMs, maxDelayMs);
287
+ const delay = Math.min(
288
+ err.retryAfterMs ?? backoffDelayMs(attempt, baseDelayMs, maxDelayMs),
289
+ maxDelayMs
290
+ );
224
291
  if (onRetry) {
225
292
  try {
226
293
  await onRetry(attempt, err, delay);
@@ -233,10 +300,116 @@ async function withRetry(fn, policy = {}) {
233
300
  }
234
301
  throw lastError ?? new RouteDockError("Retry exhausted", "NETWORK", false);
235
302
  }
303
+
304
+ // src/network.ts
236
305
  var USDC_ISSUERS = {
237
306
  testnet: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
238
307
  mainnet: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
239
308
  };
309
+ var HORIZON_URLS = {
310
+ testnet: "https://horizon-testnet.stellar.org",
311
+ mainnet: "https://horizon.stellar.org"
312
+ };
313
+
314
+ // package.json
315
+ var package_default = {
316
+ version: "0.4.0"};
317
+
318
+ // src/internal/encoding.ts
319
+ var encoder = new TextEncoder();
320
+ var decoder = new TextDecoder();
321
+ function utf8ToBytes(text) {
322
+ return encoder.encode(text);
323
+ }
324
+ function bytesToUtf8(bytes) {
325
+ return decoder.decode(bytes);
326
+ }
327
+ function base64ToBytes(b64) {
328
+ const end = b64.indexOf("=");
329
+ let clean = (end === -1 ? b64 : b64.slice(0, end)).replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
330
+ if (clean.length % 4 === 1) clean = clean.slice(0, -1);
331
+ const binary = atob(clean);
332
+ const bytes = new Uint8Array(binary.length);
333
+ for (let i = 0; i < binary.length; i++) {
334
+ bytes[i] = binary.charCodeAt(i);
335
+ }
336
+ return bytes;
337
+ }
338
+ function bytesToHex(bytes) {
339
+ let out = "";
340
+ for (let i = 0; i < bytes.length; i++) {
341
+ out += bytes[i].toString(16).padStart(2, "0");
342
+ }
343
+ return out;
344
+ }
345
+ function sha256(data) {
346
+ return sha2_js.sha256(typeof data === "string" ? utf8ToBytes(data) : data);
347
+ }
348
+
349
+ // src/manifest/sign.ts
350
+ var MANIFEST_SIGNATURE_VERSION = "2";
351
+ function canonicalize(value) {
352
+ if (Array.isArray(value)) {
353
+ return value.map(canonicalize);
354
+ }
355
+ if (value !== null && typeof value === "object") {
356
+ return Object.fromEntries(
357
+ Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
358
+ );
359
+ }
360
+ return value;
361
+ }
362
+ function manifestDigest(manifest) {
363
+ const { signature: _omit, ...rest } = manifest;
364
+ const canonical = JSON.stringify(canonicalize(rest));
365
+ return sha256(utf8ToBytes(canonical));
366
+ }
367
+ function verifyManifestSignature(manifest, expectedPayee) {
368
+ if (!manifest.signature) {
369
+ throw new RouteDockSignatureError(
370
+ "Manifest is missing a signature field \u2014 cannot verify payee authenticity"
371
+ );
372
+ }
373
+ if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
374
+ throw new RouteDockSignatureError(
375
+ `Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
376
+ );
377
+ }
378
+ if (expectedPayee !== void 0 && manifest.payee !== expectedPayee) {
379
+ throw new RouteDockSignatureError(
380
+ `Manifest payee ${manifest.payee} does not match expected payee ${expectedPayee}`
381
+ );
382
+ }
383
+ let keypair;
384
+ try {
385
+ keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
386
+ } catch (err) {
387
+ throw new RouteDockSignatureError(
388
+ `Manifest payee ${manifest.payee} is not a valid Stellar public key`,
389
+ { cause: err }
390
+ );
391
+ }
392
+ const digest = manifestDigest(manifest);
393
+ let sigBytes;
394
+ try {
395
+ sigBytes = base64ToBytes(manifest.signature);
396
+ } catch {
397
+ throw new RouteDockSignatureError("Manifest signature is not valid base64");
398
+ }
399
+ let valid = false;
400
+ try {
401
+ valid = keypair.verify(digest, sigBytes);
402
+ } catch {
403
+ throw new RouteDockSignatureError(
404
+ `Manifest signature verification failed for payee ${manifest.payee}`
405
+ );
406
+ }
407
+ if (!valid) {
408
+ throw new RouteDockSignatureError(
409
+ `Manifest signature verification failed for payee ${manifest.payee}`
410
+ );
411
+ }
412
+ }
240
413
 
241
414
  // src/schemas/routedock.schema.json
242
415
  var routedock_schema_default = {
@@ -315,6 +488,7 @@ var routedock_schema_default = {
315
488
  },
316
489
  asset_contract: {
317
490
  type: "string",
491
+ pattern: "^C[A-Z2-7]{55}$",
318
492
  description: "Stellar Asset Contract (SAC) address for the payment asset"
319
493
  },
320
494
  assets: {
@@ -327,6 +501,7 @@ var routedock_schema_default = {
327
501
  },
328
502
  payee: {
329
503
  type: "string",
504
+ pattern: "^G[A-Z2-7]{55}$",
330
505
  description: "Stellar address (G...) that receives payments"
331
506
  },
332
507
  pricing: {
@@ -497,6 +672,7 @@ var routedock_schema_default = {
497
672
  properties: {
498
673
  amount: {
499
674
  type: "string",
675
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
500
676
  description: "Cost per request in the payment asset (decimal string, e.g. '0.001')"
501
677
  },
502
678
  per: {
@@ -510,6 +686,7 @@ var routedock_schema_default = {
510
686
  },
511
687
  payee: {
512
688
  type: "string",
689
+ pattern: "^G[A-Z2-7]{55}$",
513
690
  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."
514
691
  }
515
692
  }
@@ -522,6 +699,7 @@ var routedock_schema_default = {
522
699
  properties: {
523
700
  rate: {
524
701
  type: "string",
702
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
525
703
  description: "Cost per voucher in the payment asset (decimal string, e.g. '0.0001')"
526
704
  },
527
705
  per: {
@@ -531,10 +709,12 @@ var routedock_schema_default = {
531
709
  },
532
710
  channel_factory: {
533
711
  type: "string",
712
+ pattern: "^C[A-Z2-7]{55}$",
534
713
  description: "Stable Soroban factory address (C...) used to deploy a per-session channel"
535
714
  },
536
715
  min_deposit: {
537
716
  type: "string",
717
+ pattern: "^[0-9]+(\\.[0-9]{1,7})?$",
538
718
  description: "Minimum initial deposit to open a channel (decimal string)"
539
719
  },
540
720
  refund_waiting_period_ledgers: {
@@ -632,68 +812,110 @@ var routedock_schema_default = {
632
812
  }
633
813
  };
634
814
 
635
- // package.json
636
- var package_default = {
637
- version: "0.3.0"};
638
- var MANIFEST_SIGNATURE_VERSION = "2";
639
- function canonicalize(value) {
640
- if (Array.isArray(value)) {
641
- return value.map(canonicalize);
642
- }
643
- if (value !== null && typeof value === "object") {
644
- return Object.fromEntries(
645
- Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
646
- );
815
+ // src/manifest/validate.ts
816
+ var validator = new jsonSchema.Validator(routedock_schema_default, "7");
817
+ function validateManifestSchema(manifest, source) {
818
+ const result = validator.validate(manifest);
819
+ if (!result.valid) {
820
+ const msgs = result.errors.map((err) => err.error).join("; ");
821
+ throw new RouteDockManifestError(`Invalid manifest${source ? ` at ${source}` : ""}: ${msgs}`);
647
822
  }
648
- return value;
649
823
  }
650
- function manifestDigest(manifest) {
651
- const { signature: _omit, ...rest } = manifest;
652
- const canonical = JSON.stringify(canonicalize(rest));
653
- return crypto.createHash("sha256").update(canonical, "utf8").digest();
654
- }
655
- function verifyManifestSignature(manifest, expectedPayee) {
656
- if (!manifest.signature) {
657
- throw new RouteDockSignatureError(
658
- "Manifest is missing a signature field \u2014 cannot verify payee authenticity"
659
- );
660
- }
661
- if (manifest.signature_version !== MANIFEST_SIGNATURE_VERSION) {
662
- throw new RouteDockSignatureError(
663
- `Manifest signature version must be ${MANIFEST_SIGNATURE_VERSION}; legacy signatures are not safe because nested fields were excluded from their digest`
664
- );
824
+ function assertManifestValid(manifest, baseUrl) {
825
+ const context = baseUrl ? ` at ${baseUrl}` : "";
826
+ if (manifest.latency_hints !== void 0) {
827
+ if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
828
+ throw new RouteDockManifestError(
829
+ `Invalid manifest${context}: latency_hints defined without declaring regions`
830
+ );
831
+ }
832
+ const regionSet = new Set(manifest.regions);
833
+ for (const region of Object.keys(manifest.latency_hints)) {
834
+ if (!regionSet.has(region)) {
835
+ throw new RouteDockManifestError(
836
+ `Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
837
+ );
838
+ }
839
+ }
665
840
  }
666
- if (expectedPayee !== void 0 && manifest.payee !== expectedPayee) {
667
- throw new RouteDockSignatureError(
668
- `Manifest payee ${manifest.payee} does not match expected payee ${expectedPayee}`
669
- );
841
+ if (manifest.assets !== void 0) {
842
+ if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
843
+ throw new RouteDockManifestError(
844
+ `Invalid manifest${context}: assets must be a non-empty array when defined`
845
+ );
846
+ }
847
+ for (let i = 0; i < manifest.assets.length; i++) {
848
+ const a = manifest.assets[i];
849
+ if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
850
+ throw new RouteDockManifestError(
851
+ `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
852
+ );
853
+ }
854
+ }
855
+ const first = manifest.assets[0];
856
+ if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
857
+ throw new RouteDockManifestError(
858
+ `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
859
+ );
860
+ }
670
861
  }
671
- const keypair = stellarSdk.Keypair.fromPublicKey(manifest.payee);
672
- const digest = manifestDigest(manifest);
673
- let sigBytes;
674
- try {
675
- sigBytes = Buffer.from(manifest.signature, "base64");
676
- } catch {
677
- throw new RouteDockSignatureError("Manifest signature is not valid base64");
862
+ assertPricingMatchesModes(manifest, context);
863
+ assertDeprecatedModesListed(manifest, context);
864
+ assertPricesParse(manifest, context);
865
+ }
866
+ function assertPricingMatchesModes(manifest, context) {
867
+ const pricing = manifest.pricing ?? {};
868
+ for (const mode of manifest.modes ?? []) {
869
+ if (pricing[mode] === void 0 || pricing[mode] === null) {
870
+ throw new RouteDockManifestError(
871
+ `Invalid manifest${context}: mode '${mode}' is listed in modes but pricing.${mode} is missing`
872
+ );
873
+ }
678
874
  }
679
- let valid = false;
680
- try {
681
- valid = keypair.verify(digest, sigBytes);
682
- } catch {
683
- throw new RouteDockSignatureError(
684
- `Manifest signature verification failed for payee ${manifest.payee}`
685
- );
875
+ }
876
+ function assertDeprecatedModesListed(manifest, context) {
877
+ if (manifest.deprecated_modes === void 0) return;
878
+ const modes = new Set(manifest.modes ?? []);
879
+ for (const mode of manifest.deprecated_modes) {
880
+ if (!modes.has(mode)) {
881
+ throw new RouteDockManifestError(
882
+ `Invalid manifest${context}: deprecated_modes entry '${mode}' is not listed in modes`
883
+ );
884
+ }
686
885
  }
687
- if (!valid) {
688
- throw new RouteDockSignatureError(
689
- `Manifest signature verification failed for payee ${manifest.payee}`
690
- );
886
+ }
887
+ var PRICE_FIELDS = {
888
+ x402: ["amount"],
889
+ "mpp-charge": ["amount"],
890
+ "mpp-session": ["rate", "min_deposit"],
891
+ "mpp-session-ws": ["rate", "min_deposit"]
892
+ };
893
+ function assertPricesParse(manifest, context) {
894
+ const pricing = manifest.pricing ?? {};
895
+ for (const [mode, fields] of Object.entries(PRICE_FIELDS)) {
896
+ const entry = pricing[mode];
897
+ if (entry === void 0 || entry === null) continue;
898
+ for (const field of fields) {
899
+ const value = entry[field];
900
+ try {
901
+ if (typeof value !== "string") {
902
+ throw new RangeError(`expected a decimal string, got ${typeof value}`);
903
+ }
904
+ nulthSdk.usdcToStroops(value);
905
+ } catch (err) {
906
+ const detail = err instanceof Error ? err.message : String(err);
907
+ throw new RouteDockManifestError(
908
+ `Invalid manifest${context}: pricing.${mode}.${field} ${JSON.stringify(value)} is not a valid amount (${detail})`,
909
+ { cause: err }
910
+ );
911
+ }
912
+ }
691
913
  }
692
914
  }
693
915
 
694
916
  // src/client/ModeRouter.ts
695
- var validator = new jsonSchema.Validator(routedock_schema_default, "7");
696
917
  var SDK_VERSION = package_default.version;
918
+ var SUPPORTED_MANIFEST_MAJOR = 1;
697
919
  function parseMajorMinor(version) {
698
920
  const parts = version.split(".");
699
921
  return [Number.parseInt(parts[0] ?? "0", 10), Number.parseInt(parts[1] ?? "0", 10)];
@@ -711,6 +933,14 @@ function assertClientVersionSupported(manifest, baseUrl) {
711
933
  );
712
934
  }
713
935
  }
936
+ function assertManifestVersionSupported(manifest, baseUrl) {
937
+ const [major] = parseMajorMinor(manifest.routedock);
938
+ if (major > SUPPORTED_MANIFEST_MAJOR) {
939
+ throw new RouteDockClientVersionError(
940
+ `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.`
941
+ );
942
+ }
943
+ }
714
944
  function assertManifestActive(manifest, baseUrl) {
715
945
  const sunsetAt = manifest.sunset_at;
716
946
  if (!sunsetAt) return;
@@ -726,11 +956,16 @@ function assertManifestActive(manifest, baseUrl) {
726
956
  );
727
957
  }
728
958
  }
729
- function assertEndpointActive(manifest, url, logger) {
959
+ function assertEndpointActive(manifest, url, logger, method) {
730
960
  const pathname = new URL(url).pathname;
731
- const matches = Object.entries(manifest.endpoints).filter(
961
+ const pathMatches = Object.entries(manifest.endpoints).filter(
732
962
  ([, descriptor]) => descriptor.path === pathname
733
963
  );
964
+ if (pathMatches.length === 0) return;
965
+ const requestedMethod = method?.toUpperCase();
966
+ const matches = requestedMethod ? pathMatches.filter(
967
+ ([, descriptor]) => descriptor.method.toUpperCase() === requestedMethod
968
+ ) : pathMatches;
734
969
  if (matches.length === 0) return;
735
970
  const now = Date.now();
736
971
  const allSunset = matches.every(([, descriptor]) => {
@@ -753,45 +988,6 @@ function assertEndpointActive(manifest, url, logger) {
753
988
  );
754
989
  }
755
990
  }
756
- function assertManifestValid(manifest, baseUrl) {
757
- const context = baseUrl ? ` at ${baseUrl}` : "";
758
- if (manifest.latency_hints !== void 0) {
759
- if (!Array.isArray(manifest.regions) || manifest.regions.length === 0) {
760
- throw new RouteDockManifestError(
761
- `Invalid manifest${context}: latency_hints defined without declaring regions`
762
- );
763
- }
764
- const regionSet = new Set(manifest.regions);
765
- for (const region of Object.keys(manifest.latency_hints)) {
766
- if (!regionSet.has(region)) {
767
- throw new RouteDockManifestError(
768
- `Invalid manifest${context}: latency_hints key '${region}' is not declared in regions (${manifest.regions.join(", ")})`
769
- );
770
- }
771
- }
772
- }
773
- if (manifest.assets !== void 0) {
774
- if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
775
- throw new RouteDockManifestError(
776
- `Invalid manifest${context}: assets must be a non-empty array when defined`
777
- );
778
- }
779
- for (let i = 0; i < manifest.assets.length; i++) {
780
- const a = manifest.assets[i];
781
- if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
782
- throw new RouteDockManifestError(
783
- `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
784
- );
785
- }
786
- }
787
- const first = manifest.assets[0];
788
- if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
789
- throw new RouteDockManifestError(
790
- `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
791
- );
792
- }
793
- }
794
- }
795
991
  var CACHE_TTL_MS = 6e4;
796
992
  var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
797
993
  var DEFAULT_MANIFEST_TIMEOUT_MS = 5e3;
@@ -832,6 +1028,7 @@ var LruCache = class {
832
1028
  }
833
1029
  };
834
1030
  var manifestCache = new LruCache(DEFAULT_MANIFEST_CACHE_MAX_SIZE);
1031
+ var inflightManifestFetches = /* @__PURE__ */ new Map();
835
1032
  function invalidateManifest(baseUrl) {
836
1033
  manifestCache.delete(baseUrl);
837
1034
  }
@@ -861,10 +1058,27 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
861
1058
  if (cached && Date.now() < cached.expiresAt) {
862
1059
  verifyManifestSignature(cached.manifest, expectedPayee);
863
1060
  assertClientVersionSupported(cached.manifest, baseUrl);
1061
+ assertManifestVersionSupported(cached.manifest, baseUrl);
864
1062
  assertManifestValid(cached.manifest, baseUrl);
865
1063
  assertManifestActive(cached.manifest, baseUrl);
866
1064
  return cached.manifest;
867
1065
  }
1066
+ let inflight = inflightManifestFetches.get(baseUrl);
1067
+ if (!inflight) {
1068
+ inflight = fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs).finally(() => {
1069
+ inflightManifestFetches.delete(baseUrl);
1070
+ });
1071
+ inflightManifestFetches.set(baseUrl, inflight);
1072
+ }
1073
+ const manifest = await inflight;
1074
+ verifyManifestSignature(manifest, expectedPayee);
1075
+ assertClientVersionSupported(manifest, baseUrl);
1076
+ assertManifestVersionSupported(manifest, baseUrl);
1077
+ assertManifestValid(manifest, baseUrl);
1078
+ assertManifestActive(manifest, baseUrl);
1079
+ return manifest;
1080
+ }
1081
+ async function fetchAndCacheManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_MANIFEST_TIMEOUT_MS) {
868
1082
  const url = baseUrl.replace(/\/$/, "") + "/.well-known/routedock.json";
869
1083
  return withRetry(async () => {
870
1084
  let raw;
@@ -901,15 +1115,12 @@ async function fetchManifest(baseUrl, retryPolicy, manifestTimeoutMs = DEFAULT_M
901
1115
  } catch (err) {
902
1116
  throw new RouteDockManifestError(`Manifest at ${url} is not valid JSON`, { cause: err });
903
1117
  }
904
- const result = validator.validate(raw);
905
- if (!result.valid) {
906
- const msgs = result.errors.map((err) => err.error).join("; ");
907
- throw new RouteDockManifestError(`Invalid manifest at ${url}: ${msgs}`);
908
- }
1118
+ validateManifestSchema(raw, url);
909
1119
  const manifest = raw;
910
1120
  assertManifestValid(manifest, baseUrl);
911
- verifyManifestSignature(manifest, expectedPayee);
1121
+ verifyManifestSignature(manifest);
912
1122
  assertClientVersionSupported(manifest, baseUrl);
1123
+ assertManifestVersionSupported(manifest, baseUrl);
913
1124
  assertManifestActive(manifest, baseUrl);
914
1125
  manifestCache.set(baseUrl, { manifest, fetchedAt: Date.now(), expiresAt });
915
1126
  return manifest;
@@ -1008,6 +1219,48 @@ function selectMode(manifest, options = {}) {
1008
1219
  );
1009
1220
  }
1010
1221
 
1222
+ // src/internal/settleResult.ts
1223
+ function readSettleResult(result) {
1224
+ const settle = result;
1225
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
1226
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
1227
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
1228
+ return failed(settle.errorReason);
1229
+ }
1230
+ return { ok: true, txHash: settle.transaction };
1231
+ }
1232
+
1233
+ // src/internal/request.ts
1234
+ var DEFAULT_METHOD = "GET";
1235
+ var BODYLESS_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
1236
+ function isReplayable(body) {
1237
+ 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;
1238
+ }
1239
+ async function bufferBody(body) {
1240
+ if (isReplayable(body)) return body;
1241
+ return new Response(body).arrayBuffer();
1242
+ }
1243
+ async function prepareRequest(request) {
1244
+ const method = (request?.method ?? DEFAULT_METHOD).toUpperCase();
1245
+ const headers = new Headers(request?.headers);
1246
+ const body = request?.body;
1247
+ if (body === void 0 || BODYLESS_METHODS.has(method)) {
1248
+ return { method, headers };
1249
+ }
1250
+ return { method, headers, body: await bufferBody(body) };
1251
+ }
1252
+ function toRequestInit(prepared, overrides) {
1253
+ const headers = new Headers(prepared.headers);
1254
+ if (overrides) {
1255
+ new Headers(overrides).forEach((value, name) => headers.set(name, value));
1256
+ }
1257
+ const init = { method: prepared.method, headers };
1258
+ if (prepared.body !== void 0) {
1259
+ init.body = prepared.body;
1260
+ }
1261
+ return init;
1262
+ }
1263
+
1011
1264
  // src/internal/payee.ts
1012
1265
  function resolvePayee(manifest, mode) {
1013
1266
  const override = manifest.pricing[mode]?.payee;
@@ -1068,6 +1321,102 @@ function checkChargeChallenge(request, manifest, network, maxAmount) {
1068
1321
  }
1069
1322
  return checkAmount(request["amount"], maxAmount);
1070
1323
  }
1324
+ var defaultIo = {
1325
+ async expirationLedger(network, maxTimeoutSeconds) {
1326
+ const { sequence } = await stellar.getRpcClient(network).getLatestLedger();
1327
+ const secondsPerLedger = await stellar.getEstimatedLedgerCloseTimeSeconds(network);
1328
+ return sequence + Math.ceil(maxTimeoutSeconds / secondsPerLedger);
1329
+ },
1330
+ async assemble({ asset, from, payTo, amount, networkPassphrase, rpcUrl }) {
1331
+ return stellarSdk.contract.AssembledTransaction.build({
1332
+ contractId: asset,
1333
+ method: "transfer",
1334
+ // SEP-41: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface
1335
+ args: [
1336
+ stellarSdk.nativeToScVal(from, { type: "address" }),
1337
+ stellarSdk.nativeToScVal(payTo, { type: "address" }),
1338
+ stellarSdk.nativeToScVal(amount, { type: "i128" })
1339
+ ],
1340
+ networkPassphrase,
1341
+ rpcUrl,
1342
+ parseResultXdr: (result) => result
1343
+ });
1344
+ }
1345
+ };
1346
+ var NulthStellarScheme = class {
1347
+ constructor(signer, io = defaultIo) {
1348
+ this.signer = signer;
1349
+ this.io = io;
1350
+ }
1351
+ signer;
1352
+ io;
1353
+ scheme = "exact";
1354
+ async createPaymentPayload(x402Version, paymentRequirements) {
1355
+ try {
1356
+ this.validateRequirements(paymentRequirements);
1357
+ } catch (err) {
1358
+ throw new Error(
1359
+ `Invalid input parameters for creating Nulth Stellar payment, cause: ${String(err)}`
1360
+ );
1361
+ }
1362
+ const from = this.signer.address;
1363
+ const { network, payTo, asset, amount, maxTimeoutSeconds } = paymentRequirements;
1364
+ if (!paymentRequirements.extra.areFeesSponsored) {
1365
+ throw new Error("Nulth exact scheme requires areFeesSponsored to be true");
1366
+ }
1367
+ const maxLedger = await this.io.expirationLedger(network, maxTimeoutSeconds);
1368
+ const tx = await this.io.assemble({
1369
+ asset,
1370
+ from,
1371
+ payTo,
1372
+ amount,
1373
+ networkPassphrase: stellar.getNetworkPassphrase(network),
1374
+ rpcUrl: stellar.getRpcUrl(network)
1375
+ });
1376
+ stellar.handleSimulationResult(tx.simulation);
1377
+ const signers = tx.needsNonInvokerSigningBy();
1378
+ if (!signers.includes(from) || signers.length > 1) {
1379
+ throw new Error(`Expected to sign with [${from}], but got [${signers.join(", ")}]`);
1380
+ }
1381
+ await tx.signAuthEntries({
1382
+ address: from,
1383
+ signAuthEntry: this.signer.signAuthEntry,
1384
+ expiration: maxLedger,
1385
+ authorizeEntry: nulthSdk.nulthAuthorizeEntry
1386
+ });
1387
+ await tx.simulate();
1388
+ stellar.handleSimulationResult(tx.simulation);
1389
+ const remaining = tx.needsNonInvokerSigningBy();
1390
+ if (remaining.length > 0) {
1391
+ throw new Error(`unexpected signer(s) required: [${remaining.join(", ")}]`);
1392
+ }
1393
+ if (!tx.built) {
1394
+ throw new Error("Nulth payment transaction has no assembled envelope to send");
1395
+ }
1396
+ return { x402Version, payload: { transaction: tx.built.toXDR() } };
1397
+ }
1398
+ validateRequirements(requirements) {
1399
+ const { scheme, network, payTo, asset, amount } = requirements;
1400
+ if (typeof amount !== "string" || !Number.isInteger(Number(amount)) || Number(amount) <= 0) {
1401
+ throw new Error(`Invalid amount: ${amount}. Amount must be a positive integer.`);
1402
+ }
1403
+ if (scheme !== this.scheme) {
1404
+ throw new Error(`Unsupported scheme: ${scheme}`);
1405
+ }
1406
+ if (!stellar.isStellarNetwork(network)) {
1407
+ throw new Error(`Unsupported Stellar network: ${network}`);
1408
+ }
1409
+ if (!stellar.validateStellarDestinationAddress(payTo)) {
1410
+ throw new Error(`Invalid Stellar destination address: ${payTo}`);
1411
+ }
1412
+ if (!stellar.validateStellarAssetAddress(asset)) {
1413
+ throw new Error(`Invalid Stellar asset address: ${asset}`);
1414
+ }
1415
+ }
1416
+ };
1417
+ function selectStellarScheme(signer) {
1418
+ return nulthSdk.isNulthStellarSigner(signer) ? new NulthStellarScheme(signer) : new stellar.ExactStellarScheme(signer);
1419
+ }
1071
1420
 
1072
1421
  // src/client/x402Client.ts
1073
1422
  var CAIP2 = {
@@ -1075,25 +1424,29 @@ var CAIP2 = {
1075
1424
  mainnet: "stellar:pubnet"
1076
1425
  };
1077
1426
  var X402Client = class _X402Client {
1078
- constructor(secretKeyOrSigner, network, retryPolicy) {
1427
+ constructor(secretKeyOrSigner, network, retryPolicy, preSignHook, logger) {
1079
1428
  this.network = network;
1080
1429
  this.retryPolicy = retryPolicy;
1430
+ this.preSignHook = preSignHook;
1081
1431
  const caip2 = CAIP2[network];
1082
1432
  this.signer = typeof secretKeyOrSigner === "string" ? stellar.createEd25519Signer(secretKeyOrSigner, caip2) : secretKeyOrSigner;
1083
- const scheme = new client.ExactStellarScheme(this.signer);
1084
- const core = new client$1.x402Client();
1433
+ const scheme = selectStellarScheme(this.signer);
1434
+ const core = new client.x402Client();
1085
1435
  core.register(caip2, scheme);
1086
- this.httpClient = new client$1.x402HTTPClient(core);
1436
+ this.httpClient = new client.x402HTTPClient(core);
1437
+ this.logger = logger;
1087
1438
  }
1088
1439
  network;
1089
1440
  retryPolicy;
1441
+ preSignHook;
1090
1442
  httpClient;
1091
1443
  signer;
1444
+ logger;
1092
1445
  /** Replace signer (e.g. swap to Nulth ZK account payer before pay) */
1093
1446
  withSigner(signer) {
1094
- return new _X402Client(signer, this.network, this.retryPolicy);
1447
+ return new _X402Client(signer, this.network, this.retryPolicy, this.preSignHook, this.logger);
1095
1448
  }
1096
- async pay(url, manifest) {
1449
+ async pay(url, manifest, request) {
1097
1450
  const pricing = manifest.pricing.x402;
1098
1451
  if (!pricing) {
1099
1452
  throw new RouteDockManifestError("manifest.pricing.x402 missing");
@@ -1102,13 +1455,15 @@ var X402Client = class _X402Client {
1102
1455
  throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
1103
1456
  }
1104
1457
  const caip2 = CAIP2[this.network];
1458
+ const prepared = await prepareRequest(request);
1105
1459
  const probe = await withRetry(async () => {
1106
1460
  let init2;
1107
1461
  try {
1108
- init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
1462
+ init2 = await fetch(url, toRequestInit(prepared, { "X-Preferred-Mode": "x402" }));
1109
1463
  } catch (err) {
1110
1464
  throw wrapFetchError(err, "x402 initial request");
1111
1465
  }
1466
+ this.logger?.("debug", "[x402] unpaid probe response", { status: init2.status });
1112
1467
  if (init2.status !== 402) {
1113
1468
  if (!init2.ok) {
1114
1469
  throw httpStatusToError(
@@ -1125,6 +1480,11 @@ var X402Client = class _X402Client {
1125
1480
  `Failed to parse JSON from non-402 response (HTTP ${init2.status})`
1126
1481
  );
1127
1482
  }
1483
+ this.logger?.(
1484
+ "debug",
1485
+ "[x402] unpaid probe returned non-402 success \u2014 serving as free response",
1486
+ { status: init2.status }
1487
+ );
1128
1488
  return { kind: "free", data: data2 };
1129
1489
  }
1130
1490
  return { kind: "payment-required", init: init2 };
@@ -1153,6 +1513,18 @@ var X402Client = class _X402Client {
1153
1513
  if (allowed.length === 0) {
1154
1514
  throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
1155
1515
  }
1516
+ if (this.preSignHook) {
1517
+ let decision;
1518
+ try {
1519
+ decision = await this.preSignHook({ url, manifest, accepts: allowed });
1520
+ } catch (err) {
1521
+ throw new RouteDockPolicyRejectError("pre_sign_hook_error", { cause: err });
1522
+ }
1523
+ if (!decision || decision.approve !== true) {
1524
+ const reason = decision && decision.approve === false ? decision.reason : void 0;
1525
+ throw new RouteDockPolicyRejectError(reason ?? "pre_sign_rejected");
1526
+ }
1527
+ }
1156
1528
  let paymentPayload;
1157
1529
  try {
1158
1530
  paymentPayload = await this.httpClient.createPaymentPayload({
@@ -1166,13 +1538,15 @@ var X402Client = class _X402Client {
1166
1538
  }
1167
1539
  const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1168
1540
  const signedAmount = nulthSdk.stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
1541
+ this.logger?.("debug", "[x402] sending paid request with payment header");
1169
1542
  const settled = await withRetry(async () => {
1170
1543
  let response;
1171
1544
  try {
1172
- response = await fetch(url, { headers: paymentHeaders });
1545
+ response = await fetch(url, toRequestInit(prepared, paymentHeaders));
1173
1546
  } catch (err) {
1174
1547
  throw wrapFetchError(err, "x402 settlement request");
1175
1548
  }
1549
+ this.logger?.("debug", "[x402] paid request response", { status: response.status });
1176
1550
  if (!response.ok) {
1177
1551
  if (response.status >= 500 || response.status === 429 || response.status === 503) {
1178
1552
  throw httpStatusToError(
@@ -1181,17 +1555,28 @@ var X402Client = class _X402Client {
1181
1555
  response
1182
1556
  );
1183
1557
  }
1184
- throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
1558
+ throw await paymentRejectedError("x402 payment failed", response.status, response);
1185
1559
  }
1186
1560
  return response;
1187
1561
  }, this.retryPolicy);
1188
1562
  let txHash = null;
1189
1563
  const responseHeader = settled.headers.get("X-Payment-Response");
1190
- if (responseHeader) {
1564
+ if (!responseHeader) {
1565
+ this.logger?.("debug", "[x402] settled 2xx carried no X-Payment-Response header");
1566
+ } else {
1191
1567
  try {
1192
- const settleResponse = http.decodePaymentResponseHeader(responseHeader);
1193
- txHash = settleResponse.transaction ?? null;
1194
- } catch {
1568
+ const outcome = readSettleResult(http.decodePaymentResponseHeader(responseHeader));
1569
+ if (outcome.ok) {
1570
+ txHash = outcome.txHash;
1571
+ } else {
1572
+ this.logger?.("debug", "[x402] X-Payment-Response reports an unsettled payment", {
1573
+ ...outcome.reason !== void 0 ? { reason: outcome.reason } : {}
1574
+ });
1575
+ }
1576
+ } catch (err) {
1577
+ this.logger?.("debug", "[x402] X-Payment-Response header present but undecodable", {
1578
+ error: err
1579
+ });
1195
1580
  }
1196
1581
  }
1197
1582
  let data;
@@ -1212,33 +1597,28 @@ var MppChargeClient = class {
1212
1597
  keypair;
1213
1598
  network;
1214
1599
  retryPolicy;
1215
- async pay(url, manifest) {
1600
+ async pay(url, manifest, request) {
1216
1601
  const pricing = manifest.pricing["mpp-charge"];
1217
1602
  if (!pricing) {
1218
1603
  throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
1219
1604
  }
1220
1605
  const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
1221
- let txHash = null;
1606
+ const prepared = await prepareRequest(request);
1222
1607
  let signedAmount;
1223
1608
  let credential;
1224
- const mppx = client$2.Mppx.create({
1609
+ const mppx = client$1.Mppx.create({
1225
1610
  polyfill: false,
1226
1611
  methods: [
1227
- client$3.stellar.charge({
1612
+ client$2.stellar.charge({
1228
1613
  keypair: this.keypair,
1229
- mode: "pull",
1230
- onProgress(event) {
1231
- if (event.type === "paid") {
1232
- txHash = event.hash;
1233
- }
1234
- }
1614
+ mode: "pull"
1235
1615
  })
1236
1616
  ],
1237
1617
  onChallenge: async (challenge, { createCredential }) => {
1238
- const request = challenge.request;
1239
- const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
1618
+ const request2 = challenge.request;
1619
+ const rejection = checkChargeChallenge(request2, manifest, caip2, pricing.amount);
1240
1620
  if (rejection) throw rejection;
1241
- const amount = request["amount"];
1621
+ const amount = request2["amount"];
1242
1622
  if (typeof amount !== "string") {
1243
1623
  throw new RouteDockPolicyRejectError("challenge_amount_invalid");
1244
1624
  }
@@ -1250,7 +1630,7 @@ var MppChargeClient = class {
1250
1630
  return withRetry(async () => {
1251
1631
  let response;
1252
1632
  try {
1253
- response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
1633
+ response = credential ? await mppx.rawFetch(url, toRequestInit(prepared, { Authorization: credential })) : await mppx.fetch(url, toRequestInit(prepared));
1254
1634
  } catch (err) {
1255
1635
  throw wrapMppError(err, "MPP charge request");
1256
1636
  }
@@ -1262,8 +1642,9 @@ var MppChargeClient = class {
1262
1642
  response
1263
1643
  );
1264
1644
  }
1265
- throw new RouteDockManifestError(`MPP charge failed: HTTP ${response.status}`);
1645
+ throw await paymentRejectedError("MPP charge failed", response.status, response);
1266
1646
  }
1647
+ const txHash = readSettlementHash(response);
1267
1648
  let data;
1268
1649
  try {
1269
1650
  data = await response.json();
@@ -1283,6 +1664,13 @@ var MppChargeClient = class {
1283
1664
  }, this.retryPolicy);
1284
1665
  }
1285
1666
  };
1667
+ function readSettlementHash(response) {
1668
+ try {
1669
+ return mppx.Receipt.fromResponse(response).reference;
1670
+ } catch {
1671
+ return null;
1672
+ }
1673
+ }
1286
1674
 
1287
1675
  // src/types.ts
1288
1676
  var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
@@ -1290,6 +1678,31 @@ var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
1290
1678
  // src/client/MppSessionClient.ts
1291
1679
  var MIN_REFUND_WAITING_PERIOD = 17280;
1292
1680
  var STREAM_CLEANUP_TIMEOUT_MS = 3e3;
1681
+ var TX_CONFIRM_INTERVAL_MS = 1e3;
1682
+ var TX_CONFIRM_TIMEOUT_MS = 3e4;
1683
+ async function confirmTransaction(server, hash, label, timeoutMs = TX_CONFIRM_TIMEOUT_MS, intervalMs = TX_CONFIRM_INTERVAL_MS) {
1684
+ const deadline = Date.now() + timeoutMs;
1685
+ while (true) {
1686
+ const txResponse = await server.getTransaction(hash);
1687
+ const status = txResponse?.status;
1688
+ if (status === "SUCCESS") {
1689
+ return hash;
1690
+ }
1691
+ if (status === "FAILED") {
1692
+ throw new RouteDockDisputeError(
1693
+ `${label} transaction failed on-chain: status FAILED (resultXdr: ${txResponse.resultXdr.toXDR("base64")})`
1694
+ );
1695
+ }
1696
+ if (Date.now() >= deadline) {
1697
+ break;
1698
+ }
1699
+ const remaining = deadline - Date.now();
1700
+ await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(1, remaining))));
1701
+ }
1702
+ throw new RouteDockDisputeError(
1703
+ `${label} transaction confirmation timed out after ${timeoutMs}ms (hash: ${hash})`
1704
+ );
1705
+ }
1293
1706
  var WS_CONNECTING = 0;
1294
1707
  var WS_OPEN = 1;
1295
1708
  function defaultWebSocketFactory(url, init, handlers) {
@@ -1303,9 +1716,9 @@ function defaultWebSocketFactory(url, init, handlers) {
1303
1716
  if (typeof data === "string") {
1304
1717
  handlers.onMessage(data);
1305
1718
  } else if (data instanceof ArrayBuffer) {
1306
- handlers.onMessage(Buffer.from(data).toString("utf8"));
1719
+ handlers.onMessage(bytesToUtf8(new Uint8Array(data)));
1307
1720
  } else {
1308
- void data.arrayBuffer().then((buffer) => handlers.onMessage(Buffer.from(buffer).toString("utf8"))).catch(() => {
1721
+ void data.arrayBuffer().then((buffer) => handlers.onMessage(bytesToUtf8(new Uint8Array(buffer)))).catch(() => {
1309
1722
  });
1310
1723
  }
1311
1724
  };
@@ -1458,11 +1871,11 @@ var MppSessionClient = class {
1458
1871
  throw err;
1459
1872
  }
1460
1873
  };
1461
- const streamWs = (u, m, createCredential, onSigned) => this.streamWebSocket(u, m, createCredential, onSigned);
1462
- const mppx = client$2.Mppx.create({
1874
+ const streamWs = (u, m, createCredential, onSigned, request) => this.streamWebSocket(u, m, createCredential, onSigned, request);
1875
+ const mppx = client$1.Mppx.create({
1463
1876
  polyfill: false,
1464
1877
  methods: [
1465
- client$4.stellar.channel({
1878
+ client$3.stellar.channel({
1466
1879
  commitmentKey,
1467
1880
  sourceAccount: this.keypair.publicKey(),
1468
1881
  ...store ? { store } : {},
@@ -1530,6 +1943,7 @@ var MppSessionClient = class {
1530
1943
  };
1531
1944
  },
1532
1945
  async *stream(options2) {
1946
+ const prepared = await prepareRequest(options2?.request);
1533
1947
  const checkSpend = () => {
1534
1948
  if (!onSpend) return Promise.resolve();
1535
1949
  return onSpend(pricing.rate);
@@ -1539,7 +1953,13 @@ var MppSessionClient = class {
1539
1953
  const onSigned = () => {
1540
1954
  vouchersIssued++;
1541
1955
  };
1542
- for await (const item of streamWs(url, mppx, createGuardedCredential, onSigned)) {
1956
+ for await (const item of streamWs(
1957
+ url,
1958
+ mppx,
1959
+ createGuardedCredential,
1960
+ onSigned,
1961
+ prepared
1962
+ )) {
1543
1963
  yield item;
1544
1964
  }
1545
1965
  return;
@@ -1554,7 +1974,10 @@ var MppSessionClient = class {
1554
1974
  const doFetch = () => withRetry(async () => {
1555
1975
  let resp;
1556
1976
  try {
1557
- resp = await mppx.fetch(url, { signal: abortController.signal });
1977
+ resp = await mppx.fetch(url, {
1978
+ ...toRequestInit(prepared),
1979
+ signal: abortController.signal
1980
+ });
1558
1981
  } catch (err) {
1559
1982
  if (abortController.signal.aborted) {
1560
1983
  throw err;
@@ -1639,10 +2062,10 @@ var MppSessionClient = class {
1639
2062
  async close() {
1640
2063
  clearSessionTimer();
1641
2064
  closed = true;
1642
- const { rpc: rpcMod, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
2065
+ const { rpc: rpcMod, Contract, nativeToScVal: nativeToScVal2, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
1643
2066
  const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
1644
2067
  const server = new rpcMod.Server(rpcUrl);
1645
- const contract = new Contract(channelFactory);
2068
+ const contract2 = new Contract(channelFactory);
1646
2069
  const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
1647
2070
  const account = await withRetry(async () => {
1648
2071
  try {
@@ -1657,9 +2080,9 @@ var MppSessionClient = class {
1657
2080
  fee: BASE_FEE,
1658
2081
  networkPassphrase: passphrase
1659
2082
  }).addOperation(
1660
- contract.call(
2083
+ contract2.call(
1661
2084
  "prepare_commitment",
1662
- nativeToScVal(currentCumulative, { type: "i128" })
2085
+ nativeToScVal2(currentCumulative, { type: "i128" })
1663
2086
  )
1664
2087
  ).setTimeout(30).build();
1665
2088
  return await server.simulateTransaction(simTx);
@@ -1678,7 +2101,7 @@ var MppSessionClient = class {
1678
2101
  }
1679
2102
  let signature;
1680
2103
  try {
1681
- signature = commitmentKey.sign(Buffer.from(commitmentBytes));
2104
+ signature = commitmentKey.sign(commitmentBytes);
1682
2105
  } catch (err) {
1683
2106
  throw new RouteDockSignatureError("Channel close commitment signing failed", {
1684
2107
  cause: err
@@ -1692,7 +2115,7 @@ var MppSessionClient = class {
1692
2115
  headers: { "Content-Type": "application/json" },
1693
2116
  body: JSON.stringify({
1694
2117
  amount: currentCumulative.toString(),
1695
- signature: Buffer.from(signature).toString("hex")
2118
+ signature: bytesToHex(signature)
1696
2119
  })
1697
2120
  });
1698
2121
  } catch (err) {
@@ -1734,15 +2157,19 @@ var MppSessionClient = class {
1734
2157
  vouchersIssued
1735
2158
  };
1736
2159
  },
1737
- async requestRefund() {
2160
+ /**
2161
+ * Request refund from the channel contract (initiates dispute).
2162
+ * Returns the transaction hash only after on-chain confirmation (SUCCESS).
2163
+ */
2164
+ async requestRefund(options2) {
1738
2165
  const { rpc: rpcMod, Contract, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
1739
2166
  const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
1740
2167
  const server = new rpcMod.Server(rpcUrl);
1741
- const contract = new Contract(channelFactory);
2168
+ const contract2 = new Contract(channelFactory);
1742
2169
  const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
1743
2170
  try {
1744
2171
  const account = await server.getAccount(agentPublicKey);
1745
- const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract.call("request_refund")).setTimeout(30).build();
2172
+ const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract2.call("request_refund")).setTimeout(30).build();
1746
2173
  const simResult = await server.simulateTransaction(tx);
1747
2174
  if (rpcMod.Api.isSimulationError(simResult)) {
1748
2175
  throw new RouteDockDisputeError(`request_refund simulation failed: ${simResult.error}`);
@@ -1754,37 +2181,50 @@ var MppSessionClient = class {
1754
2181
  const errDetail = result.errorResult ? JSON.stringify(result.errorResult) : "status ERROR";
1755
2182
  throw new RouteDockDisputeError(`Refund request transaction failed: ${errDetail}`);
1756
2183
  }
2184
+ if (result.status === "TRY_AGAIN_LATER") {
2185
+ throw new RouteDockDisputeError("Refund request transaction rejected with TRY_AGAIN_LATER: transaction not queued");
2186
+ }
1757
2187
  if (!result.hash) {
1758
2188
  throw new RouteDockDisputeError("Refund request transaction not sent");
1759
2189
  }
1760
- return result.hash;
2190
+ return await confirmTransaction(
2191
+ server,
2192
+ result.hash,
2193
+ "Refund request",
2194
+ options2?.timeoutMs,
2195
+ options2?.intervalMs
2196
+ );
1761
2197
  } catch (err) {
1762
2198
  if (err instanceof RouteDockDisputeError) throw err;
1763
2199
  throw new RouteDockDisputeError(`Failed to request refund: ${err instanceof Error ? err.message : String(err)}`);
1764
2200
  }
1765
2201
  },
1766
- async settleWithLatestVoucher() {
1767
- const { rpc: rpcMod, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
2202
+ /**
2203
+ * Server-side counter-mechanism to settle with latest voucher before refund window expires.
2204
+ * Returns the transaction hash only after on-chain confirmation (SUCCESS).
2205
+ */
2206
+ async settleWithLatestVoucher(options2) {
2207
+ const { rpc: rpcMod, Contract, nativeToScVal: nativeToScVal2, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
1768
2208
  const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
1769
2209
  const server = new rpcMod.Server(rpcUrl);
1770
- const contract = new Contract(channelFactory);
2210
+ const contract2 = new Contract(channelFactory);
1771
2211
  const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
1772
2212
  try {
1773
2213
  const account = await server.getAccount(agentPublicKey);
1774
- const simTx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract.call("prepare_commitment", nativeToScVal(currentCumulative, { type: "i128" }))).setTimeout(30).build();
2214
+ const simTx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract2.call("prepare_commitment", nativeToScVal2(currentCumulative, { type: "i128" }))).setTimeout(30).build();
1775
2215
  const simResult = await server.simulateTransaction(simTx);
1776
2216
  if (rpcMod.Api.isSimulationError(simResult)) {
1777
2217
  throw new RouteDockDisputeError(`prepare_commitment simulation failed: ${simResult.error}`);
1778
2218
  }
1779
2219
  const commitmentBytes = simResult.result?.retval?.bytes();
1780
2220
  if (!commitmentBytes) throw new RouteDockDisputeError("prepare_commitment returned no bytes");
1781
- const signature = commitmentKey.sign(Buffer.from(commitmentBytes));
2221
+ const signature = commitmentKey.sign(commitmentBytes);
1782
2222
  const settleAccount = await server.getAccount(agentPublicKey);
1783
2223
  const settleTx = new TransactionBuilder(settleAccount, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(
1784
- contract.call(
2224
+ contract2.call(
1785
2225
  "settle_with_signature",
1786
- nativeToScVal(currentCumulative, { type: "i128" }),
1787
- nativeToScVal(Buffer.from(signature))
2226
+ nativeToScVal2(currentCumulative, { type: "i128" }),
2227
+ nativeToScVal2(signature)
1788
2228
  )
1789
2229
  ).setTimeout(30).build();
1790
2230
  const preparedSettleTx = await server.prepareTransaction(settleTx);
@@ -1794,10 +2234,19 @@ var MppSessionClient = class {
1794
2234
  const errDetail = settleResult.errorResult ? JSON.stringify(settleResult.errorResult) : "status ERROR";
1795
2235
  throw new RouteDockDisputeError(`Settlement transaction failed: ${errDetail}`);
1796
2236
  }
2237
+ if (settleResult.status === "TRY_AGAIN_LATER") {
2238
+ throw new RouteDockDisputeError("Settlement transaction rejected with TRY_AGAIN_LATER: transaction not queued");
2239
+ }
1797
2240
  if (!settleResult.hash) {
1798
2241
  throw new RouteDockDisputeError("Settlement transaction not sent");
1799
2242
  }
1800
- return settleResult.hash;
2243
+ return await confirmTransaction(
2244
+ server,
2245
+ settleResult.hash,
2246
+ "Settlement",
2247
+ options2?.timeoutMs,
2248
+ options2?.intervalMs
2249
+ );
1801
2250
  } catch (err) {
1802
2251
  if (err instanceof RouteDockDisputeError) throw err;
1803
2252
  throw new RouteDockDisputeError(`Failed to settle with latest voucher: ${err instanceof Error ? err.message : String(err)}`);
@@ -1807,11 +2256,11 @@ var MppSessionClient = class {
1807
2256
  const { rpc: rpcMod, Contract, TransactionBuilder, BASE_FEE } = await import('@stellar/stellar-sdk');
1808
2257
  const rpcUrl = network === "testnet" ? "https://soroban-testnet.stellar.org" : "https://soroban.stellar.org";
1809
2258
  const server = new rpcMod.Server(rpcUrl);
1810
- const contract = new Contract(channelFactory);
2259
+ const contract2 = new Contract(channelFactory);
1811
2260
  const passphrase = network === "mainnet" ? stellarSdk.Networks.PUBLIC : stellarSdk.Networks.TESTNET;
1812
2261
  try {
1813
2262
  const account = await server.getAccount(agentPublicKey);
1814
- const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract.call("get_channel_state")).setTimeout(30).build();
2263
+ const tx = new TransactionBuilder(account, { fee: BASE_FEE, networkPassphrase: passphrase }).addOperation(contract2.call("get_channel_state")).setTimeout(30).build();
1815
2264
  const simResult = await server.simulateTransaction(tx);
1816
2265
  if (rpcMod.Api.isSimulationError(simResult)) {
1817
2266
  throw new RouteDockChannelStateError(`Failed to query channel state: ${simResult.error}`);
@@ -1877,11 +2326,11 @@ var MppSessionClient = class {
1877
2326
  * close code (1000/1001); abnormal closes and connection failures surface as
1878
2327
  * RouteDockChannelStateError.
1879
2328
  */
1880
- async *streamWebSocket(url, mppx$1, createCredential, onSigned) {
1881
- const request = { method: "GET" };
2329
+ async *streamWebSocket(url, mppx$1, createCredential, onSigned, request) {
2330
+ const probeInit = toRequestInit(request);
1882
2331
  let probe;
1883
2332
  try {
1884
- probe = await mppx$1.rawFetch(url, request);
2333
+ probe = await mppx$1.rawFetch(url, probeInit);
1885
2334
  } catch (err) {
1886
2335
  throw wrapFetchError(err, "Voucher request");
1887
2336
  }
@@ -1985,235 +2434,19 @@ var MppSessionClient = class {
1985
2434
  }
1986
2435
  }
1987
2436
  };
1988
- var NulthPolicyError = class extends Error {
1989
- code;
1990
- constructor(code, message) {
1991
- super(message);
1992
- this.name = "NulthPolicyError";
1993
- this.code = code;
1994
- }
1995
- };
1996
- function sha256Hex(data) {
1997
- const buf = typeof data === "string" ? Buffer.from(data, "utf8") : data;
1998
- return crypto.createHash("sha256").update(buf).digest("hex");
1999
- }
2000
- function commitDailyCap(dailyCapStroops, witnessSecret) {
2001
- return sha256Hex(`${witnessSecret}:cap:${dailyCapStroops.toString()}`);
2002
- }
2003
- function commitAllowlist(payees, witnessSecret) {
2004
- const sorted = [...payees].sort().join(",");
2005
- return sha256Hex(`${witnessSecret}:allowlist:${sorted}`);
2006
- }
2007
- function hashPayee(payee) {
2008
- return sha256Hex(`payee:${payee}`);
2009
- }
2010
- function dayBucketFromLedger(ledgerSequence) {
2011
- return Math.floor(ledgerSequence / 17280);
2012
- }
2013
- function authDigestFromEntry(authEntryBase64) {
2014
- const entryHash = stellarSdk.hash(Buffer.from(authEntryBase64, "base64"));
2015
- return sha256Hex(entryHash);
2016
- }
2017
- function assertAuthEntryMatchesContext(authEntry, from, ctx) {
2018
- let preimage;
2437
+ function createNulthSigner(config) {
2438
+ const sharedConfig = { ...config, logger: resolveLogger(config.logger) };
2439
+ Object.defineProperty(sharedConfig, "paymentContext", {
2440
+ get: () => config.paymentContext
2441
+ });
2019
2442
  try {
2020
- preimage = stellarSdk.xdr.HashIdPreimage.fromXDR(authEntry, "base64");
2021
- } catch {
2022
- throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not decode as a HashIdPreimage");
2023
- }
2024
- if (preimage.switch() !== stellarSdk.xdr.EnvelopeType.envelopeTypeSorobanAuthorization()) {
2025
- throw new NulthPolicyError(
2026
- "auth_entry_mismatch",
2027
- "authEntry is not a Soroban authorization preimage"
2028
- );
2029
- }
2030
- const inv = preimage.sorobanAuthorization().invocation();
2031
- if (inv.subInvocations().length !== 0) {
2032
- throw new NulthPolicyError("auth_entry_mismatch", "authEntry has unexpected sub-invocations");
2033
- }
2034
- if (inv.function().switch() !== stellarSdk.xdr.SorobanAuthorizedFunctionType.sorobanAuthorizedFunctionTypeContractFn()) {
2035
- throw new NulthPolicyError("auth_entry_mismatch", "authEntry does not invoke a contract function");
2036
- }
2037
- const fn = inv.function().contractFn();
2038
- const contractAddress = stellarSdk.Address.fromScAddress(fn.contractAddress()).toString();
2039
- if (contractAddress !== ctx.assetContract) {
2040
- throw new NulthPolicyError(
2041
- "auth_entry_mismatch",
2042
- `authEntry contract ${contractAddress} does not match assetContract ${ctx.assetContract}`
2043
- );
2044
- }
2045
- const functionName = fn.functionName().toString();
2046
- if (functionName !== "transfer") {
2047
- throw new NulthPolicyError(
2048
- "auth_entry_mismatch",
2049
- `authEntry function ${functionName} does not match expected transfer`
2050
- );
2051
- }
2052
- const args = fn.args();
2053
- if (args.length !== 3) {
2054
- throw new NulthPolicyError("auth_entry_mismatch", `authEntry transfer has ${args.length} args, expected 3`);
2055
- }
2056
- const entryFrom = stellarSdk.scValToNative(args[0]);
2057
- if (entryFrom !== from) {
2058
- throw new NulthPolicyError(
2059
- "auth_entry_mismatch",
2060
- `authEntry from ${entryFrom} does not match nulthAccount ${from}`
2061
- );
2062
- }
2063
- const entryTo = stellarSdk.scValToNative(args[1]);
2064
- if (entryTo !== ctx.payee) {
2065
- throw new NulthPolicyError(
2066
- "auth_entry_mismatch",
2067
- `authEntry to ${entryTo} does not match paymentContext.payee ${ctx.payee}`
2068
- );
2069
- }
2070
- const entryAmount = stellarSdk.scValToNative(args[2]);
2071
- if (entryAmount !== ctx.amountStroops) {
2072
- throw new NulthPolicyError(
2073
- "auth_entry_mismatch",
2074
- `authEntry amount ${entryAmount} does not match paymentContext.amountStroops ${ctx.amountStroops}`
2075
- );
2076
- }
2077
- }
2078
- function insecureMockProof(preimage) {
2079
- return crypto.createHash("sha512").update(`nulth:${preimage}`).digest("base64");
2080
- }
2081
- function encodeAuthSignature(proof) {
2082
- const payload = { nulth: "zk-v1", proof };
2083
- return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
2084
- }
2085
- function usdcToStroops2(amount) {
2086
- const [whole = "0", frac = ""] = amount.split(".");
2087
- const padded = (frac + "0000000").slice(0, 7);
2088
- return BigInt(whole) * 10000000n + BigInt(padded);
2089
- }
2090
- var NulthClient = class {
2091
- constructor(config) {
2092
- this.config = config;
2093
- this.policy = { ...config.policy };
2094
- this.prover = config.prover ?? "mock";
2095
- if (this.prover !== "mock") {
2096
- throw new RouteDockManifestError(
2097
- `NulthClient: unknown prover backend ${String(config.prover)}; only 'mock' is implemented`
2098
- );
2099
- }
2100
- if (config.network !== "testnet") {
2101
- throw new RouteDockManifestError(
2102
- `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
2103
- );
2104
- }
2105
- resolveLogger(config.logger)(
2106
- "warn",
2107
- "NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
2108
- );
2109
- }
2110
- config;
2111
- policy;
2112
- prover;
2113
- get proverBackend() {
2114
- return this.prover;
2115
- }
2116
- get nulthAccount() {
2117
- return this.config.nulthAccount;
2118
- }
2119
- get allowlistCommitment() {
2120
- return this.policy.allowlistCommitment;
2121
- }
2122
- get capCommitment() {
2123
- return commitDailyCap(this.policy.dailyCapStroops, this.policy.witnessSecret);
2124
- }
2125
- buildPaymentAuthProof(context) {
2126
- this.enforcePolicy(context);
2127
- const bucket = dayBucketFromLedger(context.ledgerSequence);
2128
- if (bucket !== this.policy.dayBucket) {
2129
- this.policy.dayBucket = bucket;
2130
- this.policy.dailySpendStroops = 0n;
2131
- }
2132
- const authDigest = authDigestFromEntry(context.authEntry);
2133
- const capCommitment = this.capCommitment;
2134
- const payeeHash = hashPayee(context.payee);
2135
- const preimage = [
2136
- authDigest,
2137
- this.policy.allowlistCommitment,
2138
- capCommitment,
2139
- payeeHash,
2140
- context.amountStroops.toString(),
2141
- bucket.toString(),
2142
- this.config.nulthAccount
2143
- ].join("|");
2144
- const proof = {
2145
- version: 1,
2146
- proof: insecureMockProof(preimage),
2147
- publicInputs: {
2148
- authDigest,
2149
- allowlistCommitment: this.policy.allowlistCommitment,
2150
- capCommitment,
2151
- payeeHash,
2152
- amountStroops: context.amountStroops.toString(),
2153
- dayBucket: bucket.toString()
2154
- }
2155
- };
2156
- this.policy.dailySpendStroops += context.amountStroops;
2157
- return proof;
2158
- }
2159
- enforcePolicy(context) {
2160
- if (context.amountStroops < 0n) {
2161
- throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
2162
- }
2163
- if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
2164
- throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
2165
- }
2166
- if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
2167
- throw new NulthPolicyError("session_expired", "Nulth session expired");
2168
- }
2169
- if (!this.policy.allowedPayees.includes(context.payee)) {
2170
- throw new NulthPolicyError("payee_not_allowed", `Payee ${context.payee} not in off-chain allowlist`);
2171
- }
2172
- const bucket = dayBucketFromLedger(context.ledgerSequence);
2173
- const spend = bucket === this.policy.dayBucket ? this.policy.dailySpendStroops : 0n;
2174
- if (spend + context.amountStroops > this.policy.dailyCapStroops) {
2175
- throw new NulthPolicyError("daily_cap_exceeded", "Nulth daily cap exceeded");
2443
+ return nulthSdk.createNulthSigner(sharedConfig);
2444
+ } catch (error) {
2445
+ if (error instanceof nulthSdk.NulthConfigError) {
2446
+ throw new RouteDockManifestError(error.message, { cause: error });
2176
2447
  }
2448
+ throw error;
2177
2449
  }
2178
- };
2179
- function createNulthSigner(config) {
2180
- const client = new NulthClient(config);
2181
- const nulthAccount = config.nulthAccount;
2182
- return {
2183
- address: nulthAccount,
2184
- signAuthEntry: async (authEntry) => {
2185
- const ctx = config.paymentContext;
2186
- if (!ctx) throw new Error("Nulth paymentContext must be set before signing");
2187
- assertAuthEntryMatchesContext(authEntry, nulthAccount, ctx);
2188
- const proof = client.buildPaymentAuthProof({ authEntry, ...ctx });
2189
- return { signedAuthEntry: encodeAuthSignature(proof), signerAddress: nulthAccount };
2190
- }
2191
- };
2192
- }
2193
- function setNulthPaymentContext(config, context) {
2194
- config.paymentContext = context;
2195
- }
2196
- function paymentContextFromManifest(manifest, mode, ledgerSequence) {
2197
- const pricing = manifest.pricing[mode];
2198
- if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
2199
- return {
2200
- payee: manifest.payee,
2201
- amountStroops: usdcToStroops2(pricing.amount),
2202
- assetContract: manifest.asset_contract,
2203
- ledgerSequence
2204
- };
2205
- }
2206
- function createPolicyState(input) {
2207
- const dailyCapStroops = usdcToStroops2(input.dailyCapUsdc);
2208
- return {
2209
- dailyCapStroops,
2210
- allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
2211
- allowedPayees: input.allowedPayees,
2212
- dailySpendStroops: 0n,
2213
- dayBucket: dayBucketFromLedger(input.ledgerSequence ?? 0),
2214
- witnessSecret: input.witnessSecret,
2215
- ...input.expiryLedger !== void 0 ? { expiryLedger: input.expiryLedger } : {}
2216
- };
2217
2450
  }
2218
2451
  function assertNulthVaultManifest(manifest, nulthAccount) {
2219
2452
  if (manifest.vault !== void 0 && manifest.vault !== "nulth") {
@@ -2238,7 +2471,7 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
2238
2471
  );
2239
2472
  }
2240
2473
  const ledgerSequence = await fetchLedgerSequence(network);
2241
- const policy = createPolicyState({
2474
+ const policy = nulthSdk.createPolicyState({
2242
2475
  dailyCapUsdc: vault.dailyCapUsdc,
2243
2476
  allowedPayees: vault.allowedPayees,
2244
2477
  witnessSecret: vault.witnessSecret,
@@ -2253,7 +2486,7 @@ async function prepareNulthSigner(vault, manifest, mode, network, ledgerSequence
2253
2486
  ...vault.verifierContract !== void 0 ? { verifierContract: vault.verifierContract } : {}
2254
2487
  };
2255
2488
  if (mode === "x402" || mode === "mpp-charge") {
2256
- setNulthPaymentContext(config, paymentContextFromManifest(manifest, mode, ledgerSequence));
2489
+ nulthSdk.setNulthPaymentContext(config, nulthSdk.paymentContextFromManifest(manifest, mode, ledgerSequence));
2257
2490
  } else {
2258
2491
  throw new RouteDockManifestError(`Nulth ZK vault does not support payment mode: ${mode}`);
2259
2492
  }
@@ -2438,7 +2671,7 @@ var RouteDockClient = class _RouteDockClient {
2438
2671
  }
2439
2672
  const secretKey = this.keypair.secret();
2440
2673
  const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
2441
- this.x402 = new X402Client(secretKey, this.network, retryPolicy);
2674
+ this.x402 = new X402Client(secretKey, this.network, retryPolicy, config.preSignHook, this.logger);
2442
2675
  this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
2443
2676
  this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
2444
2677
  }
@@ -2446,7 +2679,7 @@ var RouteDockClient = class _RouteDockClient {
2446
2679
  async _resolveManifest(url, options) {
2447
2680
  const baseUrl = new URL(url).origin;
2448
2681
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2449
- assertEndpointActive(manifest, url, this.logger);
2682
+ assertEndpointActive(manifest, url, this.logger, options?.request?.method);
2450
2683
  const mode = selectMode(manifest, options);
2451
2684
  return { manifest, mode };
2452
2685
  }
@@ -2463,13 +2696,9 @@ var RouteDockClient = class _RouteDockClient {
2463
2696
  });
2464
2697
  return next;
2465
2698
  }
2466
- /**
2467
- * Check that the payer has a trustline for the payment asset and surface
2468
- * manifest metadata (regions, latency hints, capabilities). Safe to
2469
- * call before committing to a payment — for approval gates and manual
2470
- * trustline remediation.
2471
- */
2472
- async preflight(manifest, mode, endpoint) {
2699
+ async preflight(target, mode, endpoint) {
2700
+ if (typeof target === "string") return this._preflightUrl(target);
2701
+ const manifest = target;
2473
2702
  assertManifestValid(manifest);
2474
2703
  const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
2475
2704
  await this._checkTrustline(manifest, selectedAsset);
@@ -2482,6 +2711,42 @@ var RouteDockClient = class _RouteDockClient {
2482
2711
  ...manifest.capabilities && { capabilities: manifest.capabilities }
2483
2712
  };
2484
2713
  }
2714
+ /** Manifest-only preflight for a URL — no trustline check, no payment. */
2715
+ async _preflightUrl(url) {
2716
+ const baseUrl = new URL(url).origin;
2717
+ const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2718
+ assertEndpointActive(manifest, url);
2719
+ const costs = manifest.modes.map((mode) => {
2720
+ const asset = selectAsset(manifest, mode, url).asset;
2721
+ switch (mode) {
2722
+ case "x402":
2723
+ case "mpp-charge": {
2724
+ const pricing = manifest.pricing[mode];
2725
+ return { mode, amount: pricing.amount, per: "request", asset };
2726
+ }
2727
+ case "mpp-session":
2728
+ case "mpp-session-ws": {
2729
+ const pricing = manifest.pricing[mode];
2730
+ return { mode, amount: pricing.rate, per: "voucher", asset, min_deposit: pricing.min_deposit };
2731
+ }
2732
+ default:
2733
+ throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2734
+ }
2735
+ });
2736
+ const pathname = new URL(url).pathname;
2737
+ const matches = Object.values(manifest.endpoints).filter((descriptor) => descriptor.path === pathname);
2738
+ const endpointSunsets = matches.map((descriptor) => descriptor.sunset_at).filter((value) => typeof value === "string").sort((a, b) => Date.parse(a) - Date.parse(b));
2739
+ return {
2740
+ modes: manifest.modes,
2741
+ costs,
2742
+ compliance: {
2743
+ deprecated_modes: manifest.deprecated_modes ?? [],
2744
+ ...manifest.sunset_at && { sunset_at: manifest.sunset_at },
2745
+ endpoint_deprecated: matches.some((descriptor) => descriptor.deprecated === true),
2746
+ ...endpointSunsets[0] && { endpoint_sunset_at: endpointSunsets[0] }
2747
+ }
2748
+ };
2749
+ }
2485
2750
  /**
2486
2751
  * Verify via Horizon that the payer account has a trustline for the
2487
2752
  * payment asset declared in the manifest. Throws RouteDockTrustlineError
@@ -2493,8 +2758,7 @@ var RouteDockClient = class _RouteDockClient {
2493
2758
  const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
2494
2759
  const cached = _RouteDockClient._trustlineCache.get(cacheKey);
2495
2760
  if (cached && Date.now() < cached.expiresAt) return;
2496
- const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
2497
- const server = new stellarSdk.Horizon.Server(horizonUrl);
2761
+ const server = new stellarSdk.Horizon.Server(HORIZON_URLS[this.network]);
2498
2762
  try {
2499
2763
  const account = await server.loadAccount(this.keypair.publicKey());
2500
2764
  const balances = account.balances;
@@ -2530,9 +2794,11 @@ var RouteDockClient = class _RouteDockClient {
2530
2794
  async pay(url, options) {
2531
2795
  const baseUrl = new URL(url).origin;
2532
2796
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2533
- assertEndpointActive(manifest, url, this.logger);
2797
+ const request = options?.request;
2798
+ assertEndpointActive(manifest, url, this.logger, request?.method);
2534
2799
  const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
2535
2800
  const selectedAsset = selectAsset(manifest, mode, url).asset;
2801
+ this._assertCapAsset(selectedAsset);
2536
2802
  await this._checkTrustline(manifest, selectedAsset);
2537
2803
  let amount;
2538
2804
  switch (mode) {
@@ -2550,18 +2816,18 @@ var RouteDockClient = class _RouteDockClient {
2550
2816
  default:
2551
2817
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2552
2818
  }
2553
- const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
2819
+ const reserveId = await this._checkAndReserveSpend(amount, baseUrl, selectedAsset);
2554
2820
  let result;
2555
2821
  try {
2556
2822
  if (this.vault?.mode === "nulth") {
2557
- result = await this._payWithNulthVault(url, manifest, mode);
2823
+ result = await this._payWithNulthVault(url, manifest, mode, request);
2558
2824
  } else {
2559
2825
  switch (mode) {
2560
2826
  case "x402":
2561
- result = await this.x402.pay(url, manifest);
2827
+ result = await this.x402.pay(url, manifest, request);
2562
2828
  break;
2563
2829
  case "mpp-charge":
2564
- result = await this.charge.pay(url, manifest);
2830
+ result = await this.charge.pay(url, manifest, request);
2565
2831
  break;
2566
2832
  default:
2567
2833
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
@@ -2580,7 +2846,7 @@ var RouteDockClient = class _RouteDockClient {
2580
2846
  return result;
2581
2847
  }
2582
2848
  /** Nulth ZK vault path — proof built off-chain, attached as auth signature */
2583
- async _payWithNulthVault(url, manifest, mode) {
2849
+ async _payWithNulthVault(url, manifest, mode, request) {
2584
2850
  const prover = this.vault?.prover ?? "mock";
2585
2851
  if (this.network === "mainnet" && prover === "mock") {
2586
2852
  throw new RouteDockManifestError("nulth vault uses a MOCK Groth16 prover and cannot be used on mainnet");
@@ -2593,10 +2859,10 @@ var RouteDockClient = class _RouteDockClient {
2593
2859
  try {
2594
2860
  const { signer } = await prepareNulthSigner(this.vault, manifest, mode, this.network);
2595
2861
  const x402 = this.x402.withSigner(signer);
2596
- const result = await x402.pay(url, manifest);
2862
+ const result = await x402.pay(url, manifest, request);
2597
2863
  return result;
2598
2864
  } catch (err) {
2599
- if (err instanceof NulthPolicyError) {
2865
+ if (err instanceof nulthSdk.NulthPolicyError) {
2600
2866
  throw new RouteDockPolicyRejectError(err.code);
2601
2867
  }
2602
2868
  throw err;
@@ -2662,17 +2928,35 @@ var RouteDockClient = class _RouteDockClient {
2662
2928
  );
2663
2929
  }
2664
2930
  const endpointKey = new URL(url).origin;
2665
- const onSpend = this.spendCap ? (amount) => this._recordSessionSpend(amount, endpointKey) : void 0;
2931
+ let onSpend;
2932
+ if (this.spendCap) {
2933
+ const sessionAsset = selectAsset(manifest, mode, url).asset;
2934
+ this._assertCapAsset(sessionAsset);
2935
+ onSpend = (amount) => this._recordSessionSpend(amount, endpointKey, sessionAsset);
2936
+ }
2666
2937
  return this.session.openSession(url, manifest, secret, options, onSpend);
2667
2938
  }
2939
+ /**
2940
+ * The local cap accumulates one USDC total. Refuse to count any other asset
2941
+ * against it (or let it through uncounted) when a cap is configured.
2942
+ */
2943
+ _assertCapAsset(asset) {
2944
+ if (this.spendCap && asset !== this.spendCap.asset) {
2945
+ throw new RouteDockPolicyRejectError("local_cap_asset_mismatch", {
2946
+ cause: new Error(
2947
+ `spendCap is denominated in ${this.spendCap.asset} but the selected payment asset is ${asset}`
2948
+ )
2949
+ });
2950
+ }
2951
+ }
2668
2952
  /**
2669
2953
  * Record spend for an MPP session voucher. Called before each voucher
2670
2954
  * fetch in stream(). Enforces the same local daily and per-endpoint caps
2671
2955
  * as discrete pay() calls. Returns a commit function to clean up the
2672
2956
  * reservation after the voucher fetch succeeds.
2673
2957
  */
2674
- async _recordSessionSpend(amount, endpointKey) {
2675
- const reserveId = await this._checkAndReserveSpend(amount, endpointKey);
2958
+ async _recordSessionSpend(amount, endpointKey, asset) {
2959
+ const reserveId = await this._checkAndReserveSpend(amount, endpointKey, asset);
2676
2960
  if (reserveId) {
2677
2961
  await this._commitSpend(reserveId);
2678
2962
  }
@@ -2711,15 +2995,16 @@ var RouteDockClient = class _RouteDockClient {
2711
2995
  * Both limits are independent: a payment is only reserved after **both** pass.
2712
2996
  * All spend (regardless of endpoint) counts toward the global accumulator.
2713
2997
  */
2714
- async _checkAndReserveSpend(amount, endpointKey) {
2998
+ async _checkAndReserveSpend(amount, endpointKey, asset) {
2715
2999
  if (!this.spendCap) return "";
2716
- return this._withSpendMutex(async () => {
3000
+ this._assertCapAsset(asset);
3001
+ const spendCap = this.spendCap;
3002
+ const amountMicros = nulthSdk.usdcToStroops(amount);
3003
+ const reserve = (prev) => {
2717
3004
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2718
- const persisted = await this.spendStore.read();
2719
- const current = persisted && persisted.date === today ? persisted : { date: today, totalMicros: "0", endpoints: {} };
2720
- const amountMicros = nulthSdk.usdcToStroops(amount);
3005
+ const current = prev && prev.date === today ? { ...prev, endpoints: { ...prev.endpoints } } : { date: today, totalMicros: "0", endpoints: {} };
2721
3006
  const total = BigInt(current.totalMicros);
2722
- const endpointCapStr = this.spendCap.endpointCaps?.[endpointKey];
3007
+ const endpointCapStr = spendCap.endpointCaps?.[endpointKey];
2723
3008
  if (endpointCapStr !== void 0) {
2724
3009
  const endpointCapMicros = nulthSdk.usdcToStroops(endpointCapStr);
2725
3010
  const endpointTotal = BigInt(current.endpoints[endpointKey] ?? "0");
@@ -2727,17 +3012,25 @@ var RouteDockClient = class _RouteDockClient {
2727
3012
  throw new RouteDockPolicyRejectError("local_endpoint_cap_exceeded");
2728
3013
  }
2729
3014
  }
2730
- const globalCapMicros = nulthSdk.usdcToStroops(this.spendCap.daily);
3015
+ const globalCapMicros = nulthSdk.usdcToStroops(spendCap.daily);
2731
3016
  if (total + amountMicros > globalCapMicros) {
2732
3017
  throw new RouteDockPolicyRejectError("local_daily_cap_exceeded");
2733
3018
  }
2734
- const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
2735
- this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
2736
3019
  current.totalMicros = (total + amountMicros).toString();
2737
3020
  if (endpointCapStr !== void 0) {
2738
3021
  current.endpoints[endpointKey] = (BigInt(current.endpoints[endpointKey] ?? "0") + amountMicros).toString();
2739
3022
  }
2740
- await this.spendStore.write(current);
3023
+ return current;
3024
+ };
3025
+ return this._withSpendMutex(async () => {
3026
+ const store = this.spendStore;
3027
+ if (store.update) {
3028
+ await store.update(reserve);
3029
+ } else {
3030
+ await store.write(reserve(await store.read()));
3031
+ }
3032
+ const reserveId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3033
+ this._pendingReserves.set(reserveId, { endpointKey, amountMicros });
2741
3034
  return reserveId;
2742
3035
  });
2743
3036
  }
@@ -2759,22 +3052,29 @@ var RouteDockClient = class _RouteDockClient {
2759
3052
  if (!reserve) return;
2760
3053
  this._pendingReserves.delete(reserveId);
2761
3054
  const { endpointKey, amountMicros } = reserve;
2762
- await this._withSpendMutex(async () => {
3055
+ const release = (prev) => {
2763
3056
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2764
- const persisted = await this.spendStore.read();
2765
- if (!persisted || persisted.date !== today) return;
2766
- const currentTotal = BigInt(persisted.totalMicros);
2767
- persisted.totalMicros = (currentTotal - amountMicros).toString();
3057
+ if (!prev || prev.date !== today) return void 0;
3058
+ const persisted = { ...prev, endpoints: { ...prev.endpoints } };
3059
+ persisted.totalMicros = (BigInt(persisted.totalMicros) - amountMicros).toString();
2768
3060
  if (persisted.endpoints[endpointKey]) {
2769
- const endpointTotal = BigInt(persisted.endpoints[endpointKey]);
2770
- const newEndpointTotal = endpointTotal - amountMicros;
3061
+ const newEndpointTotal = BigInt(persisted.endpoints[endpointKey]) - amountMicros;
2771
3062
  if (newEndpointTotal <= 0n) {
2772
3063
  delete persisted.endpoints[endpointKey];
2773
3064
  } else {
2774
3065
  persisted.endpoints[endpointKey] = newEndpointTotal.toString();
2775
3066
  }
2776
3067
  }
2777
- await this.spendStore.write(persisted);
3068
+ return persisted;
3069
+ };
3070
+ await this._withSpendMutex(async () => {
3071
+ const store = this.spendStore;
3072
+ if (store.update) {
3073
+ await store.update(release);
3074
+ } else {
3075
+ const next = release(await store.read());
3076
+ if (next) await store.write(next);
3077
+ }
2778
3078
  });
2779
3079
  }
2780
3080
  };