@routedock/routedock 0.2.0 → 0.3.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 (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-C50nyhaF.d.ts → ProviderRegistry-CA_TdREG.d.ts} +2 -2
  5. package/dist/{ProviderRegistry-BJ9oJLVz.d.cts → ProviderRegistry-D-0rqK0N.d.cts} +2 -2
  6. package/dist/{RouteDockClient-B1roCa8h.d.cts → RouteDockClient-Bh86bVly.d.ts} +23 -9
  7. package/dist/{RouteDockClient-BbkqjhMG.d.ts → RouteDockClient-go9aopfF.d.cts} +23 -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 +429 -117
  13. package/dist/client.d.cts +9 -8
  14. package/dist/client.d.ts +9 -8
  15. package/dist/client.js +421 -119
  16. package/dist/index.cjs +652 -220
  17. package/dist/index.d.cts +8 -8
  18. package/dist/index.d.ts +8 -8
  19. package/dist/index.js +643 -221
  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 +407 -117
  35. package/dist/react.d.cts +2 -2
  36. package/dist/react.d.ts +2 -2
  37. package/dist/react.js +408 -118
  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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { usdcToStroops } from '@routedock/nulth-sdk';
1
+ import { usdcToStroops, stroopsToUsdc } from '@routedock/nulth-sdk';
2
2
  export { USDC_DECIMALS, usdcToStroops } from '@routedock/nulth-sdk';
3
3
  import { Keypair, rpc, Horizon, Networks, xdr, Address, scValToNative, hash } from '@stellar/stellar-sdk';
4
4
  import { Validator } from '@cfworker/json-schema';
@@ -159,10 +159,35 @@ function wrapFetchError(err, context) {
159
159
  if (err instanceof RouteDockError) return err;
160
160
  return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
161
161
  }
162
+ function wrapMppError(err, context) {
163
+ if (err instanceof RouteDockError) return err;
164
+ if (err instanceof TypeError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
165
+ return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
166
+ }
167
+ return new RouteDockSignatureError(`${context}: ${String(err)}`, { cause: err });
168
+ }
162
169
 
163
170
  // src/types.ts
164
171
  var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
165
172
 
173
+ // src/internal/logger.ts
174
+ function createConsoleLogger() {
175
+ return (level, message, fields) => {
176
+ const values = fields ? Object.values(fields) : [];
177
+ if (values.length > 0) {
178
+ console[level](message, ...values);
179
+ } else {
180
+ console[level](message);
181
+ }
182
+ };
183
+ }
184
+ var consoleLogger = createConsoleLogger();
185
+ var noopLogger = () => {
186
+ };
187
+ function resolveLogger(logger, fallback = consoleLogger) {
188
+ return logger ?? fallback;
189
+ }
190
+
166
191
  // src/internal/retry.ts
167
192
  var DEFAULT_RETRY_POLICY = {
168
193
  maxAttempts: 4,
@@ -177,10 +202,11 @@ function sleep(ms) {
177
202
  return new Promise((resolve) => setTimeout(resolve, ms));
178
203
  }
179
204
  async function withRetry(fn, policy = {}) {
180
- const { maxAttempts, baseDelayMs, maxDelayMs, onRetry } = {
205
+ const { maxAttempts, baseDelayMs, maxDelayMs, onRetry, logger } = {
181
206
  ...DEFAULT_RETRY_POLICY,
182
207
  ...policy
183
208
  };
209
+ const log = resolveLogger(logger);
184
210
  let lastError;
185
211
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
186
212
  try {
@@ -198,7 +224,7 @@ async function withRetry(fn, policy = {}) {
198
224
  try {
199
225
  await onRetry(attempt, err, delay);
200
226
  } catch (cbErr) {
201
- console.error("[withRetry] onRetry callback threw:", cbErr);
227
+ log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
202
228
  }
203
229
  }
204
230
  await sleep(delay);
@@ -212,6 +238,84 @@ var USDC_ISSUERS = {
212
238
  };
213
239
  var usdcToUnits = usdcToStroops;
214
240
 
241
+ // src/internal/assetUtils.ts
242
+ function normalizeManifestAssets(manifest) {
243
+ if (manifest.assets && manifest.assets.length > 0) {
244
+ return manifest.assets;
245
+ }
246
+ if (manifest.asset && manifest.asset_contract) {
247
+ return [
248
+ {
249
+ asset: manifest.asset,
250
+ asset_contract: manifest.asset_contract
251
+ }
252
+ ];
253
+ }
254
+ throw new RouteDockManifestError(
255
+ "Manifest must define either assets[] or root asset/asset_contract fields"
256
+ );
257
+ }
258
+ function getEligibleAssets(manifest, mode, endpoint) {
259
+ const assets = normalizeManifestAssets(manifest);
260
+ return assets.filter((asset) => {
261
+ if (asset.modes && !asset.modes.includes(mode)) {
262
+ return false;
263
+ }
264
+ if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
265
+ const trimmedEndpoint = endpoint.replace(/^\//, "");
266
+ if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
267
+ return true;
268
+ }
269
+ if (manifest.endpoints) {
270
+ let pathname = endpoint;
271
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
272
+ try {
273
+ pathname = new URL(endpoint).pathname;
274
+ } catch {
275
+ }
276
+ }
277
+ const trimmedPathname = pathname.replace(/^\//, "");
278
+ const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
279
+ const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
280
+ const keyMatches = key === endpoint || key === trimmedEndpoint;
281
+ if (pathMatches || keyMatches) {
282
+ return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
283
+ }
284
+ return false;
285
+ });
286
+ if (!matchesEndpoint) {
287
+ return false;
288
+ }
289
+ } else {
290
+ return false;
291
+ }
292
+ }
293
+ return true;
294
+ });
295
+ }
296
+ function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
297
+ if (manifest.assets && manifest.assets.length > 0) {
298
+ return selectAsset(manifest, mode, endpoint).asset_contract;
299
+ }
300
+ return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
301
+ }
302
+ function selectAsset(manifest, mode, endpoint) {
303
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
304
+ if (eligible.length === 0) {
305
+ const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
306
+ throw new RouteDockManifestError(
307
+ `No eligible assets found for mode '${mode}'${endpointMsg}`
308
+ );
309
+ }
310
+ return eligible[0];
311
+ }
312
+ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
313
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
314
+ return eligible.some(
315
+ (asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
316
+ );
317
+ }
318
+
215
319
  // src/schemas/routedock.schema.json
216
320
  var routedock_schema_default = {
217
321
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -291,6 +395,14 @@ var routedock_schema_default = {
291
395
  type: "string",
292
396
  description: "Stellar Asset Contract (SAC) address for the payment asset"
293
397
  },
398
+ assets: {
399
+ type: "array",
400
+ minItems: 1,
401
+ items: {
402
+ $ref: "#/definitions/AssetConfig"
403
+ },
404
+ 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."
405
+ },
294
406
  payee: {
295
407
  type: "string",
296
408
  description: "Stellar address (G...) that receives payments"
@@ -423,6 +535,38 @@ var routedock_schema_default = {
423
535
  enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
424
536
  description: "A supported RouteDock payment mode"
425
537
  },
538
+ AssetConfig: {
539
+ type: "object",
540
+ description: "Payment asset configuration with optional mode and endpoint scoping",
541
+ required: ["asset", "asset_contract"],
542
+ additionalProperties: false,
543
+ properties: {
544
+ asset: {
545
+ type: "string",
546
+ description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
547
+ },
548
+ asset_contract: {
549
+ type: "string",
550
+ description: "Stellar Asset Contract (SAC) address for the payment asset"
551
+ },
552
+ modes: {
553
+ type: "array",
554
+ items: {
555
+ $ref: "#/definitions/PaymentMode"
556
+ },
557
+ uniqueItems: true,
558
+ description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
559
+ },
560
+ endpoints: {
561
+ type: "array",
562
+ items: {
563
+ type: "string"
564
+ },
565
+ uniqueItems: true,
566
+ description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
567
+ }
568
+ }
569
+ },
426
570
  PricingConfig: {
427
571
  type: "object",
428
572
  description: "Per-request pricing for x402 or mpp-charge modes",
@@ -568,7 +712,7 @@ var routedock_schema_default = {
568
712
 
569
713
  // package.json
570
714
  var package_default = {
571
- version: "0.2.0"};
715
+ version: "0.3.0"};
572
716
  var MANIFEST_SIGNATURE_VERSION = "2";
573
717
  function canonicalize(value) {
574
718
  if (Array.isArray(value)) {
@@ -682,6 +826,7 @@ function assertEndpointActive(manifest, url, logger) {
682
826
  const [key, descriptor] = deprecated;
683
827
  const sunsetSuffix = descriptor.sunset_at ? `; sunset_at ${descriptor.sunset_at}` : "";
684
828
  logger(
829
+ "warn",
685
830
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${pathname}; endpoint '${key}' is deprecated${sunsetSuffix}`
686
831
  );
687
832
  }
@@ -703,6 +848,27 @@ function assertManifestValid(manifest, baseUrl) {
703
848
  }
704
849
  }
705
850
  }
851
+ if (manifest.assets !== void 0) {
852
+ if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
853
+ throw new RouteDockManifestError(
854
+ `Invalid manifest${context}: assets must be a non-empty array when defined`
855
+ );
856
+ }
857
+ for (let i = 0; i < manifest.assets.length; i++) {
858
+ const a = manifest.assets[i];
859
+ if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
860
+ throw new RouteDockManifestError(
861
+ `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
862
+ );
863
+ }
864
+ }
865
+ const first = manifest.assets[0];
866
+ if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
867
+ throw new RouteDockManifestError(
868
+ `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
869
+ );
870
+ }
871
+ }
706
872
  }
707
873
  var CACHE_TTL_MS = 6e4;
708
874
  var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
@@ -882,16 +1048,16 @@ function logSelection(manifest, selection, log, deprecated) {
882
1048
  const reason = selection.reason ? ` (${selection.reason})` : "";
883
1049
  if (deprecated) {
884
1050
  log(
1051
+ "warn",
885
1052
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${selection.mode}${reason}; selected deprecated mode because no active supported mode is available`
886
1053
  );
887
1054
  return;
888
1055
  }
889
- log(`[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
1056
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
890
1057
  }
891
1058
  function selectMode(manifest, options = {}) {
892
1059
  const modes = manifest.modes;
893
- const log = options.logger ?? (() => {
894
- });
1060
+ const log = options.logger ?? noopLogger;
895
1061
  const deprecatedSet = new Set(manifest.deprecated_modes ?? []);
896
1062
  if (options.forceMode) {
897
1063
  if (!modes.includes(options.forceMode)) {
@@ -901,10 +1067,11 @@ function selectMode(manifest, options = {}) {
901
1067
  }
902
1068
  if (deprecatedSet.has(options.forceMode)) {
903
1069
  log(
1070
+ "warn",
904
1071
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${options.forceMode} (forced); selected mode is deprecated`
905
1072
  );
906
1073
  } else {
907
- log(`[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
1074
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
908
1075
  }
909
1076
  return options.forceMode;
910
1077
  }
@@ -940,6 +1107,69 @@ function rankProvidersByLatency(manifests, targetRegion) {
940
1107
  return 0;
941
1108
  });
942
1109
  }
1110
+
1111
+ // src/internal/payee.ts
1112
+ function resolvePayee(manifest, mode) {
1113
+ const override = manifest.pricing[mode]?.payee;
1114
+ return override ?? manifest.payee;
1115
+ }
1116
+
1117
+ // src/client/challenge.ts
1118
+ function isRecord(value) {
1119
+ return typeof value === "object" && value !== null;
1120
+ }
1121
+ function isPositiveIntegerString(value) {
1122
+ return typeof value === "string" && /^[1-9]\d*$/.test(value);
1123
+ }
1124
+ function checkAmount(amount, maxAmount) {
1125
+ if (!isPositiveIntegerString(amount)) {
1126
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1127
+ }
1128
+ let max;
1129
+ try {
1130
+ max = usdcToStroops(maxAmount);
1131
+ } catch {
1132
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1133
+ }
1134
+ if (BigInt(amount) > max) {
1135
+ return new RouteDockPolicyRejectError("challenge_amount_exceeds_manifest");
1136
+ }
1137
+ return null;
1138
+ }
1139
+ function checkX402Accept(accept, manifest, network, maxAmount) {
1140
+ if (accept.network !== network) {
1141
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1142
+ }
1143
+ if (accept.payTo !== resolvePayee(manifest, "x402")) {
1144
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1145
+ }
1146
+ if (accept.asset !== manifest.asset_contract) {
1147
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1148
+ }
1149
+ return checkAmount(accept.amount, maxAmount);
1150
+ }
1151
+ function filterX402Accepts(accepts, manifest, network, maxAmount) {
1152
+ return accepts.filter((accept) => checkX402Accept(accept, manifest, network, maxAmount) === null);
1153
+ }
1154
+ function checkChargeChallenge(request, manifest, network, maxAmount) {
1155
+ const methodDetails = request["methodDetails"];
1156
+ const rawNetwork = isRecord(methodDetails) ? methodDetails["network"] : void 0;
1157
+ const challengeNetwork = rawNetwork === void 0 ? "stellar:testnet" : rawNetwork;
1158
+ if (challengeNetwork !== network) {
1159
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1160
+ }
1161
+ const recipient = request["recipient"];
1162
+ if (typeof recipient !== "string" || recipient !== resolvePayee(manifest, "mpp-charge")) {
1163
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1164
+ }
1165
+ const currency = request["currency"];
1166
+ if (typeof currency !== "string" || currency !== manifest.asset_contract) {
1167
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1168
+ }
1169
+ return checkAmount(request["amount"], maxAmount);
1170
+ }
1171
+
1172
+ // src/client/x402Client.ts
943
1173
  var CAIP2 = {
944
1174
  testnet: "stellar:testnet",
945
1175
  mainnet: "stellar:pubnet"
@@ -971,83 +1201,106 @@ var X402Client = class _X402Client {
971
1201
  if (!pricing.facilitator) {
972
1202
  throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
973
1203
  }
974
- return withRetry(async () => {
975
- let init;
1204
+ const caip2 = CAIP2[this.network];
1205
+ const probe = await withRetry(async () => {
1206
+ let init2;
976
1207
  try {
977
- init = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
1208
+ init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
978
1209
  } catch (err) {
979
1210
  throw wrapFetchError(err, "x402 initial request");
980
1211
  }
981
- if (init.status !== 402) {
982
- if (!init.ok) {
1212
+ if (init2.status !== 402) {
1213
+ if (!init2.ok) {
983
1214
  throw httpStatusToError(
984
- `x402 initial request failed: HTTP ${init.status}`,
985
- init.status,
986
- init
1215
+ `x402 initial request failed: HTTP ${init2.status}`,
1216
+ init2.status,
1217
+ init2
987
1218
  );
988
1219
  }
989
1220
  let data2;
990
1221
  try {
991
- data2 = await init.json();
1222
+ data2 = await init2.json();
992
1223
  } catch {
993
- throw new RouteDockManifestError(`Failed to parse JSON from non-402 response (HTTP ${init.status})`);
1224
+ throw new RouteDockManifestError(
1225
+ `Failed to parse JSON from non-402 response (HTTP ${init2.status})`
1226
+ );
994
1227
  }
995
- return { data: data2, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
996
- }
997
- const reqHeader = init.headers.get("X-Payment-Requirements");
998
- if (!reqHeader) {
999
- throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1000
- }
1001
- let paymentRequired;
1002
- try {
1003
- paymentRequired = decodePaymentRequiredHeader(reqHeader);
1004
- } catch (err) {
1005
- throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
1006
- cause: err
1007
- });
1228
+ return { kind: "free", data: data2 };
1008
1229
  }
1009
- let paymentPayload;
1010
- try {
1011
- paymentPayload = await this.httpClient.createPaymentPayload(paymentRequired);
1012
- } catch (err) {
1013
- throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
1014
- cause: err
1015
- });
1016
- }
1017
- const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1018
- let settled;
1230
+ return { kind: "payment-required", init: init2 };
1231
+ }, this.retryPolicy);
1232
+ if (probe.kind === "free") {
1233
+ return { data: probe.data, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
1234
+ }
1235
+ const init = probe.init;
1236
+ const reqHeader = init.headers.get("X-Payment-Requirements");
1237
+ if (!reqHeader) {
1238
+ throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1239
+ }
1240
+ let paymentRequired;
1241
+ try {
1242
+ paymentRequired = decodePaymentRequiredHeader(reqHeader);
1243
+ } catch (err) {
1244
+ throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
1245
+ cause: err
1246
+ });
1247
+ }
1248
+ const firstAccept = paymentRequired.accepts[0];
1249
+ if (!firstAccept) {
1250
+ throw new RouteDockManifestError("402 response carries no payment accepts");
1251
+ }
1252
+ const allowed = filterX402Accepts(paymentRequired.accepts, manifest, caip2, pricing.amount);
1253
+ if (allowed.length === 0) {
1254
+ throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
1255
+ }
1256
+ let paymentPayload;
1257
+ try {
1258
+ paymentPayload = await this.httpClient.createPaymentPayload({
1259
+ ...paymentRequired,
1260
+ accepts: allowed
1261
+ });
1262
+ } catch (err) {
1263
+ throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
1264
+ cause: err
1265
+ });
1266
+ }
1267
+ const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1268
+ const signedAmount = stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
1269
+ const settled = await withRetry(async () => {
1270
+ let response;
1019
1271
  try {
1020
- settled = await fetch(url, { headers: paymentHeaders });
1272
+ response = await fetch(url, { headers: paymentHeaders });
1021
1273
  } catch (err) {
1022
1274
  throw wrapFetchError(err, "x402 settlement request");
1023
1275
  }
1024
- if (!settled.ok) {
1025
- if (settled.status >= 500 || settled.status === 429 || settled.status === 503) {
1276
+ if (!response.ok) {
1277
+ if (response.status >= 500 || response.status === 429 || response.status === 503) {
1026
1278
  throw httpStatusToError(
1027
- `x402 payment failed: HTTP ${settled.status}`,
1028
- settled.status,
1029
- settled
1279
+ `x402 payment failed: HTTP ${response.status}`,
1280
+ response.status,
1281
+ response
1030
1282
  );
1031
1283
  }
1032
- throw new RouteDockManifestError(`x402 payment failed: HTTP ${settled.status}`);
1033
- }
1034
- let txHash = null;
1035
- const responseHeader = settled.headers.get("X-Payment-Response");
1036
- if (responseHeader) {
1037
- try {
1038
- const settleResponse = decodePaymentResponseHeader(responseHeader);
1039
- txHash = settleResponse.transaction ?? null;
1040
- } catch {
1041
- }
1284
+ throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
1042
1285
  }
1043
- let data;
1286
+ return response;
1287
+ }, this.retryPolicy);
1288
+ let txHash = null;
1289
+ const responseHeader = settled.headers.get("X-Payment-Response");
1290
+ if (responseHeader) {
1044
1291
  try {
1045
- data = await settled.json();
1292
+ const settleResponse = decodePaymentResponseHeader(responseHeader);
1293
+ txHash = settleResponse.transaction ?? null;
1046
1294
  } catch {
1047
- throw new RouteDockManifestError("Failed to parse JSON from settled response");
1048
1295
  }
1049
- return { data, txHash, mode: "x402", amount: pricing.amount, timestamp: Date.now() };
1050
- }, this.retryPolicy);
1296
+ }
1297
+ let data;
1298
+ try {
1299
+ data = await settled.json();
1300
+ } catch {
1301
+ throw new RouteDockManifestError("Failed to parse JSON from settled response");
1302
+ }
1303
+ return { data, txHash, mode: "x402", amount: signedAmount, timestamp: Date.now() };
1051
1304
  }
1052
1305
  };
1053
1306
  var MppChargeClient = class {
@@ -1064,27 +1317,42 @@ var MppChargeClient = class {
1064
1317
  if (!pricing) {
1065
1318
  throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
1066
1319
  }
1067
- return withRetry(async () => {
1068
- let txHash = null;
1069
- const mppx = Mppx.create({
1070
- polyfill: false,
1071
- methods: [
1072
- stellar.charge({
1073
- keypair: this.keypair,
1074
- mode: "pull",
1075
- onProgress(event) {
1076
- if (event.type === "paid") {
1077
- txHash = event.hash;
1078
- }
1320
+ const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
1321
+ let txHash = null;
1322
+ let signedAmount;
1323
+ let credential;
1324
+ const mppx = Mppx.create({
1325
+ polyfill: false,
1326
+ methods: [
1327
+ stellar.charge({
1328
+ keypair: this.keypair,
1329
+ mode: "pull",
1330
+ onProgress(event) {
1331
+ if (event.type === "paid") {
1332
+ txHash = event.hash;
1079
1333
  }
1080
- })
1081
- ]
1082
- });
1334
+ }
1335
+ })
1336
+ ],
1337
+ onChallenge: async (challenge, { createCredential }) => {
1338
+ const request = challenge.request;
1339
+ const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
1340
+ if (rejection) throw rejection;
1341
+ const amount = request["amount"];
1342
+ if (typeof amount !== "string") {
1343
+ throw new RouteDockPolicyRejectError("challenge_amount_invalid");
1344
+ }
1345
+ signedAmount = stroopsToUsdc(BigInt(amount));
1346
+ credential ??= await createCredential();
1347
+ return credential;
1348
+ }
1349
+ });
1350
+ return withRetry(async () => {
1083
1351
  let response;
1084
1352
  try {
1085
- response = await mppx.fetch(url);
1353
+ response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
1086
1354
  } catch (err) {
1087
- throw wrapFetchError(err, "MPP charge request");
1355
+ throw wrapMppError(err, "MPP charge request");
1088
1356
  }
1089
1357
  if (!response.ok) {
1090
1358
  if (response.status >= 500 || response.status === 429 || response.status === 503) {
@@ -1105,7 +1373,13 @@ var MppChargeClient = class {
1105
1373
  { cause }
1106
1374
  );
1107
1375
  }
1108
- return { data, txHash, mode: "mpp-charge", amount: pricing.amount, timestamp: Date.now() };
1376
+ return {
1377
+ data,
1378
+ txHash,
1379
+ mode: "mpp-charge",
1380
+ amount: signedAmount ?? pricing.amount,
1381
+ timestamp: Date.now()
1382
+ };
1109
1383
  }, this.retryPolicy);
1110
1384
  }
1111
1385
  };
@@ -1144,16 +1418,18 @@ function resolveStoreNetworkId(network) {
1144
1418
  return network === STELLAR_PUBNET ? STELLAR_PUBNET : STELLAR_TESTNET;
1145
1419
  }
1146
1420
  var MppSessionClient = class {
1147
- constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory) {
1421
+ constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory, logger = consoleLogger) {
1148
1422
  this.keypair = keypair;
1149
1423
  this.network = network;
1150
1424
  this.retryPolicy = retryPolicy;
1151
1425
  this.webSocketFactory = webSocketFactory;
1426
+ this.logger = logger;
1152
1427
  }
1153
1428
  keypair;
1154
1429
  network;
1155
1430
  retryPolicy;
1156
1431
  webSocketFactory;
1432
+ logger;
1157
1433
  async openSession(url, manifest, commitmentSecret, options, onSpend) {
1158
1434
  const mode = options?.mode ?? "mpp-session";
1159
1435
  const pricing = manifest.pricing[mode];
@@ -1378,7 +1654,7 @@ var MppSessionClient = class {
1378
1654
  if (abortController.signal.aborted) {
1379
1655
  throw err;
1380
1656
  }
1381
- throw wrapFetchError(err, "Voucher request");
1657
+ throw wrapMppError(err, "Voucher request");
1382
1658
  }
1383
1659
  if (!resp.ok) {
1384
1660
  if (resp.status >= 500 || resp.status === 429 || resp.status === 503) {
@@ -1671,9 +1947,10 @@ var MppSessionClient = class {
1671
1947
  emit("session:timeout", { maxDurationMs });
1672
1948
  void handle.close().catch((error) => {
1673
1949
  emit("session:close-failed", { maxDurationMs, error });
1674
- console.warn(
1950
+ this.logger(
1951
+ "warn",
1675
1952
  `RouteDock: maxDuration auto-close failed after ${maxDurationMs}ms \u2014 the channel may still hold collateral; retry close() or call requestRefund().`,
1676
- error
1953
+ { error }
1677
1954
  );
1678
1955
  });
1679
1956
  }, maxDurationMs);
@@ -1723,7 +2000,7 @@ var MppSessionClient = class {
1723
2000
  );
1724
2001
  onSigned?.();
1725
2002
  } catch (err) {
1726
- throw wrapFetchError(err, "Voucher credential");
2003
+ throw wrapMppError(err, "Voucher credential");
1727
2004
  }
1728
2005
  const wsUrl = url.replace(/^http/, "ws");
1729
2006
  const queue = [];
@@ -1920,7 +2197,10 @@ var NulthClient = class {
1920
2197
  `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
1921
2198
  );
1922
2199
  }
1923
- console.warn("NulthClient is using the insecure mock prover; proofs are not cryptographically sound");
2200
+ resolveLogger(config.logger)(
2201
+ "warn",
2202
+ "NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
2203
+ );
1924
2204
  }
1925
2205
  config;
1926
2206
  policy;
@@ -1972,6 +2252,12 @@ var NulthClient = class {
1972
2252
  return proof;
1973
2253
  }
1974
2254
  enforcePolicy(context) {
2255
+ if (context.amountStroops < 0n) {
2256
+ throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
2257
+ }
2258
+ if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
2259
+ throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
2260
+ }
1975
2261
  if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
1976
2262
  throw new NulthPolicyError("session_expired", "Nulth session expired");
1977
2263
  }
@@ -2115,7 +2401,8 @@ var InMemorySpendStore = class {
2115
2401
  state = null;
2116
2402
  constructor(options = {}) {
2117
2403
  if (options.warn !== false) {
2118
- console.warn(
2404
+ resolveLogger(options.logger)(
2405
+ "warn",
2119
2406
  "[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."
2120
2407
  );
2121
2408
  }
@@ -2205,7 +2492,10 @@ var RouteDockClient = class _RouteDockClient {
2205
2492
  this.network = config.network;
2206
2493
  this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
2207
2494
  this.retryPolicy = config.retryPolicy;
2208
- this.spendStore = config.spendStore ?? new InMemorySpendStore({ warn: !!config.spendCap });
2495
+ this.spendStore = config.spendStore ?? new InMemorySpendStore({
2496
+ warn: !!config.spendCap,
2497
+ ...config.logger && { logger: config.logger }
2498
+ });
2209
2499
  this.logger = config.logger;
2210
2500
  this.manifestTimeoutMs = config.manifestTimeoutMs;
2211
2501
  this.expectedPayee = config.expectedPayee;
@@ -2217,9 +2507,10 @@ var RouteDockClient = class _RouteDockClient {
2217
2507
  _secrets.set(this, config.commitmentSecret);
2218
2508
  }
2219
2509
  const secretKey = this.keypair.secret();
2220
- this.x402 = new X402Client(secretKey, this.network, this.retryPolicy);
2221
- this.charge = new MppChargeClient(this.keypair, this.network, this.retryPolicy);
2222
- this.session = new MppSessionClient(this.keypair, this.network, this.retryPolicy);
2510
+ const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
2511
+ this.x402 = new X402Client(secretKey, this.network, retryPolicy);
2512
+ this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
2513
+ this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
2223
2514
  }
2224
2515
  /** Fetch manifest and select mode — shared by pay() and estimateCost(). */
2225
2516
  async _resolveManifest(url, options) {
@@ -2248,12 +2539,13 @@ var RouteDockClient = class _RouteDockClient {
2248
2539
  * call before committing to a payment — for approval gates and manual
2249
2540
  * trustline remediation.
2250
2541
  */
2251
- async preflight(manifest) {
2542
+ async preflight(manifest, mode, endpoint) {
2252
2543
  assertManifestValid(manifest);
2253
- await this._checkTrustline(manifest);
2544
+ const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
2545
+ await this._checkTrustline(manifest, selectedAsset);
2254
2546
  return {
2255
2547
  hasTrustline: true,
2256
- asset: manifest.asset,
2548
+ asset: selectedAsset,
2257
2549
  modes: manifest.modes,
2258
2550
  ...manifest.regions && { regions: manifest.regions },
2259
2551
  ...manifest.latency_hints && { latency_hints: manifest.latency_hints },
@@ -2267,8 +2559,8 @@ var RouteDockClient = class _RouteDockClient {
2267
2559
  * trustline is missing. Results are cached per (account, asset) so the
2268
2560
  * per-payment overhead is one RPC call amortized to zero after the first.
2269
2561
  */
2270
- async _checkTrustline(manifest) {
2271
- const cacheKey = `${this.network}:${this.keypair.publicKey()}:${manifest.asset}`;
2562
+ async _checkTrustline(manifest, targetAsset = manifest.asset) {
2563
+ const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
2272
2564
  const cached = _RouteDockClient._trustlineCache.get(cacheKey);
2273
2565
  if (cached && Date.now() < cached.expiresAt) return;
2274
2566
  const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
@@ -2276,13 +2568,13 @@ var RouteDockClient = class _RouteDockClient {
2276
2568
  try {
2277
2569
  const account = await server.loadAccount(this.keypair.publicKey());
2278
2570
  const balances = account.balances;
2279
- const expectedIssuer = getAssetIssuer(manifest.asset, this.network);
2571
+ const expectedIssuer = getAssetIssuer(targetAsset, this.network);
2280
2572
  const hasTrustline = balances.some(
2281
- (b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code === manifest.asset && (!expectedIssuer || b.asset_issuer === expectedIssuer)
2573
+ (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))
2282
2574
  );
2283
2575
  if (!hasTrustline) {
2284
- 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}`;
2285
- throw new RouteDockTrustlineError(manifest.asset, expectedIssuer || "unknown", remediation);
2576
+ 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}`;
2577
+ throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
2286
2578
  }
2287
2579
  _RouteDockClient._trustlineCache.set(cacheKey, {
2288
2580
  exists: true,
@@ -2291,7 +2583,8 @@ var RouteDockClient = class _RouteDockClient {
2291
2583
  } catch (err) {
2292
2584
  if (err instanceof RouteDockTrustlineError) throw err;
2293
2585
  this.logger?.(
2294
- `[RouteDock] Trustline preflight: could not verify trustline for ${manifest.asset} \u2014 continuing`
2586
+ "warn",
2587
+ `[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
2295
2588
  );
2296
2589
  }
2297
2590
  }
@@ -2299,18 +2592,18 @@ var RouteDockClient = class _RouteDockClient {
2299
2592
  * Pay for one request at `url`. Fetches manifest, selects payment mode,
2300
2593
  * runs trustline preflight, reserves local spend cap BEFORE executing the
2301
2594
  * payment, then commits the spend record on success. Rolls back the
2302
- * reservation if the on-chain payment fails. Concurrent pay() calls are
2303
- * serialized through a per-instance mutex to prevent spend-cap overrun.
2595
+ * reservation if the on-chain payment fails, and also when the call
2596
+ * completes without paying anything (an x402 endpoint answering a plain
2597
+ * 200 with no challenge). Concurrent pay() calls are serialized through a
2598
+ * per-instance mutex to prevent spend-cap overrun.
2304
2599
  */
2305
2600
  async pay(url, options) {
2306
2601
  const baseUrl = new URL(url).origin;
2307
2602
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2308
2603
  assertEndpointActive(manifest, url, this.logger);
2309
2604
  const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
2310
- await this._checkTrustline(manifest);
2311
- if (this.vault?.mode === "nulth") {
2312
- return this._payWithNulthVault(url, manifest, mode);
2313
- }
2605
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2606
+ await this._checkTrustline(manifest, selectedAsset);
2314
2607
  let amount;
2315
2608
  switch (mode) {
2316
2609
  case "x402":
@@ -2330,22 +2623,30 @@ var RouteDockClient = class _RouteDockClient {
2330
2623
  const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
2331
2624
  let result;
2332
2625
  try {
2333
- switch (mode) {
2334
- case "x402":
2335
- result = await this.x402.pay(url, manifest);
2336
- break;
2337
- case "mpp-charge":
2338
- result = await this.charge.pay(url, manifest);
2339
- break;
2340
- default:
2341
- throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2626
+ if (this.vault?.mode === "nulth") {
2627
+ result = await this._payWithNulthVault(url, manifest, mode);
2628
+ } else {
2629
+ switch (mode) {
2630
+ case "x402":
2631
+ result = await this.x402.pay(url, manifest);
2632
+ break;
2633
+ case "mpp-charge":
2634
+ result = await this.charge.pay(url, manifest);
2635
+ break;
2636
+ default:
2637
+ throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2638
+ }
2342
2639
  }
2343
2640
  } catch (err) {
2344
2641
  await this._rollbackSpend(reserveId).catch(() => {
2345
2642
  });
2346
2643
  throw err;
2347
2644
  }
2348
- await this._commitSpend(reserveId);
2645
+ if (result.txHash === null && result.amount === "0") {
2646
+ await this._rollbackSpend(reserveId);
2647
+ } else {
2648
+ await this._commitSpend(reserveId);
2649
+ }
2349
2650
  return result;
2350
2651
  }
2351
2652
  /** Nulth ZK vault path — proof built off-chain, attached as auth signature */
@@ -2395,9 +2696,10 @@ var RouteDockClient = class _RouteDockClient {
2395
2696
  default:
2396
2697
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2397
2698
  }
2699
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2398
2700
  return {
2399
2701
  amount,
2400
- asset: manifest.asset,
2702
+ asset: selectedAsset,
2401
2703
  mode,
2402
2704
  manifest,
2403
2705
  ...manifest.regions && { regions: manifest.regions },
@@ -2768,4 +3070,4 @@ var ProviderRegistry = class {
2768
3070
  }
2769
3071
  };
2770
3072
 
2771
- export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient, MppSessionClient, NulthPolicyError, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSessionStore, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, createNulthSigner, createPolicyState, decodeAuthSignature, fetchLedgerSequence, fetchManifest, httpStatusToError, invalidateManifest, parseRetryAfterMs, paymentContextFromManifest, prepareNulthSigner, rankProvidersByLatency, selectMode, setNulthPaymentContext, usdcToMicros, usdcToUnits, withRetry, wrapFetchError };
3073
+ export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient, MppSessionClient, NulthPolicyError, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSessionStore, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, consoleLogger, createConsoleLogger, createNulthSigner, createPolicyState, decodeAuthSignature, fetchLedgerSequence, fetchManifest, getEligibleAssets, httpStatusToError, invalidateManifest, isAssetEligible, noopLogger, normalizeManifestAssets, parseRetryAfterMs, paymentContextFromManifest, prepareNulthSigner, rankProvidersByLatency, resolveAssetContract, resolveLogger, selectAsset, selectMode, setNulthPaymentContext, usdcToMicros, usdcToUnits, withRetry, wrapFetchError, wrapMppError };