@routedock/routedock 0.2.0 → 0.3.1

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 (50) hide show
  1. package/README.md +305 -0
  2. package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
  3. package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
  4. package/dist/{ProviderRegistry-BJ9oJLVz.d.cts → ProviderRegistry-CyZ5eryO.d.cts} +2 -2
  5. package/dist/{ProviderRegistry-C50nyhaF.d.ts → ProviderRegistry-w4_hkI_1.d.ts} +2 -2
  6. package/dist/{RouteDockClient-BbkqjhMG.d.ts → RouteDockClient-CXvWXix5.d.cts} +25 -9
  7. package/dist/{RouteDockClient-B1roCa8h.d.cts → RouteDockClient-CjM2VyOd.d.ts} +25 -9
  8. package/dist/{SessionReconciler-Dscw43F3.d.ts → SessionReconciler-Bw-PoBVl.d.ts} +14 -5
  9. package/dist/{SessionReconciler-bi0z2qtN.d.cts → SessionReconciler-Cz2nuOlJ.d.cts} +14 -5
  10. package/dist/{SessionStore-DvkZv8-E.d.ts → SessionStore-B4-Hi4H8.d.cts} +34 -2
  11. package/dist/{SessionStore-BpK8vv0L.d.cts → SessionStore-BaYiegP0.d.ts} +34 -2
  12. package/dist/client.cjs +432 -125
  13. package/dist/client.d.cts +9 -8
  14. package/dist/client.d.ts +9 -8
  15. package/dist/client.js +424 -127
  16. package/dist/index.cjs +655 -228
  17. package/dist/index.d.cts +8 -8
  18. package/dist/index.d.ts +8 -8
  19. package/dist/index.js +646 -229
  20. package/dist/{types-DmhmfLzP.d.cts → logger-D8_Q2ytp.d.cts} +58 -1
  21. package/dist/{types-DmhmfLzP.d.ts → logger-D8_Q2ytp.d.ts} +58 -1
  22. package/dist/provider/fastify.cjs +306 -89
  23. package/dist/provider/fastify.d.cts +9 -4
  24. package/dist/provider/fastify.d.ts +9 -4
  25. package/dist/provider/fastify.js +305 -88
  26. package/dist/provider/hono.cjs +286 -82
  27. package/dist/provider/hono.d.cts +9 -7
  28. package/dist/provider/hono.d.ts +9 -7
  29. package/dist/provider/hono.js +282 -78
  30. package/dist/provider.cjs +326 -98
  31. package/dist/provider.d.cts +14 -10
  32. package/dist/provider.d.ts +14 -10
  33. package/dist/provider.js +318 -98
  34. package/dist/react.cjs +410 -125
  35. package/dist/react.d.cts +2 -2
  36. package/dist/react.d.ts +2 -2
  37. package/dist/react.js +411 -126
  38. package/dist/{routedockMiddleware-BlDsHuPl.d.ts → routedockMiddleware-57gUaEMf.d.ts} +9 -4
  39. package/dist/{routedockMiddleware-D97A5ptQ.d.cts → routedockMiddleware-DlyLrxp7.d.cts} +9 -4
  40. package/dist/schema.cjs +40 -0
  41. package/dist/schema.js +40 -0
  42. package/dist/{sign-Dpj2Mp7k.d.ts → sign-AKH67sft.d.cts} +1 -1
  43. package/dist/{sign-CUf3rhTx.d.cts → sign-BEq-OsSq.d.ts} +1 -1
  44. package/dist/testing.cjs +6 -4
  45. package/dist/testing.d.cts +32 -13
  46. package/dist/testing.d.ts +32 -13
  47. package/dist/testing.js +6 -4
  48. package/package.json +2 -2
  49. package/dist/MppSessionHandler-B4TyD7Xg.d.cts +0 -121
  50. package/dist/MppSessionHandler-CLB5SzgN.d.ts +0 -121
package/dist/react.cjs CHANGED
@@ -163,6 +163,31 @@ function wrapFetchError(err, context) {
163
163
  if (err instanceof RouteDockError) return err;
164
164
  return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
165
165
  }
166
+ function wrapMppError(err, context) {
167
+ if (err instanceof RouteDockError) return err;
168
+ if (err instanceof TypeError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
169
+ return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
170
+ }
171
+ return new RouteDockSignatureError(`${context}: ${String(err)}`, { cause: err });
172
+ }
173
+
174
+ // src/internal/logger.ts
175
+ function createConsoleLogger() {
176
+ return (level, message, fields) => {
177
+ const values = fields ? Object.values(fields) : [];
178
+ if (values.length > 0) {
179
+ console[level](message, ...values);
180
+ } else {
181
+ console[level](message);
182
+ }
183
+ };
184
+ }
185
+ var consoleLogger = createConsoleLogger();
186
+ var noopLogger = () => {
187
+ };
188
+ function resolveLogger(logger, fallback = consoleLogger) {
189
+ return logger ?? fallback;
190
+ }
166
191
 
167
192
  // src/internal/retry.ts
168
193
  var DEFAULT_RETRY_POLICY = {
@@ -178,10 +203,11 @@ function sleep(ms) {
178
203
  return new Promise((resolve) => setTimeout(resolve, ms));
179
204
  }
180
205
  async function withRetry(fn, policy = {}) {
181
- const { maxAttempts, baseDelayMs, maxDelayMs, onRetry } = {
206
+ const { maxAttempts, baseDelayMs, maxDelayMs, onRetry, logger } = {
182
207
  ...DEFAULT_RETRY_POLICY,
183
208
  ...policy
184
209
  };
210
+ const log = resolveLogger(logger);
185
211
  let lastError;
186
212
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
187
213
  try {
@@ -199,7 +225,7 @@ async function withRetry(fn, policy = {}) {
199
225
  try {
200
226
  await onRetry(attempt, err, delay);
201
227
  } catch (cbErr) {
202
- console.error("[withRetry] onRetry callback threw:", cbErr);
228
+ log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
203
229
  }
204
230
  }
205
231
  await sleep(delay);
@@ -291,6 +317,14 @@ var routedock_schema_default = {
291
317
  type: "string",
292
318
  description: "Stellar Asset Contract (SAC) address for the payment asset"
293
319
  },
320
+ assets: {
321
+ type: "array",
322
+ minItems: 1,
323
+ items: {
324
+ $ref: "#/definitions/AssetConfig"
325
+ },
326
+ description: "Payment assets accepted by this provider. Each asset can be scoped to specific modes and endpoints. When present, assets[0] must match the root-level asset and asset_contract."
327
+ },
294
328
  payee: {
295
329
  type: "string",
296
330
  description: "Stellar address (G...) that receives payments"
@@ -423,6 +457,38 @@ var routedock_schema_default = {
423
457
  enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
424
458
  description: "A supported RouteDock payment mode"
425
459
  },
460
+ AssetConfig: {
461
+ type: "object",
462
+ description: "Payment asset configuration with optional mode and endpoint scoping",
463
+ required: ["asset", "asset_contract"],
464
+ additionalProperties: false,
465
+ properties: {
466
+ asset: {
467
+ type: "string",
468
+ description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
469
+ },
470
+ asset_contract: {
471
+ type: "string",
472
+ description: "Stellar Asset Contract (SAC) address for the payment asset"
473
+ },
474
+ modes: {
475
+ type: "array",
476
+ items: {
477
+ $ref: "#/definitions/PaymentMode"
478
+ },
479
+ uniqueItems: true,
480
+ description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
481
+ },
482
+ endpoints: {
483
+ type: "array",
484
+ items: {
485
+ type: "string"
486
+ },
487
+ uniqueItems: true,
488
+ description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
489
+ }
490
+ }
491
+ },
426
492
  PricingConfig: {
427
493
  type: "object",
428
494
  description: "Per-request pricing for x402 or mpp-charge modes",
@@ -568,7 +634,7 @@ var routedock_schema_default = {
568
634
 
569
635
  // package.json
570
636
  var package_default = {
571
- version: "0.2.0"};
637
+ version: "0.3.1"};
572
638
  var MANIFEST_SIGNATURE_VERSION = "2";
573
639
  function canonicalize(value) {
574
640
  if (Array.isArray(value)) {
@@ -682,6 +748,7 @@ function assertEndpointActive(manifest, url, logger) {
682
748
  const [key, descriptor] = deprecated;
683
749
  const sunsetSuffix = descriptor.sunset_at ? `; sunset_at ${descriptor.sunset_at}` : "";
684
750
  logger(
751
+ "warn",
685
752
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${pathname}; endpoint '${key}' is deprecated${sunsetSuffix}`
686
753
  );
687
754
  }
@@ -703,6 +770,27 @@ function assertManifestValid(manifest, baseUrl) {
703
770
  }
704
771
  }
705
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
+ }
706
794
  }
707
795
  var CACHE_TTL_MS = 6e4;
708
796
  var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
@@ -876,16 +964,16 @@ function logSelection(manifest, selection, log, deprecated) {
876
964
  const reason = selection.reason ? ` (${selection.reason})` : "";
877
965
  if (deprecated) {
878
966
  log(
967
+ "warn",
879
968
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${selection.mode}${reason}; selected deprecated mode because no active supported mode is available`
880
969
  );
881
970
  return;
882
971
  }
883
- log(`[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
972
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
884
973
  }
885
974
  function selectMode(manifest, options = {}) {
886
975
  const modes = manifest.modes;
887
- const log = options.logger ?? (() => {
888
- });
976
+ const log = options.logger ?? noopLogger;
889
977
  const deprecatedSet = new Set(manifest.deprecated_modes ?? []);
890
978
  if (options.forceMode) {
891
979
  if (!modes.includes(options.forceMode)) {
@@ -895,10 +983,11 @@ function selectMode(manifest, options = {}) {
895
983
  }
896
984
  if (deprecatedSet.has(options.forceMode)) {
897
985
  log(
986
+ "warn",
898
987
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${options.forceMode} (forced); selected mode is deprecated`
899
988
  );
900
989
  } else {
901
- log(`[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
990
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
902
991
  }
903
992
  return options.forceMode;
904
993
  }
@@ -918,6 +1007,69 @@ function selectMode(manifest, options = {}) {
918
1007
  `No supported payment mode found in manifest (modes: ${modes.join(", ")})`
919
1008
  );
920
1009
  }
1010
+
1011
+ // src/internal/payee.ts
1012
+ function resolvePayee(manifest, mode) {
1013
+ const override = manifest.pricing[mode]?.payee;
1014
+ return override ?? manifest.payee;
1015
+ }
1016
+
1017
+ // src/client/challenge.ts
1018
+ function isRecord(value) {
1019
+ return typeof value === "object" && value !== null;
1020
+ }
1021
+ function isPositiveIntegerString(value) {
1022
+ return typeof value === "string" && /^[1-9]\d*$/.test(value);
1023
+ }
1024
+ function checkAmount(amount, maxAmount) {
1025
+ if (!isPositiveIntegerString(amount)) {
1026
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1027
+ }
1028
+ let max;
1029
+ try {
1030
+ max = nulthSdk.usdcToStroops(maxAmount);
1031
+ } catch {
1032
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1033
+ }
1034
+ if (BigInt(amount) > max) {
1035
+ return new RouteDockPolicyRejectError("challenge_amount_exceeds_manifest");
1036
+ }
1037
+ return null;
1038
+ }
1039
+ function checkX402Accept(accept, manifest, network, maxAmount) {
1040
+ if (accept.network !== network) {
1041
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1042
+ }
1043
+ if (accept.payTo !== resolvePayee(manifest, "x402")) {
1044
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1045
+ }
1046
+ if (accept.asset !== manifest.asset_contract) {
1047
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1048
+ }
1049
+ return checkAmount(accept.amount, maxAmount);
1050
+ }
1051
+ function filterX402Accepts(accepts, manifest, network, maxAmount) {
1052
+ return accepts.filter((accept) => checkX402Accept(accept, manifest, network, maxAmount) === null);
1053
+ }
1054
+ function checkChargeChallenge(request, manifest, network, maxAmount) {
1055
+ const methodDetails = request["methodDetails"];
1056
+ const rawNetwork = isRecord(methodDetails) ? methodDetails["network"] : void 0;
1057
+ const challengeNetwork = rawNetwork === void 0 ? "stellar:testnet" : rawNetwork;
1058
+ if (challengeNetwork !== network) {
1059
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1060
+ }
1061
+ const recipient = request["recipient"];
1062
+ if (typeof recipient !== "string" || recipient !== resolvePayee(manifest, "mpp-charge")) {
1063
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1064
+ }
1065
+ const currency = request["currency"];
1066
+ if (typeof currency !== "string" || currency !== manifest.asset_contract) {
1067
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1068
+ }
1069
+ return checkAmount(request["amount"], maxAmount);
1070
+ }
1071
+
1072
+ // src/client/x402Client.ts
921
1073
  var CAIP2 = {
922
1074
  testnet: "stellar:testnet",
923
1075
  mainnet: "stellar:pubnet"
@@ -949,83 +1101,106 @@ var X402Client = class _X402Client {
949
1101
  if (!pricing.facilitator) {
950
1102
  throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
951
1103
  }
952
- return withRetry(async () => {
953
- let init;
1104
+ const caip2 = CAIP2[this.network];
1105
+ const probe = await withRetry(async () => {
1106
+ let init2;
954
1107
  try {
955
- init = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
1108
+ init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
956
1109
  } catch (err) {
957
1110
  throw wrapFetchError(err, "x402 initial request");
958
1111
  }
959
- if (init.status !== 402) {
960
- if (!init.ok) {
1112
+ if (init2.status !== 402) {
1113
+ if (!init2.ok) {
961
1114
  throw httpStatusToError(
962
- `x402 initial request failed: HTTP ${init.status}`,
963
- init.status,
964
- init
1115
+ `x402 initial request failed: HTTP ${init2.status}`,
1116
+ init2.status,
1117
+ init2
965
1118
  );
966
1119
  }
967
1120
  let data2;
968
1121
  try {
969
- data2 = await init.json();
1122
+ data2 = await init2.json();
970
1123
  } catch {
971
- throw new RouteDockManifestError(`Failed to parse JSON from non-402 response (HTTP ${init.status})`);
1124
+ throw new RouteDockManifestError(
1125
+ `Failed to parse JSON from non-402 response (HTTP ${init2.status})`
1126
+ );
972
1127
  }
973
- return { data: data2, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
974
- }
975
- const reqHeader = init.headers.get("X-Payment-Requirements");
976
- if (!reqHeader) {
977
- throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1128
+ return { kind: "free", data: data2 };
978
1129
  }
979
- let paymentRequired;
980
- try {
981
- paymentRequired = http.decodePaymentRequiredHeader(reqHeader);
982
- } catch (err) {
983
- throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
984
- cause: err
985
- });
986
- }
987
- let paymentPayload;
988
- try {
989
- paymentPayload = await this.httpClient.createPaymentPayload(paymentRequired);
990
- } catch (err) {
991
- throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
992
- cause: err
993
- });
994
- }
995
- const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
996
- let settled;
1130
+ return { kind: "payment-required", init: init2 };
1131
+ }, this.retryPolicy);
1132
+ if (probe.kind === "free") {
1133
+ return { data: probe.data, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
1134
+ }
1135
+ const init = probe.init;
1136
+ const reqHeader = init.headers.get("X-Payment-Requirements");
1137
+ if (!reqHeader) {
1138
+ throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1139
+ }
1140
+ let paymentRequired;
1141
+ try {
1142
+ paymentRequired = http.decodePaymentRequiredHeader(reqHeader);
1143
+ } catch (err) {
1144
+ throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
1145
+ cause: err
1146
+ });
1147
+ }
1148
+ const firstAccept = paymentRequired.accepts[0];
1149
+ if (!firstAccept) {
1150
+ throw new RouteDockManifestError("402 response carries no payment accepts");
1151
+ }
1152
+ const allowed = filterX402Accepts(paymentRequired.accepts, manifest, caip2, pricing.amount);
1153
+ if (allowed.length === 0) {
1154
+ throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
1155
+ }
1156
+ let paymentPayload;
1157
+ try {
1158
+ paymentPayload = await this.httpClient.createPaymentPayload({
1159
+ ...paymentRequired,
1160
+ accepts: allowed
1161
+ });
1162
+ } catch (err) {
1163
+ throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
1164
+ cause: err
1165
+ });
1166
+ }
1167
+ const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1168
+ const signedAmount = nulthSdk.stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
1169
+ const settled = await withRetry(async () => {
1170
+ let response;
997
1171
  try {
998
- settled = await fetch(url, { headers: paymentHeaders });
1172
+ response = await fetch(url, { headers: paymentHeaders });
999
1173
  } catch (err) {
1000
1174
  throw wrapFetchError(err, "x402 settlement request");
1001
1175
  }
1002
- if (!settled.ok) {
1003
- if (settled.status >= 500 || settled.status === 429 || settled.status === 503) {
1176
+ if (!response.ok) {
1177
+ if (response.status >= 500 || response.status === 429 || response.status === 503) {
1004
1178
  throw httpStatusToError(
1005
- `x402 payment failed: HTTP ${settled.status}`,
1006
- settled.status,
1007
- settled
1179
+ `x402 payment failed: HTTP ${response.status}`,
1180
+ response.status,
1181
+ response
1008
1182
  );
1009
1183
  }
1010
- throw new RouteDockManifestError(`x402 payment failed: HTTP ${settled.status}`);
1011
- }
1012
- let txHash = null;
1013
- const responseHeader = settled.headers.get("X-Payment-Response");
1014
- if (responseHeader) {
1015
- try {
1016
- const settleResponse = http.decodePaymentResponseHeader(responseHeader);
1017
- txHash = settleResponse.transaction ?? null;
1018
- } catch {
1019
- }
1184
+ throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
1020
1185
  }
1021
- let data;
1186
+ return response;
1187
+ }, this.retryPolicy);
1188
+ let txHash = null;
1189
+ const responseHeader = settled.headers.get("X-Payment-Response");
1190
+ if (responseHeader) {
1022
1191
  try {
1023
- data = await settled.json();
1192
+ const settleResponse = http.decodePaymentResponseHeader(responseHeader);
1193
+ txHash = settleResponse.transaction ?? null;
1024
1194
  } catch {
1025
- throw new RouteDockManifestError("Failed to parse JSON from settled response");
1026
1195
  }
1027
- return { data, txHash, mode: "x402", amount: pricing.amount, timestamp: Date.now() };
1028
- }, this.retryPolicy);
1196
+ }
1197
+ let data;
1198
+ try {
1199
+ data = await settled.json();
1200
+ } catch {
1201
+ throw new RouteDockManifestError("Failed to parse JSON from settled response");
1202
+ }
1203
+ return { data, txHash, mode: "x402", amount: signedAmount, timestamp: Date.now() };
1029
1204
  }
1030
1205
  };
1031
1206
  var MppChargeClient = class {
@@ -1042,27 +1217,42 @@ var MppChargeClient = class {
1042
1217
  if (!pricing) {
1043
1218
  throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
1044
1219
  }
1045
- return withRetry(async () => {
1046
- let txHash = null;
1047
- const mppx = client$2.Mppx.create({
1048
- polyfill: false,
1049
- methods: [
1050
- client$3.stellar.charge({
1051
- keypair: this.keypair,
1052
- mode: "pull",
1053
- onProgress(event) {
1054
- if (event.type === "paid") {
1055
- txHash = event.hash;
1056
- }
1220
+ const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
1221
+ let txHash = null;
1222
+ let signedAmount;
1223
+ let credential;
1224
+ const mppx = client$2.Mppx.create({
1225
+ polyfill: false,
1226
+ methods: [
1227
+ client$3.stellar.charge({
1228
+ keypair: this.keypair,
1229
+ mode: "pull",
1230
+ onProgress(event) {
1231
+ if (event.type === "paid") {
1232
+ txHash = event.hash;
1057
1233
  }
1058
- })
1059
- ]
1060
- });
1234
+ }
1235
+ })
1236
+ ],
1237
+ onChallenge: async (challenge, { createCredential }) => {
1238
+ const request = challenge.request;
1239
+ const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
1240
+ if (rejection) throw rejection;
1241
+ const amount = request["amount"];
1242
+ if (typeof amount !== "string") {
1243
+ throw new RouteDockPolicyRejectError("challenge_amount_invalid");
1244
+ }
1245
+ signedAmount = nulthSdk.stroopsToUsdc(BigInt(amount));
1246
+ credential ??= await createCredential();
1247
+ return credential;
1248
+ }
1249
+ });
1250
+ return withRetry(async () => {
1061
1251
  let response;
1062
1252
  try {
1063
- response = await mppx.fetch(url);
1253
+ response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
1064
1254
  } catch (err) {
1065
- throw wrapFetchError(err, "MPP charge request");
1255
+ throw wrapMppError(err, "MPP charge request");
1066
1256
  }
1067
1257
  if (!response.ok) {
1068
1258
  if (response.status >= 500 || response.status === 429 || response.status === 503) {
@@ -1083,7 +1273,13 @@ var MppChargeClient = class {
1083
1273
  { cause }
1084
1274
  );
1085
1275
  }
1086
- return { data, txHash, mode: "mpp-charge", amount: pricing.amount, timestamp: Date.now() };
1276
+ return {
1277
+ data,
1278
+ txHash,
1279
+ mode: "mpp-charge",
1280
+ amount: signedAmount ?? pricing.amount,
1281
+ timestamp: Date.now()
1282
+ };
1087
1283
  }, this.retryPolicy);
1088
1284
  }
1089
1285
  };
@@ -1127,16 +1323,18 @@ function resolveStoreNetworkId(network) {
1127
1323
  return network === mpp.STELLAR_PUBNET ? mpp.STELLAR_PUBNET : mpp.STELLAR_TESTNET;
1128
1324
  }
1129
1325
  var MppSessionClient = class {
1130
- constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory) {
1326
+ constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory, logger = consoleLogger) {
1131
1327
  this.keypair = keypair;
1132
1328
  this.network = network;
1133
1329
  this.retryPolicy = retryPolicy;
1134
1330
  this.webSocketFactory = webSocketFactory;
1331
+ this.logger = logger;
1135
1332
  }
1136
1333
  keypair;
1137
1334
  network;
1138
1335
  retryPolicy;
1139
1336
  webSocketFactory;
1337
+ logger;
1140
1338
  async openSession(url, manifest, commitmentSecret, options, onSpend) {
1141
1339
  const mode = options?.mode ?? "mpp-session";
1142
1340
  const pricing = manifest.pricing[mode];
@@ -1361,7 +1559,7 @@ var MppSessionClient = class {
1361
1559
  if (abortController.signal.aborted) {
1362
1560
  throw err;
1363
1561
  }
1364
- throw wrapFetchError(err, "Voucher request");
1562
+ throw wrapMppError(err, "Voucher request");
1365
1563
  }
1366
1564
  if (!resp.ok) {
1367
1565
  if (resp.status >= 500 || resp.status === 429 || resp.status === 503) {
@@ -1654,9 +1852,10 @@ var MppSessionClient = class {
1654
1852
  emit("session:timeout", { maxDurationMs });
1655
1853
  void handle.close().catch((error) => {
1656
1854
  emit("session:close-failed", { maxDurationMs, error });
1657
- console.warn(
1855
+ this.logger(
1856
+ "warn",
1658
1857
  `RouteDock: maxDuration auto-close failed after ${maxDurationMs}ms \u2014 the channel may still hold collateral; retry close() or call requestRefund().`,
1659
- error
1858
+ { error }
1660
1859
  );
1661
1860
  });
1662
1861
  }, maxDurationMs);
@@ -1706,7 +1905,7 @@ var MppSessionClient = class {
1706
1905
  );
1707
1906
  onSigned?.();
1708
1907
  } catch (err) {
1709
- throw wrapFetchError(err, "Voucher credential");
1908
+ throw wrapMppError(err, "Voucher credential");
1710
1909
  }
1711
1910
  const wsUrl = url.replace(/^http/, "ws");
1712
1911
  const queue = [];
@@ -1883,11 +2082,6 @@ function encodeAuthSignature(proof) {
1883
2082
  const payload = { nulth: "zk-v1", proof };
1884
2083
  return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
1885
2084
  }
1886
- function usdcToStroops2(amount) {
1887
- const [whole = "0", frac = ""] = amount.split(".");
1888
- const padded = (frac + "0000000").slice(0, 7);
1889
- return BigInt(whole) * 10000000n + BigInt(padded);
1890
- }
1891
2085
  var NulthClient = class {
1892
2086
  constructor(config) {
1893
2087
  this.config = config;
@@ -1903,7 +2097,10 @@ var NulthClient = class {
1903
2097
  `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
1904
2098
  );
1905
2099
  }
1906
- console.warn("NulthClient is using the insecure mock prover; proofs are not cryptographically sound");
2100
+ resolveLogger(config.logger)(
2101
+ "warn",
2102
+ "NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
2103
+ );
1907
2104
  }
1908
2105
  config;
1909
2106
  policy;
@@ -1955,6 +2152,12 @@ var NulthClient = class {
1955
2152
  return proof;
1956
2153
  }
1957
2154
  enforcePolicy(context) {
2155
+ if (context.amountStroops < 0n) {
2156
+ throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
2157
+ }
2158
+ if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
2159
+ throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
2160
+ }
1958
2161
  if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
1959
2162
  throw new NulthPolicyError("session_expired", "Nulth session expired");
1960
2163
  }
@@ -1989,14 +2192,14 @@ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
1989
2192
  const pricing = manifest.pricing[mode];
1990
2193
  if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
1991
2194
  return {
1992
- payee: manifest.payee,
1993
- amountStroops: usdcToStroops2(pricing.amount),
2195
+ payee: pricing.payee ?? manifest.payee,
2196
+ amountStroops: nulthSdk.usdcToStroops(pricing.amount),
1994
2197
  assetContract: manifest.asset_contract,
1995
2198
  ledgerSequence
1996
2199
  };
1997
2200
  }
1998
2201
  function createPolicyState(input) {
1999
- const dailyCapStroops = usdcToStroops2(input.dailyCapUsdc);
2202
+ const dailyCapStroops = nulthSdk.usdcToStroops(input.dailyCapUsdc);
2000
2203
  return {
2001
2204
  dailyCapStroops,
2002
2205
  allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
@@ -2058,7 +2261,8 @@ var InMemorySpendStore = class {
2058
2261
  state = null;
2059
2262
  constructor(options = {}) {
2060
2263
  if (options.warn !== false) {
2061
- console.warn(
2264
+ resolveLogger(options.logger)(
2265
+ "warn",
2062
2266
  "[RouteDock] Using in-memory SpendStore: the daily spend cap is NOT durable and resets on every process restart. Inject a persistent SpendStore via RouteDockClientConfig.spendStore for production safety."
2063
2267
  );
2064
2268
  }
@@ -2071,6 +2275,72 @@ var InMemorySpendStore = class {
2071
2275
  }
2072
2276
  };
2073
2277
 
2278
+ // src/internal/assetUtils.ts
2279
+ function normalizeManifestAssets(manifest) {
2280
+ if (manifest.assets && manifest.assets.length > 0) {
2281
+ return manifest.assets;
2282
+ }
2283
+ if (manifest.asset && manifest.asset_contract) {
2284
+ return [
2285
+ {
2286
+ asset: manifest.asset,
2287
+ asset_contract: manifest.asset_contract
2288
+ }
2289
+ ];
2290
+ }
2291
+ throw new RouteDockManifestError(
2292
+ "Manifest must define either assets[] or root asset/asset_contract fields"
2293
+ );
2294
+ }
2295
+ function getEligibleAssets(manifest, mode, endpoint) {
2296
+ const assets = normalizeManifestAssets(manifest);
2297
+ return assets.filter((asset) => {
2298
+ if (asset.modes && !asset.modes.includes(mode)) {
2299
+ return false;
2300
+ }
2301
+ if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
2302
+ const trimmedEndpoint = endpoint.replace(/^\//, "");
2303
+ if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
2304
+ return true;
2305
+ }
2306
+ if (manifest.endpoints) {
2307
+ let pathname = endpoint;
2308
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
2309
+ try {
2310
+ pathname = new URL(endpoint).pathname;
2311
+ } catch {
2312
+ }
2313
+ }
2314
+ const trimmedPathname = pathname.replace(/^\//, "");
2315
+ const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
2316
+ const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
2317
+ const keyMatches = key === endpoint || key === trimmedEndpoint;
2318
+ if (pathMatches || keyMatches) {
2319
+ return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
2320
+ }
2321
+ return false;
2322
+ });
2323
+ if (!matchesEndpoint) {
2324
+ return false;
2325
+ }
2326
+ } else {
2327
+ return false;
2328
+ }
2329
+ }
2330
+ return true;
2331
+ });
2332
+ }
2333
+ function selectAsset(manifest, mode, endpoint) {
2334
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
2335
+ if (eligible.length === 0) {
2336
+ const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
2337
+ throw new RouteDockManifestError(
2338
+ `No eligible assets found for mode '${mode}'${endpointMsg}`
2339
+ );
2340
+ }
2341
+ return eligible[0];
2342
+ }
2343
+
2074
2344
  // src/client/RouteDockClient.ts
2075
2345
  var _secrets = /* @__PURE__ */ new WeakMap();
2076
2346
  function normalizeEndpointCaps(endpointCaps) {
@@ -2147,7 +2417,10 @@ var RouteDockClient = class _RouteDockClient {
2147
2417
  this.network = config.network;
2148
2418
  this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
2149
2419
  this.retryPolicy = config.retryPolicy;
2150
- this.spendStore = config.spendStore ?? new InMemorySpendStore({ warn: !!config.spendCap });
2420
+ this.spendStore = config.spendStore ?? new InMemorySpendStore({
2421
+ warn: !!config.spendCap,
2422
+ ...config.logger && { logger: config.logger }
2423
+ });
2151
2424
  this.logger = config.logger;
2152
2425
  this.manifestTimeoutMs = config.manifestTimeoutMs;
2153
2426
  this.expectedPayee = config.expectedPayee;
@@ -2159,9 +2432,10 @@ var RouteDockClient = class _RouteDockClient {
2159
2432
  _secrets.set(this, config.commitmentSecret);
2160
2433
  }
2161
2434
  const secretKey = this.keypair.secret();
2162
- this.x402 = new X402Client(secretKey, this.network, this.retryPolicy);
2163
- this.charge = new MppChargeClient(this.keypair, this.network, this.retryPolicy);
2164
- this.session = new MppSessionClient(this.keypair, this.network, this.retryPolicy);
2435
+ const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
2436
+ this.x402 = new X402Client(secretKey, this.network, retryPolicy);
2437
+ this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
2438
+ this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
2165
2439
  }
2166
2440
  /** Fetch manifest and select mode — shared by pay() and estimateCost(). */
2167
2441
  async _resolveManifest(url, options) {
@@ -2190,12 +2464,13 @@ var RouteDockClient = class _RouteDockClient {
2190
2464
  * call before committing to a payment — for approval gates and manual
2191
2465
  * trustline remediation.
2192
2466
  */
2193
- async preflight(manifest) {
2467
+ async preflight(manifest, mode, endpoint) {
2194
2468
  assertManifestValid(manifest);
2195
- await this._checkTrustline(manifest);
2469
+ const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
2470
+ await this._checkTrustline(manifest, selectedAsset);
2196
2471
  return {
2197
2472
  hasTrustline: true,
2198
- asset: manifest.asset,
2473
+ asset: selectedAsset,
2199
2474
  modes: manifest.modes,
2200
2475
  ...manifest.regions && { regions: manifest.regions },
2201
2476
  ...manifest.latency_hints && { latency_hints: manifest.latency_hints },
@@ -2209,8 +2484,8 @@ var RouteDockClient = class _RouteDockClient {
2209
2484
  * trustline is missing. Results are cached per (account, asset) so the
2210
2485
  * per-payment overhead is one RPC call amortized to zero after the first.
2211
2486
  */
2212
- async _checkTrustline(manifest) {
2213
- const cacheKey = `${this.network}:${this.keypair.publicKey()}:${manifest.asset}`;
2487
+ async _checkTrustline(manifest, targetAsset = manifest.asset) {
2488
+ const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
2214
2489
  const cached = _RouteDockClient._trustlineCache.get(cacheKey);
2215
2490
  if (cached && Date.now() < cached.expiresAt) return;
2216
2491
  const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
@@ -2218,13 +2493,13 @@ var RouteDockClient = class _RouteDockClient {
2218
2493
  try {
2219
2494
  const account = await server.loadAccount(this.keypair.publicKey());
2220
2495
  const balances = account.balances;
2221
- const expectedIssuer = getAssetIssuer(manifest.asset, this.network);
2496
+ const expectedIssuer = getAssetIssuer(targetAsset, this.network);
2222
2497
  const hasTrustline = balances.some(
2223
- (b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code === manifest.asset && (!expectedIssuer || b.asset_issuer === expectedIssuer)
2498
+ (b) => typeof b === "object" && b !== null && (targetAsset === "XLM" && b.asset_type === "native" || "asset_code" in b && b.asset_code === targetAsset && (!expectedIssuer || b.asset_issuer === expectedIssuer))
2224
2499
  );
2225
2500
  if (!hasTrustline) {
2226
- const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${manifest.asset}:${expectedIssuer} --limit 100000` : `Establish a trustline for ${manifest.asset} with the appropriate issuer on ${this.network}`;
2227
- throw new RouteDockTrustlineError(manifest.asset, expectedIssuer || "unknown", remediation);
2501
+ const remediation = expectedIssuer ? `Run: stellar tx new --source ${this.keypair.publicKey()} --network ${this.network} change-trust --asset ${targetAsset}:${expectedIssuer} --limit 100000` : `Establish a trustline for ${targetAsset} with the appropriate issuer on ${this.network}`;
2502
+ throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
2228
2503
  }
2229
2504
  _RouteDockClient._trustlineCache.set(cacheKey, {
2230
2505
  exists: true,
@@ -2233,7 +2508,8 @@ var RouteDockClient = class _RouteDockClient {
2233
2508
  } catch (err) {
2234
2509
  if (err instanceof RouteDockTrustlineError) throw err;
2235
2510
  this.logger?.(
2236
- `[RouteDock] Trustline preflight: could not verify trustline for ${manifest.asset} \u2014 continuing`
2511
+ "warn",
2512
+ `[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
2237
2513
  );
2238
2514
  }
2239
2515
  }
@@ -2241,18 +2517,18 @@ var RouteDockClient = class _RouteDockClient {
2241
2517
  * Pay for one request at `url`. Fetches manifest, selects payment mode,
2242
2518
  * runs trustline preflight, reserves local spend cap BEFORE executing the
2243
2519
  * payment, then commits the spend record on success. Rolls back the
2244
- * reservation if the on-chain payment fails. Concurrent pay() calls are
2245
- * serialized through a per-instance mutex to prevent spend-cap overrun.
2520
+ * reservation if the on-chain payment fails, and also when the call
2521
+ * completes without paying anything (an x402 endpoint answering a plain
2522
+ * 200 with no challenge). Concurrent pay() calls are serialized through a
2523
+ * per-instance mutex to prevent spend-cap overrun.
2246
2524
  */
2247
2525
  async pay(url, options) {
2248
2526
  const baseUrl = new URL(url).origin;
2249
2527
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2250
2528
  assertEndpointActive(manifest, url, this.logger);
2251
2529
  const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
2252
- await this._checkTrustline(manifest);
2253
- if (this.vault?.mode === "nulth") {
2254
- return this._payWithNulthVault(url, manifest, mode);
2255
- }
2530
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2531
+ await this._checkTrustline(manifest, selectedAsset);
2256
2532
  let amount;
2257
2533
  switch (mode) {
2258
2534
  case "x402":
@@ -2272,22 +2548,30 @@ var RouteDockClient = class _RouteDockClient {
2272
2548
  const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
2273
2549
  let result;
2274
2550
  try {
2275
- switch (mode) {
2276
- case "x402":
2277
- result = await this.x402.pay(url, manifest);
2278
- break;
2279
- case "mpp-charge":
2280
- result = await this.charge.pay(url, manifest);
2281
- break;
2282
- default:
2283
- throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2551
+ if (this.vault?.mode === "nulth") {
2552
+ result = await this._payWithNulthVault(url, manifest, mode);
2553
+ } else {
2554
+ switch (mode) {
2555
+ case "x402":
2556
+ result = await this.x402.pay(url, manifest);
2557
+ break;
2558
+ case "mpp-charge":
2559
+ result = await this.charge.pay(url, manifest);
2560
+ break;
2561
+ default:
2562
+ throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2563
+ }
2284
2564
  }
2285
2565
  } catch (err) {
2286
2566
  await this._rollbackSpend(reserveId).catch(() => {
2287
2567
  });
2288
2568
  throw err;
2289
2569
  }
2290
- await this._commitSpend(reserveId);
2570
+ if (result.txHash === null && result.amount === "0") {
2571
+ await this._rollbackSpend(reserveId);
2572
+ } else {
2573
+ await this._commitSpend(reserveId);
2574
+ }
2291
2575
  return result;
2292
2576
  }
2293
2577
  /** Nulth ZK vault path — proof built off-chain, attached as auth signature */
@@ -2337,9 +2621,10 @@ var RouteDockClient = class _RouteDockClient {
2337
2621
  default:
2338
2622
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2339
2623
  }
2624
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2340
2625
  return {
2341
2626
  amount,
2342
- asset: manifest.asset,
2627
+ asset: selectedAsset,
2343
2628
  mode,
2344
2629
  manifest,
2345
2630
  ...manifest.regions && { regions: manifest.regions },