@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/client.cjs CHANGED
@@ -165,10 +165,35 @@ function wrapFetchError(err, context) {
165
165
  if (err instanceof RouteDockError) return err;
166
166
  return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
167
167
  }
168
+ function wrapMppError(err, context) {
169
+ if (err instanceof RouteDockError) return err;
170
+ if (err instanceof TypeError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
171
+ return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
172
+ }
173
+ return new RouteDockSignatureError(`${context}: ${String(err)}`, { cause: err });
174
+ }
168
175
 
169
176
  // src/types.ts
170
177
  var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
171
178
 
179
+ // src/internal/logger.ts
180
+ function createConsoleLogger() {
181
+ return (level, message, fields) => {
182
+ const values = fields ? Object.values(fields) : [];
183
+ if (values.length > 0) {
184
+ console[level](message, ...values);
185
+ } else {
186
+ console[level](message);
187
+ }
188
+ };
189
+ }
190
+ var consoleLogger = createConsoleLogger();
191
+ var noopLogger = () => {
192
+ };
193
+ function resolveLogger(logger, fallback = consoleLogger) {
194
+ return logger ?? fallback;
195
+ }
196
+
172
197
  // src/internal/retry.ts
173
198
  var DEFAULT_RETRY_POLICY = {
174
199
  maxAttempts: 4,
@@ -183,10 +208,11 @@ function sleep(ms) {
183
208
  return new Promise((resolve) => setTimeout(resolve, ms));
184
209
  }
185
210
  async function withRetry(fn, policy = {}) {
186
- const { maxAttempts, baseDelayMs, maxDelayMs, onRetry } = {
211
+ const { maxAttempts, baseDelayMs, maxDelayMs, onRetry, logger } = {
187
212
  ...DEFAULT_RETRY_POLICY,
188
213
  ...policy
189
214
  };
215
+ const log = resolveLogger(logger);
190
216
  let lastError;
191
217
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
192
218
  try {
@@ -204,7 +230,7 @@ async function withRetry(fn, policy = {}) {
204
230
  try {
205
231
  await onRetry(attempt, err, delay);
206
232
  } catch (cbErr) {
207
- console.error("[withRetry] onRetry callback threw:", cbErr);
233
+ log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
208
234
  }
209
235
  }
210
236
  await sleep(delay);
@@ -218,6 +244,84 @@ var USDC_ISSUERS = {
218
244
  };
219
245
  var usdcToUnits = nulthSdk.usdcToStroops;
220
246
 
247
+ // src/internal/assetUtils.ts
248
+ function normalizeManifestAssets(manifest) {
249
+ if (manifest.assets && manifest.assets.length > 0) {
250
+ return manifest.assets;
251
+ }
252
+ if (manifest.asset && manifest.asset_contract) {
253
+ return [
254
+ {
255
+ asset: manifest.asset,
256
+ asset_contract: manifest.asset_contract
257
+ }
258
+ ];
259
+ }
260
+ throw new RouteDockManifestError(
261
+ "Manifest must define either assets[] or root asset/asset_contract fields"
262
+ );
263
+ }
264
+ function getEligibleAssets(manifest, mode, endpoint) {
265
+ const assets = normalizeManifestAssets(manifest);
266
+ return assets.filter((asset) => {
267
+ if (asset.modes && !asset.modes.includes(mode)) {
268
+ return false;
269
+ }
270
+ if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
271
+ const trimmedEndpoint = endpoint.replace(/^\//, "");
272
+ if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
273
+ return true;
274
+ }
275
+ if (manifest.endpoints) {
276
+ let pathname = endpoint;
277
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
278
+ try {
279
+ pathname = new URL(endpoint).pathname;
280
+ } catch {
281
+ }
282
+ }
283
+ const trimmedPathname = pathname.replace(/^\//, "");
284
+ const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
285
+ const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
286
+ const keyMatches = key === endpoint || key === trimmedEndpoint;
287
+ if (pathMatches || keyMatches) {
288
+ return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
289
+ }
290
+ return false;
291
+ });
292
+ if (!matchesEndpoint) {
293
+ return false;
294
+ }
295
+ } else {
296
+ return false;
297
+ }
298
+ }
299
+ return true;
300
+ });
301
+ }
302
+ function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
303
+ if (manifest.assets && manifest.assets.length > 0) {
304
+ return selectAsset(manifest, mode, endpoint).asset_contract;
305
+ }
306
+ return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
307
+ }
308
+ function selectAsset(manifest, mode, endpoint) {
309
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
310
+ if (eligible.length === 0) {
311
+ const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
312
+ throw new RouteDockManifestError(
313
+ `No eligible assets found for mode '${mode}'${endpointMsg}`
314
+ );
315
+ }
316
+ return eligible[0];
317
+ }
318
+ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
319
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
320
+ return eligible.some(
321
+ (asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
322
+ );
323
+ }
324
+
221
325
  // src/schemas/routedock.schema.json
222
326
  var routedock_schema_default = {
223
327
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -297,6 +401,14 @@ var routedock_schema_default = {
297
401
  type: "string",
298
402
  description: "Stellar Asset Contract (SAC) address for the payment asset"
299
403
  },
404
+ assets: {
405
+ type: "array",
406
+ minItems: 1,
407
+ items: {
408
+ $ref: "#/definitions/AssetConfig"
409
+ },
410
+ 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."
411
+ },
300
412
  payee: {
301
413
  type: "string",
302
414
  description: "Stellar address (G...) that receives payments"
@@ -429,6 +541,38 @@ var routedock_schema_default = {
429
541
  enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
430
542
  description: "A supported RouteDock payment mode"
431
543
  },
544
+ AssetConfig: {
545
+ type: "object",
546
+ description: "Payment asset configuration with optional mode and endpoint scoping",
547
+ required: ["asset", "asset_contract"],
548
+ additionalProperties: false,
549
+ properties: {
550
+ asset: {
551
+ type: "string",
552
+ description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
553
+ },
554
+ asset_contract: {
555
+ type: "string",
556
+ description: "Stellar Asset Contract (SAC) address for the payment asset"
557
+ },
558
+ modes: {
559
+ type: "array",
560
+ items: {
561
+ $ref: "#/definitions/PaymentMode"
562
+ },
563
+ uniqueItems: true,
564
+ description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
565
+ },
566
+ endpoints: {
567
+ type: "array",
568
+ items: {
569
+ type: "string"
570
+ },
571
+ uniqueItems: true,
572
+ description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
573
+ }
574
+ }
575
+ },
432
576
  PricingConfig: {
433
577
  type: "object",
434
578
  description: "Per-request pricing for x402 or mpp-charge modes",
@@ -574,7 +718,7 @@ var routedock_schema_default = {
574
718
 
575
719
  // package.json
576
720
  var package_default = {
577
- version: "0.2.0"};
721
+ version: "0.3.1"};
578
722
  var MANIFEST_SIGNATURE_VERSION = "2";
579
723
  function canonicalize(value) {
580
724
  if (Array.isArray(value)) {
@@ -688,6 +832,7 @@ function assertEndpointActive(manifest, url, logger) {
688
832
  const [key, descriptor] = deprecated;
689
833
  const sunsetSuffix = descriptor.sunset_at ? `; sunset_at ${descriptor.sunset_at}` : "";
690
834
  logger(
835
+ "warn",
691
836
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${pathname}; endpoint '${key}' is deprecated${sunsetSuffix}`
692
837
  );
693
838
  }
@@ -709,6 +854,27 @@ function assertManifestValid(manifest, baseUrl) {
709
854
  }
710
855
  }
711
856
  }
857
+ if (manifest.assets !== void 0) {
858
+ if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
859
+ throw new RouteDockManifestError(
860
+ `Invalid manifest${context}: assets must be a non-empty array when defined`
861
+ );
862
+ }
863
+ for (let i = 0; i < manifest.assets.length; i++) {
864
+ const a = manifest.assets[i];
865
+ if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
866
+ throw new RouteDockManifestError(
867
+ `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
868
+ );
869
+ }
870
+ }
871
+ const first = manifest.assets[0];
872
+ if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
873
+ throw new RouteDockManifestError(
874
+ `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
875
+ );
876
+ }
877
+ }
712
878
  }
713
879
  var CACHE_TTL_MS = 6e4;
714
880
  var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
@@ -888,16 +1054,16 @@ function logSelection(manifest, selection, log, deprecated) {
888
1054
  const reason = selection.reason ? ` (${selection.reason})` : "";
889
1055
  if (deprecated) {
890
1056
  log(
1057
+ "warn",
891
1058
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${selection.mode}${reason}; selected deprecated mode because no active supported mode is available`
892
1059
  );
893
1060
  return;
894
1061
  }
895
- log(`[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
1062
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
896
1063
  }
897
1064
  function selectMode(manifest, options = {}) {
898
1065
  const modes = manifest.modes;
899
- const log = options.logger ?? (() => {
900
- });
1066
+ const log = options.logger ?? noopLogger;
901
1067
  const deprecatedSet = new Set(manifest.deprecated_modes ?? []);
902
1068
  if (options.forceMode) {
903
1069
  if (!modes.includes(options.forceMode)) {
@@ -907,10 +1073,11 @@ function selectMode(manifest, options = {}) {
907
1073
  }
908
1074
  if (deprecatedSet.has(options.forceMode)) {
909
1075
  log(
1076
+ "warn",
910
1077
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${options.forceMode} (forced); selected mode is deprecated`
911
1078
  );
912
1079
  } else {
913
- log(`[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
1080
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
914
1081
  }
915
1082
  return options.forceMode;
916
1083
  }
@@ -946,6 +1113,69 @@ function rankProvidersByLatency(manifests, targetRegion) {
946
1113
  return 0;
947
1114
  });
948
1115
  }
1116
+
1117
+ // src/internal/payee.ts
1118
+ function resolvePayee(manifest, mode) {
1119
+ const override = manifest.pricing[mode]?.payee;
1120
+ return override ?? manifest.payee;
1121
+ }
1122
+
1123
+ // src/client/challenge.ts
1124
+ function isRecord(value) {
1125
+ return typeof value === "object" && value !== null;
1126
+ }
1127
+ function isPositiveIntegerString(value) {
1128
+ return typeof value === "string" && /^[1-9]\d*$/.test(value);
1129
+ }
1130
+ function checkAmount(amount, maxAmount) {
1131
+ if (!isPositiveIntegerString(amount)) {
1132
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1133
+ }
1134
+ let max;
1135
+ try {
1136
+ max = nulthSdk.usdcToStroops(maxAmount);
1137
+ } catch {
1138
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1139
+ }
1140
+ if (BigInt(amount) > max) {
1141
+ return new RouteDockPolicyRejectError("challenge_amount_exceeds_manifest");
1142
+ }
1143
+ return null;
1144
+ }
1145
+ function checkX402Accept(accept, manifest, network, maxAmount) {
1146
+ if (accept.network !== network) {
1147
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1148
+ }
1149
+ if (accept.payTo !== resolvePayee(manifest, "x402")) {
1150
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1151
+ }
1152
+ if (accept.asset !== manifest.asset_contract) {
1153
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1154
+ }
1155
+ return checkAmount(accept.amount, maxAmount);
1156
+ }
1157
+ function filterX402Accepts(accepts, manifest, network, maxAmount) {
1158
+ return accepts.filter((accept) => checkX402Accept(accept, manifest, network, maxAmount) === null);
1159
+ }
1160
+ function checkChargeChallenge(request, manifest, network, maxAmount) {
1161
+ const methodDetails = request["methodDetails"];
1162
+ const rawNetwork = isRecord(methodDetails) ? methodDetails["network"] : void 0;
1163
+ const challengeNetwork = rawNetwork === void 0 ? "stellar:testnet" : rawNetwork;
1164
+ if (challengeNetwork !== network) {
1165
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1166
+ }
1167
+ const recipient = request["recipient"];
1168
+ if (typeof recipient !== "string" || recipient !== resolvePayee(manifest, "mpp-charge")) {
1169
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1170
+ }
1171
+ const currency = request["currency"];
1172
+ if (typeof currency !== "string" || currency !== manifest.asset_contract) {
1173
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1174
+ }
1175
+ return checkAmount(request["amount"], maxAmount);
1176
+ }
1177
+
1178
+ // src/client/x402Client.ts
949
1179
  var CAIP2 = {
950
1180
  testnet: "stellar:testnet",
951
1181
  mainnet: "stellar:pubnet"
@@ -977,83 +1207,106 @@ var X402Client = class _X402Client {
977
1207
  if (!pricing.facilitator) {
978
1208
  throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
979
1209
  }
980
- return withRetry(async () => {
981
- let init;
1210
+ const caip2 = CAIP2[this.network];
1211
+ const probe = await withRetry(async () => {
1212
+ let init2;
982
1213
  try {
983
- init = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
1214
+ init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
984
1215
  } catch (err) {
985
1216
  throw wrapFetchError(err, "x402 initial request");
986
1217
  }
987
- if (init.status !== 402) {
988
- if (!init.ok) {
1218
+ if (init2.status !== 402) {
1219
+ if (!init2.ok) {
989
1220
  throw httpStatusToError(
990
- `x402 initial request failed: HTTP ${init.status}`,
991
- init.status,
992
- init
1221
+ `x402 initial request failed: HTTP ${init2.status}`,
1222
+ init2.status,
1223
+ init2
993
1224
  );
994
1225
  }
995
1226
  let data2;
996
1227
  try {
997
- data2 = await init.json();
1228
+ data2 = await init2.json();
998
1229
  } catch {
999
- throw new RouteDockManifestError(`Failed to parse JSON from non-402 response (HTTP ${init.status})`);
1230
+ throw new RouteDockManifestError(
1231
+ `Failed to parse JSON from non-402 response (HTTP ${init2.status})`
1232
+ );
1000
1233
  }
1001
- return { data: data2, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
1002
- }
1003
- const reqHeader = init.headers.get("X-Payment-Requirements");
1004
- if (!reqHeader) {
1005
- throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1006
- }
1007
- let paymentRequired;
1008
- try {
1009
- paymentRequired = http.decodePaymentRequiredHeader(reqHeader);
1010
- } catch (err) {
1011
- throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
1012
- cause: err
1013
- });
1014
- }
1015
- let paymentPayload;
1016
- try {
1017
- paymentPayload = await this.httpClient.createPaymentPayload(paymentRequired);
1018
- } catch (err) {
1019
- throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
1020
- cause: err
1021
- });
1234
+ return { kind: "free", data: data2 };
1022
1235
  }
1023
- const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1024
- let settled;
1236
+ return { kind: "payment-required", init: init2 };
1237
+ }, this.retryPolicy);
1238
+ if (probe.kind === "free") {
1239
+ return { data: probe.data, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
1240
+ }
1241
+ const init = probe.init;
1242
+ const reqHeader = init.headers.get("X-Payment-Requirements");
1243
+ if (!reqHeader) {
1244
+ throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1245
+ }
1246
+ let paymentRequired;
1247
+ try {
1248
+ paymentRequired = http.decodePaymentRequiredHeader(reqHeader);
1249
+ } catch (err) {
1250
+ throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
1251
+ cause: err
1252
+ });
1253
+ }
1254
+ const firstAccept = paymentRequired.accepts[0];
1255
+ if (!firstAccept) {
1256
+ throw new RouteDockManifestError("402 response carries no payment accepts");
1257
+ }
1258
+ const allowed = filterX402Accepts(paymentRequired.accepts, manifest, caip2, pricing.amount);
1259
+ if (allowed.length === 0) {
1260
+ throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
1261
+ }
1262
+ let paymentPayload;
1263
+ try {
1264
+ paymentPayload = await this.httpClient.createPaymentPayload({
1265
+ ...paymentRequired,
1266
+ accepts: allowed
1267
+ });
1268
+ } catch (err) {
1269
+ throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
1270
+ cause: err
1271
+ });
1272
+ }
1273
+ const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1274
+ const signedAmount = nulthSdk.stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
1275
+ const settled = await withRetry(async () => {
1276
+ let response;
1025
1277
  try {
1026
- settled = await fetch(url, { headers: paymentHeaders });
1278
+ response = await fetch(url, { headers: paymentHeaders });
1027
1279
  } catch (err) {
1028
1280
  throw wrapFetchError(err, "x402 settlement request");
1029
1281
  }
1030
- if (!settled.ok) {
1031
- if (settled.status >= 500 || settled.status === 429 || settled.status === 503) {
1282
+ if (!response.ok) {
1283
+ if (response.status >= 500 || response.status === 429 || response.status === 503) {
1032
1284
  throw httpStatusToError(
1033
- `x402 payment failed: HTTP ${settled.status}`,
1034
- settled.status,
1035
- settled
1285
+ `x402 payment failed: HTTP ${response.status}`,
1286
+ response.status,
1287
+ response
1036
1288
  );
1037
1289
  }
1038
- throw new RouteDockManifestError(`x402 payment failed: HTTP ${settled.status}`);
1039
- }
1040
- let txHash = null;
1041
- const responseHeader = settled.headers.get("X-Payment-Response");
1042
- if (responseHeader) {
1043
- try {
1044
- const settleResponse = http.decodePaymentResponseHeader(responseHeader);
1045
- txHash = settleResponse.transaction ?? null;
1046
- } catch {
1047
- }
1290
+ throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
1048
1291
  }
1049
- let data;
1292
+ return response;
1293
+ }, this.retryPolicy);
1294
+ let txHash = null;
1295
+ const responseHeader = settled.headers.get("X-Payment-Response");
1296
+ if (responseHeader) {
1050
1297
  try {
1051
- data = await settled.json();
1298
+ const settleResponse = http.decodePaymentResponseHeader(responseHeader);
1299
+ txHash = settleResponse.transaction ?? null;
1052
1300
  } catch {
1053
- throw new RouteDockManifestError("Failed to parse JSON from settled response");
1054
1301
  }
1055
- return { data, txHash, mode: "x402", amount: pricing.amount, timestamp: Date.now() };
1056
- }, this.retryPolicy);
1302
+ }
1303
+ let data;
1304
+ try {
1305
+ data = await settled.json();
1306
+ } catch {
1307
+ throw new RouteDockManifestError("Failed to parse JSON from settled response");
1308
+ }
1309
+ return { data, txHash, mode: "x402", amount: signedAmount, timestamp: Date.now() };
1057
1310
  }
1058
1311
  };
1059
1312
  var MppChargeClient = class {
@@ -1070,27 +1323,42 @@ var MppChargeClient = class {
1070
1323
  if (!pricing) {
1071
1324
  throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
1072
1325
  }
1073
- return withRetry(async () => {
1074
- let txHash = null;
1075
- const mppx = client$2.Mppx.create({
1076
- polyfill: false,
1077
- methods: [
1078
- client$3.stellar.charge({
1079
- keypair: this.keypair,
1080
- mode: "pull",
1081
- onProgress(event) {
1082
- if (event.type === "paid") {
1083
- txHash = event.hash;
1084
- }
1326
+ const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
1327
+ let txHash = null;
1328
+ let signedAmount;
1329
+ let credential;
1330
+ const mppx = client$2.Mppx.create({
1331
+ polyfill: false,
1332
+ methods: [
1333
+ client$3.stellar.charge({
1334
+ keypair: this.keypair,
1335
+ mode: "pull",
1336
+ onProgress(event) {
1337
+ if (event.type === "paid") {
1338
+ txHash = event.hash;
1085
1339
  }
1086
- })
1087
- ]
1088
- });
1340
+ }
1341
+ })
1342
+ ],
1343
+ onChallenge: async (challenge, { createCredential }) => {
1344
+ const request = challenge.request;
1345
+ const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
1346
+ if (rejection) throw rejection;
1347
+ const amount = request["amount"];
1348
+ if (typeof amount !== "string") {
1349
+ throw new RouteDockPolicyRejectError("challenge_amount_invalid");
1350
+ }
1351
+ signedAmount = nulthSdk.stroopsToUsdc(BigInt(amount));
1352
+ credential ??= await createCredential();
1353
+ return credential;
1354
+ }
1355
+ });
1356
+ return withRetry(async () => {
1089
1357
  let response;
1090
1358
  try {
1091
- response = await mppx.fetch(url);
1359
+ response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
1092
1360
  } catch (err) {
1093
- throw wrapFetchError(err, "MPP charge request");
1361
+ throw wrapMppError(err, "MPP charge request");
1094
1362
  }
1095
1363
  if (!response.ok) {
1096
1364
  if (response.status >= 500 || response.status === 429 || response.status === 503) {
@@ -1111,7 +1379,13 @@ var MppChargeClient = class {
1111
1379
  { cause }
1112
1380
  );
1113
1381
  }
1114
- return { data, txHash, mode: "mpp-charge", amount: pricing.amount, timestamp: Date.now() };
1382
+ return {
1383
+ data,
1384
+ txHash,
1385
+ mode: "mpp-charge",
1386
+ amount: signedAmount ?? pricing.amount,
1387
+ timestamp: Date.now()
1388
+ };
1115
1389
  }, this.retryPolicy);
1116
1390
  }
1117
1391
  };
@@ -1150,16 +1424,18 @@ function resolveStoreNetworkId(network) {
1150
1424
  return network === mpp.STELLAR_PUBNET ? mpp.STELLAR_PUBNET : mpp.STELLAR_TESTNET;
1151
1425
  }
1152
1426
  var MppSessionClient = class {
1153
- constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory) {
1427
+ constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory, logger = consoleLogger) {
1154
1428
  this.keypair = keypair;
1155
1429
  this.network = network;
1156
1430
  this.retryPolicy = retryPolicy;
1157
1431
  this.webSocketFactory = webSocketFactory;
1432
+ this.logger = logger;
1158
1433
  }
1159
1434
  keypair;
1160
1435
  network;
1161
1436
  retryPolicy;
1162
1437
  webSocketFactory;
1438
+ logger;
1163
1439
  async openSession(url, manifest, commitmentSecret, options, onSpend) {
1164
1440
  const mode = options?.mode ?? "mpp-session";
1165
1441
  const pricing = manifest.pricing[mode];
@@ -1384,7 +1660,7 @@ var MppSessionClient = class {
1384
1660
  if (abortController.signal.aborted) {
1385
1661
  throw err;
1386
1662
  }
1387
- throw wrapFetchError(err, "Voucher request");
1663
+ throw wrapMppError(err, "Voucher request");
1388
1664
  }
1389
1665
  if (!resp.ok) {
1390
1666
  if (resp.status >= 500 || resp.status === 429 || resp.status === 503) {
@@ -1677,9 +1953,10 @@ var MppSessionClient = class {
1677
1953
  emit("session:timeout", { maxDurationMs });
1678
1954
  void handle.close().catch((error) => {
1679
1955
  emit("session:close-failed", { maxDurationMs, error });
1680
- console.warn(
1956
+ this.logger(
1957
+ "warn",
1681
1958
  `RouteDock: maxDuration auto-close failed after ${maxDurationMs}ms \u2014 the channel may still hold collateral; retry close() or call requestRefund().`,
1682
- error
1959
+ { error }
1683
1960
  );
1684
1961
  });
1685
1962
  }, maxDurationMs);
@@ -1729,7 +2006,7 @@ var MppSessionClient = class {
1729
2006
  );
1730
2007
  onSigned?.();
1731
2008
  } catch (err) {
1732
- throw wrapFetchError(err, "Voucher credential");
2009
+ throw wrapMppError(err, "Voucher credential");
1733
2010
  }
1734
2011
  const wsUrl = url.replace(/^http/, "ws");
1735
2012
  const queue = [];
@@ -1906,11 +2183,6 @@ function encodeAuthSignature(proof) {
1906
2183
  const payload = { nulth: "zk-v1", proof };
1907
2184
  return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
1908
2185
  }
1909
- function usdcToStroops2(amount) {
1910
- const [whole = "0", frac = ""] = amount.split(".");
1911
- const padded = (frac + "0000000").slice(0, 7);
1912
- return BigInt(whole) * 10000000n + BigInt(padded);
1913
- }
1914
2186
  var NulthClient = class {
1915
2187
  constructor(config) {
1916
2188
  this.config = config;
@@ -1926,7 +2198,10 @@ var NulthClient = class {
1926
2198
  `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
1927
2199
  );
1928
2200
  }
1929
- console.warn("NulthClient is using the insecure mock prover; proofs are not cryptographically sound");
2201
+ resolveLogger(config.logger)(
2202
+ "warn",
2203
+ "NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
2204
+ );
1930
2205
  }
1931
2206
  config;
1932
2207
  policy;
@@ -1978,6 +2253,12 @@ var NulthClient = class {
1978
2253
  return proof;
1979
2254
  }
1980
2255
  enforcePolicy(context) {
2256
+ if (context.amountStroops < 0n) {
2257
+ throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
2258
+ }
2259
+ if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
2260
+ throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
2261
+ }
1981
2262
  if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
1982
2263
  throw new NulthPolicyError("session_expired", "Nulth session expired");
1983
2264
  }
@@ -2012,14 +2293,14 @@ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
2012
2293
  const pricing = manifest.pricing[mode];
2013
2294
  if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
2014
2295
  return {
2015
- payee: manifest.payee,
2016
- amountStroops: usdcToStroops2(pricing.amount),
2296
+ payee: pricing.payee ?? manifest.payee,
2297
+ amountStroops: nulthSdk.usdcToStroops(pricing.amount),
2017
2298
  assetContract: manifest.asset_contract,
2018
2299
  ledgerSequence
2019
2300
  };
2020
2301
  }
2021
2302
  function createPolicyState(input) {
2022
- const dailyCapStroops = usdcToStroops2(input.dailyCapUsdc);
2303
+ const dailyCapStroops = nulthSdk.usdcToStroops(input.dailyCapUsdc);
2023
2304
  return {
2024
2305
  dailyCapStroops,
2025
2306
  allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
@@ -2121,7 +2402,8 @@ var InMemorySpendStore = class {
2121
2402
  state = null;
2122
2403
  constructor(options = {}) {
2123
2404
  if (options.warn !== false) {
2124
- console.warn(
2405
+ resolveLogger(options.logger)(
2406
+ "warn",
2125
2407
  "[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."
2126
2408
  );
2127
2409
  }
@@ -2211,7 +2493,10 @@ var RouteDockClient = class _RouteDockClient {
2211
2493
  this.network = config.network;
2212
2494
  this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
2213
2495
  this.retryPolicy = config.retryPolicy;
2214
- this.spendStore = config.spendStore ?? new InMemorySpendStore({ warn: !!config.spendCap });
2496
+ this.spendStore = config.spendStore ?? new InMemorySpendStore({
2497
+ warn: !!config.spendCap,
2498
+ ...config.logger && { logger: config.logger }
2499
+ });
2215
2500
  this.logger = config.logger;
2216
2501
  this.manifestTimeoutMs = config.manifestTimeoutMs;
2217
2502
  this.expectedPayee = config.expectedPayee;
@@ -2223,9 +2508,10 @@ var RouteDockClient = class _RouteDockClient {
2223
2508
  _secrets.set(this, config.commitmentSecret);
2224
2509
  }
2225
2510
  const secretKey = this.keypair.secret();
2226
- this.x402 = new X402Client(secretKey, this.network, this.retryPolicy);
2227
- this.charge = new MppChargeClient(this.keypair, this.network, this.retryPolicy);
2228
- this.session = new MppSessionClient(this.keypair, this.network, this.retryPolicy);
2511
+ const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
2512
+ this.x402 = new X402Client(secretKey, this.network, retryPolicy);
2513
+ this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
2514
+ this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
2229
2515
  }
2230
2516
  /** Fetch manifest and select mode — shared by pay() and estimateCost(). */
2231
2517
  async _resolveManifest(url, options) {
@@ -2254,12 +2540,13 @@ var RouteDockClient = class _RouteDockClient {
2254
2540
  * call before committing to a payment — for approval gates and manual
2255
2541
  * trustline remediation.
2256
2542
  */
2257
- async preflight(manifest) {
2543
+ async preflight(manifest, mode, endpoint) {
2258
2544
  assertManifestValid(manifest);
2259
- await this._checkTrustline(manifest);
2545
+ const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
2546
+ await this._checkTrustline(manifest, selectedAsset);
2260
2547
  return {
2261
2548
  hasTrustline: true,
2262
- asset: manifest.asset,
2549
+ asset: selectedAsset,
2263
2550
  modes: manifest.modes,
2264
2551
  ...manifest.regions && { regions: manifest.regions },
2265
2552
  ...manifest.latency_hints && { latency_hints: manifest.latency_hints },
@@ -2273,8 +2560,8 @@ var RouteDockClient = class _RouteDockClient {
2273
2560
  * trustline is missing. Results are cached per (account, asset) so the
2274
2561
  * per-payment overhead is one RPC call amortized to zero after the first.
2275
2562
  */
2276
- async _checkTrustline(manifest) {
2277
- const cacheKey = `${this.network}:${this.keypair.publicKey()}:${manifest.asset}`;
2563
+ async _checkTrustline(manifest, targetAsset = manifest.asset) {
2564
+ const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
2278
2565
  const cached = _RouteDockClient._trustlineCache.get(cacheKey);
2279
2566
  if (cached && Date.now() < cached.expiresAt) return;
2280
2567
  const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
@@ -2282,13 +2569,13 @@ var RouteDockClient = class _RouteDockClient {
2282
2569
  try {
2283
2570
  const account = await server.loadAccount(this.keypair.publicKey());
2284
2571
  const balances = account.balances;
2285
- const expectedIssuer = getAssetIssuer(manifest.asset, this.network);
2572
+ const expectedIssuer = getAssetIssuer(targetAsset, this.network);
2286
2573
  const hasTrustline = balances.some(
2287
- (b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code === manifest.asset && (!expectedIssuer || b.asset_issuer === expectedIssuer)
2574
+ (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))
2288
2575
  );
2289
2576
  if (!hasTrustline) {
2290
- 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}`;
2291
- throw new RouteDockTrustlineError(manifest.asset, expectedIssuer || "unknown", remediation);
2577
+ 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}`;
2578
+ throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
2292
2579
  }
2293
2580
  _RouteDockClient._trustlineCache.set(cacheKey, {
2294
2581
  exists: true,
@@ -2297,7 +2584,8 @@ var RouteDockClient = class _RouteDockClient {
2297
2584
  } catch (err) {
2298
2585
  if (err instanceof RouteDockTrustlineError) throw err;
2299
2586
  this.logger?.(
2300
- `[RouteDock] Trustline preflight: could not verify trustline for ${manifest.asset} \u2014 continuing`
2587
+ "warn",
2588
+ `[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
2301
2589
  );
2302
2590
  }
2303
2591
  }
@@ -2305,18 +2593,18 @@ var RouteDockClient = class _RouteDockClient {
2305
2593
  * Pay for one request at `url`. Fetches manifest, selects payment mode,
2306
2594
  * runs trustline preflight, reserves local spend cap BEFORE executing the
2307
2595
  * payment, then commits the spend record on success. Rolls back the
2308
- * reservation if the on-chain payment fails. Concurrent pay() calls are
2309
- * serialized through a per-instance mutex to prevent spend-cap overrun.
2596
+ * reservation if the on-chain payment fails, and also when the call
2597
+ * completes without paying anything (an x402 endpoint answering a plain
2598
+ * 200 with no challenge). Concurrent pay() calls are serialized through a
2599
+ * per-instance mutex to prevent spend-cap overrun.
2310
2600
  */
2311
2601
  async pay(url, options) {
2312
2602
  const baseUrl = new URL(url).origin;
2313
2603
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2314
2604
  assertEndpointActive(manifest, url, this.logger);
2315
2605
  const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
2316
- await this._checkTrustline(manifest);
2317
- if (this.vault?.mode === "nulth") {
2318
- return this._payWithNulthVault(url, manifest, mode);
2319
- }
2606
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2607
+ await this._checkTrustline(manifest, selectedAsset);
2320
2608
  let amount;
2321
2609
  switch (mode) {
2322
2610
  case "x402":
@@ -2336,22 +2624,30 @@ var RouteDockClient = class _RouteDockClient {
2336
2624
  const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
2337
2625
  let result;
2338
2626
  try {
2339
- switch (mode) {
2340
- case "x402":
2341
- result = await this.x402.pay(url, manifest);
2342
- break;
2343
- case "mpp-charge":
2344
- result = await this.charge.pay(url, manifest);
2345
- break;
2346
- default:
2347
- throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2627
+ if (this.vault?.mode === "nulth") {
2628
+ result = await this._payWithNulthVault(url, manifest, mode);
2629
+ } else {
2630
+ switch (mode) {
2631
+ case "x402":
2632
+ result = await this.x402.pay(url, manifest);
2633
+ break;
2634
+ case "mpp-charge":
2635
+ result = await this.charge.pay(url, manifest);
2636
+ break;
2637
+ default:
2638
+ throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2639
+ }
2348
2640
  }
2349
2641
  } catch (err) {
2350
2642
  await this._rollbackSpend(reserveId).catch(() => {
2351
2643
  });
2352
2644
  throw err;
2353
2645
  }
2354
- await this._commitSpend(reserveId);
2646
+ if (result.txHash === null && result.amount === "0") {
2647
+ await this._rollbackSpend(reserveId);
2648
+ } else {
2649
+ await this._commitSpend(reserveId);
2650
+ }
2355
2651
  return result;
2356
2652
  }
2357
2653
  /** Nulth ZK vault path — proof built off-chain, attached as auth signature */
@@ -2401,9 +2697,10 @@ var RouteDockClient = class _RouteDockClient {
2401
2697
  default:
2402
2698
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2403
2699
  }
2700
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2404
2701
  return {
2405
2702
  amount,
2406
- asset: manifest.asset,
2703
+ asset: selectedAsset,
2407
2704
  mode,
2408
2705
  manifest,
2409
2706
  ...manifest.regions && { regions: manifest.regions },
@@ -2816,20 +3113,30 @@ exports.assertManifestValid = assertManifestValid;
2816
3113
  exports.assertNulthVaultManifest = assertNulthVaultManifest;
2817
3114
  exports.backoffDelayMs = backoffDelayMs;
2818
3115
  exports.configureManifestCache = configureManifestCache;
3116
+ exports.consoleLogger = consoleLogger;
3117
+ exports.createConsoleLogger = createConsoleLogger;
2819
3118
  exports.createNulthSigner = createNulthSigner;
2820
3119
  exports.createPolicyState = createPolicyState;
2821
3120
  exports.decodeAuthSignature = decodeAuthSignature;
2822
3121
  exports.fetchLedgerSequence = fetchLedgerSequence;
2823
3122
  exports.fetchManifest = fetchManifest;
3123
+ exports.getEligibleAssets = getEligibleAssets;
2824
3124
  exports.httpStatusToError = httpStatusToError;
2825
3125
  exports.invalidateManifest = invalidateManifest;
3126
+ exports.isAssetEligible = isAssetEligible;
3127
+ exports.noopLogger = noopLogger;
3128
+ exports.normalizeManifestAssets = normalizeManifestAssets;
2826
3129
  exports.parseRetryAfterMs = parseRetryAfterMs;
2827
3130
  exports.paymentContextFromManifest = paymentContextFromManifest;
2828
3131
  exports.prepareNulthSigner = prepareNulthSigner;
2829
3132
  exports.rankProvidersByLatency = rankProvidersByLatency;
3133
+ exports.resolveAssetContract = resolveAssetContract;
3134
+ exports.resolveLogger = resolveLogger;
3135
+ exports.selectAsset = selectAsset;
2830
3136
  exports.selectMode = selectMode;
2831
3137
  exports.setNulthPaymentContext = setNulthPaymentContext;
2832
3138
  exports.usdcToMicros = usdcToMicros;
2833
3139
  exports.usdcToUnits = usdcToUnits;
2834
3140
  exports.withRetry = withRetry;
2835
3141
  exports.wrapFetchError = wrapFetchError;
3142
+ exports.wrapMppError = wrapMppError;