@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/index.cjs CHANGED
@@ -18,9 +18,9 @@ var path = require('path');
18
18
  var facilitator = require('@x402/stellar/exact/facilitator');
19
19
  var server$1 = require('@x402/stellar/exact/server');
20
20
  var server = require('@x402/core/server');
21
- var server$3 = require('@stellar/mpp/charge/server');
21
+ var server$4 = require('@stellar/mpp/charge/server');
22
22
  var server$2 = require('mppx/server');
23
- var server$4 = require('@stellar/mpp/channel/server');
23
+ var server$3 = require('@stellar/mpp/channel/server');
24
24
 
25
25
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
26
26
 
@@ -171,10 +171,35 @@ function wrapFetchError(err, context) {
171
171
  if (err instanceof RouteDockError) return err;
172
172
  return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
173
173
  }
174
+ function wrapMppError(err, context) {
175
+ if (err instanceof RouteDockError) return err;
176
+ if (err instanceof TypeError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
177
+ return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
178
+ }
179
+ return new RouteDockSignatureError(`${context}: ${String(err)}`, { cause: err });
180
+ }
174
181
 
175
182
  // src/types.ts
176
183
  var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
177
184
 
185
+ // src/internal/logger.ts
186
+ function createConsoleLogger() {
187
+ return (level, message, fields) => {
188
+ const values = fields ? Object.values(fields) : [];
189
+ if (values.length > 0) {
190
+ console[level](message, ...values);
191
+ } else {
192
+ console[level](message);
193
+ }
194
+ };
195
+ }
196
+ var consoleLogger = createConsoleLogger();
197
+ var noopLogger = () => {
198
+ };
199
+ function resolveLogger(logger, fallback = consoleLogger) {
200
+ return logger ?? fallback;
201
+ }
202
+
178
203
  // src/internal/retry.ts
179
204
  var DEFAULT_RETRY_POLICY = {
180
205
  maxAttempts: 4,
@@ -189,10 +214,11 @@ function sleep(ms) {
189
214
  return new Promise((resolve) => setTimeout(resolve, ms));
190
215
  }
191
216
  async function withRetry(fn, policy = {}) {
192
- const { maxAttempts, baseDelayMs, maxDelayMs, onRetry } = {
217
+ const { maxAttempts, baseDelayMs, maxDelayMs, onRetry, logger } = {
193
218
  ...DEFAULT_RETRY_POLICY,
194
219
  ...policy
195
220
  };
221
+ const log = resolveLogger(logger);
196
222
  let lastError;
197
223
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
198
224
  try {
@@ -210,7 +236,7 @@ async function withRetry(fn, policy = {}) {
210
236
  try {
211
237
  await onRetry(attempt, err, delay);
212
238
  } catch (cbErr) {
213
- console.error("[withRetry] onRetry callback threw:", cbErr);
239
+ log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
214
240
  }
215
241
  }
216
242
  await sleep(delay);
@@ -224,6 +250,84 @@ var USDC_ISSUERS = {
224
250
  };
225
251
  var usdcToUnits = nulthSdk.usdcToStroops;
226
252
 
253
+ // src/internal/assetUtils.ts
254
+ function normalizeManifestAssets(manifest) {
255
+ if (manifest.assets && manifest.assets.length > 0) {
256
+ return manifest.assets;
257
+ }
258
+ if (manifest.asset && manifest.asset_contract) {
259
+ return [
260
+ {
261
+ asset: manifest.asset,
262
+ asset_contract: manifest.asset_contract
263
+ }
264
+ ];
265
+ }
266
+ throw new RouteDockManifestError(
267
+ "Manifest must define either assets[] or root asset/asset_contract fields"
268
+ );
269
+ }
270
+ function getEligibleAssets(manifest, mode, endpoint) {
271
+ const assets = normalizeManifestAssets(manifest);
272
+ return assets.filter((asset) => {
273
+ if (asset.modes && !asset.modes.includes(mode)) {
274
+ return false;
275
+ }
276
+ if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
277
+ const trimmedEndpoint = endpoint.replace(/^\//, "");
278
+ if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
279
+ return true;
280
+ }
281
+ if (manifest.endpoints) {
282
+ let pathname = endpoint;
283
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
284
+ try {
285
+ pathname = new URL(endpoint).pathname;
286
+ } catch {
287
+ }
288
+ }
289
+ const trimmedPathname = pathname.replace(/^\//, "");
290
+ const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
291
+ const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
292
+ const keyMatches = key === endpoint || key === trimmedEndpoint;
293
+ if (pathMatches || keyMatches) {
294
+ return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
295
+ }
296
+ return false;
297
+ });
298
+ if (!matchesEndpoint) {
299
+ return false;
300
+ }
301
+ } else {
302
+ return false;
303
+ }
304
+ }
305
+ return true;
306
+ });
307
+ }
308
+ function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
309
+ if (manifest.assets && manifest.assets.length > 0) {
310
+ return selectAsset(manifest, mode, endpoint).asset_contract;
311
+ }
312
+ return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
313
+ }
314
+ function selectAsset(manifest, mode, endpoint) {
315
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
316
+ if (eligible.length === 0) {
317
+ const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
318
+ throw new RouteDockManifestError(
319
+ `No eligible assets found for mode '${mode}'${endpointMsg}`
320
+ );
321
+ }
322
+ return eligible[0];
323
+ }
324
+ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
325
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
326
+ return eligible.some(
327
+ (asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
328
+ );
329
+ }
330
+
227
331
  // src/schemas/routedock.schema.json
228
332
  var routedock_schema_default = {
229
333
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -303,6 +407,14 @@ var routedock_schema_default = {
303
407
  type: "string",
304
408
  description: "Stellar Asset Contract (SAC) address for the payment asset"
305
409
  },
410
+ assets: {
411
+ type: "array",
412
+ minItems: 1,
413
+ items: {
414
+ $ref: "#/definitions/AssetConfig"
415
+ },
416
+ 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."
417
+ },
306
418
  payee: {
307
419
  type: "string",
308
420
  description: "Stellar address (G...) that receives payments"
@@ -435,6 +547,38 @@ var routedock_schema_default = {
435
547
  enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
436
548
  description: "A supported RouteDock payment mode"
437
549
  },
550
+ AssetConfig: {
551
+ type: "object",
552
+ description: "Payment asset configuration with optional mode and endpoint scoping",
553
+ required: ["asset", "asset_contract"],
554
+ additionalProperties: false,
555
+ properties: {
556
+ asset: {
557
+ type: "string",
558
+ description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
559
+ },
560
+ asset_contract: {
561
+ type: "string",
562
+ description: "Stellar Asset Contract (SAC) address for the payment asset"
563
+ },
564
+ modes: {
565
+ type: "array",
566
+ items: {
567
+ $ref: "#/definitions/PaymentMode"
568
+ },
569
+ uniqueItems: true,
570
+ description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
571
+ },
572
+ endpoints: {
573
+ type: "array",
574
+ items: {
575
+ type: "string"
576
+ },
577
+ uniqueItems: true,
578
+ description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
579
+ }
580
+ }
581
+ },
438
582
  PricingConfig: {
439
583
  type: "object",
440
584
  description: "Per-request pricing for x402 or mpp-charge modes",
@@ -580,7 +724,7 @@ var routedock_schema_default = {
580
724
 
581
725
  // package.json
582
726
  var package_default = {
583
- version: "0.2.0"};
727
+ version: "0.3.1"};
584
728
  var MANIFEST_SIGNATURE_VERSION = "2";
585
729
  function canonicalize(value) {
586
730
  if (Array.isArray(value)) {
@@ -704,6 +848,7 @@ function assertEndpointActive(manifest, url, logger) {
704
848
  const [key, descriptor] = deprecated;
705
849
  const sunsetSuffix = descriptor.sunset_at ? `; sunset_at ${descriptor.sunset_at}` : "";
706
850
  logger(
851
+ "warn",
707
852
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${pathname}; endpoint '${key}' is deprecated${sunsetSuffix}`
708
853
  );
709
854
  }
@@ -725,6 +870,27 @@ function assertManifestValid(manifest, baseUrl) {
725
870
  }
726
871
  }
727
872
  }
873
+ if (manifest.assets !== void 0) {
874
+ if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
875
+ throw new RouteDockManifestError(
876
+ `Invalid manifest${context}: assets must be a non-empty array when defined`
877
+ );
878
+ }
879
+ for (let i = 0; i < manifest.assets.length; i++) {
880
+ const a = manifest.assets[i];
881
+ if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
882
+ throw new RouteDockManifestError(
883
+ `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
884
+ );
885
+ }
886
+ }
887
+ const first = manifest.assets[0];
888
+ if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
889
+ throw new RouteDockManifestError(
890
+ `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
891
+ );
892
+ }
893
+ }
728
894
  }
729
895
  var CACHE_TTL_MS = 6e4;
730
896
  var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
@@ -904,16 +1070,16 @@ function logSelection(manifest, selection, log, deprecated) {
904
1070
  const reason = selection.reason ? ` (${selection.reason})` : "";
905
1071
  if (deprecated) {
906
1072
  log(
1073
+ "warn",
907
1074
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${selection.mode}${reason}; selected deprecated mode because no active supported mode is available`
908
1075
  );
909
1076
  return;
910
1077
  }
911
- log(`[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
1078
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
912
1079
  }
913
1080
  function selectMode(manifest, options = {}) {
914
1081
  const modes = manifest.modes;
915
- const log = options.logger ?? (() => {
916
- });
1082
+ const log = options.logger ?? noopLogger;
917
1083
  const deprecatedSet = new Set(manifest.deprecated_modes ?? []);
918
1084
  if (options.forceMode) {
919
1085
  if (!modes.includes(options.forceMode)) {
@@ -923,10 +1089,11 @@ function selectMode(manifest, options = {}) {
923
1089
  }
924
1090
  if (deprecatedSet.has(options.forceMode)) {
925
1091
  log(
1092
+ "warn",
926
1093
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${options.forceMode} (forced); selected mode is deprecated`
927
1094
  );
928
1095
  } else {
929
- log(`[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
1096
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
930
1097
  }
931
1098
  return options.forceMode;
932
1099
  }
@@ -962,6 +1129,69 @@ function rankProvidersByLatency(manifests, targetRegion) {
962
1129
  return 0;
963
1130
  });
964
1131
  }
1132
+
1133
+ // src/internal/payee.ts
1134
+ function resolvePayee(manifest, mode) {
1135
+ const override = manifest.pricing[mode]?.payee;
1136
+ return override ?? manifest.payee;
1137
+ }
1138
+
1139
+ // src/client/challenge.ts
1140
+ function isRecord(value) {
1141
+ return typeof value === "object" && value !== null;
1142
+ }
1143
+ function isPositiveIntegerString(value) {
1144
+ return typeof value === "string" && /^[1-9]\d*$/.test(value);
1145
+ }
1146
+ function checkAmount(amount, maxAmount) {
1147
+ if (!isPositiveIntegerString(amount)) {
1148
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1149
+ }
1150
+ let max;
1151
+ try {
1152
+ max = nulthSdk.usdcToStroops(maxAmount);
1153
+ } catch {
1154
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1155
+ }
1156
+ if (BigInt(amount) > max) {
1157
+ return new RouteDockPolicyRejectError("challenge_amount_exceeds_manifest");
1158
+ }
1159
+ return null;
1160
+ }
1161
+ function checkX402Accept(accept, manifest, network, maxAmount) {
1162
+ if (accept.network !== network) {
1163
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1164
+ }
1165
+ if (accept.payTo !== resolvePayee(manifest, "x402")) {
1166
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1167
+ }
1168
+ if (accept.asset !== manifest.asset_contract) {
1169
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1170
+ }
1171
+ return checkAmount(accept.amount, maxAmount);
1172
+ }
1173
+ function filterX402Accepts(accepts, manifest, network, maxAmount) {
1174
+ return accepts.filter((accept) => checkX402Accept(accept, manifest, network, maxAmount) === null);
1175
+ }
1176
+ function checkChargeChallenge(request, manifest, network, maxAmount) {
1177
+ const methodDetails = request["methodDetails"];
1178
+ const rawNetwork = isRecord(methodDetails) ? methodDetails["network"] : void 0;
1179
+ const challengeNetwork = rawNetwork === void 0 ? "stellar:testnet" : rawNetwork;
1180
+ if (challengeNetwork !== network) {
1181
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1182
+ }
1183
+ const recipient = request["recipient"];
1184
+ if (typeof recipient !== "string" || recipient !== resolvePayee(manifest, "mpp-charge")) {
1185
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1186
+ }
1187
+ const currency = request["currency"];
1188
+ if (typeof currency !== "string" || currency !== manifest.asset_contract) {
1189
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1190
+ }
1191
+ return checkAmount(request["amount"], maxAmount);
1192
+ }
1193
+
1194
+ // src/client/x402Client.ts
965
1195
  var CAIP2 = {
966
1196
  testnet: "stellar:testnet",
967
1197
  mainnet: "stellar:pubnet"
@@ -993,83 +1223,106 @@ var X402Client = class _X402Client {
993
1223
  if (!pricing.facilitator) {
994
1224
  throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
995
1225
  }
996
- return withRetry(async () => {
997
- let init;
1226
+ const caip2 = CAIP2[this.network];
1227
+ const probe = await withRetry(async () => {
1228
+ let init2;
998
1229
  try {
999
- init = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
1230
+ init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
1000
1231
  } catch (err) {
1001
1232
  throw wrapFetchError(err, "x402 initial request");
1002
1233
  }
1003
- if (init.status !== 402) {
1004
- if (!init.ok) {
1234
+ if (init2.status !== 402) {
1235
+ if (!init2.ok) {
1005
1236
  throw httpStatusToError(
1006
- `x402 initial request failed: HTTP ${init.status}`,
1007
- init.status,
1008
- init
1237
+ `x402 initial request failed: HTTP ${init2.status}`,
1238
+ init2.status,
1239
+ init2
1009
1240
  );
1010
1241
  }
1011
1242
  let data2;
1012
1243
  try {
1013
- data2 = await init.json();
1244
+ data2 = await init2.json();
1014
1245
  } catch {
1015
- throw new RouteDockManifestError(`Failed to parse JSON from non-402 response (HTTP ${init.status})`);
1246
+ throw new RouteDockManifestError(
1247
+ `Failed to parse JSON from non-402 response (HTTP ${init2.status})`
1248
+ );
1016
1249
  }
1017
- return { data: data2, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
1018
- }
1019
- const reqHeader = init.headers.get("X-Payment-Requirements");
1020
- if (!reqHeader) {
1021
- throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1250
+ return { kind: "free", data: data2 };
1022
1251
  }
1023
- let paymentRequired;
1024
- try {
1025
- paymentRequired = http.decodePaymentRequiredHeader(reqHeader);
1026
- } catch (err) {
1027
- throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
1028
- cause: err
1029
- });
1030
- }
1031
- let paymentPayload;
1032
- try {
1033
- paymentPayload = await this.httpClient.createPaymentPayload(paymentRequired);
1034
- } catch (err) {
1035
- throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
1036
- cause: err
1037
- });
1038
- }
1039
- const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1040
- let settled;
1252
+ return { kind: "payment-required", init: init2 };
1253
+ }, this.retryPolicy);
1254
+ if (probe.kind === "free") {
1255
+ return { data: probe.data, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
1256
+ }
1257
+ const init = probe.init;
1258
+ const reqHeader = init.headers.get("X-Payment-Requirements");
1259
+ if (!reqHeader) {
1260
+ throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1261
+ }
1262
+ let paymentRequired;
1263
+ try {
1264
+ paymentRequired = http.decodePaymentRequiredHeader(reqHeader);
1265
+ } catch (err) {
1266
+ throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
1267
+ cause: err
1268
+ });
1269
+ }
1270
+ const firstAccept = paymentRequired.accepts[0];
1271
+ if (!firstAccept) {
1272
+ throw new RouteDockManifestError("402 response carries no payment accepts");
1273
+ }
1274
+ const allowed = filterX402Accepts(paymentRequired.accepts, manifest, caip2, pricing.amount);
1275
+ if (allowed.length === 0) {
1276
+ throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
1277
+ }
1278
+ let paymentPayload;
1279
+ try {
1280
+ paymentPayload = await this.httpClient.createPaymentPayload({
1281
+ ...paymentRequired,
1282
+ accepts: allowed
1283
+ });
1284
+ } catch (err) {
1285
+ throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
1286
+ cause: err
1287
+ });
1288
+ }
1289
+ const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1290
+ const signedAmount = nulthSdk.stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
1291
+ const settled = await withRetry(async () => {
1292
+ let response;
1041
1293
  try {
1042
- settled = await fetch(url, { headers: paymentHeaders });
1294
+ response = await fetch(url, { headers: paymentHeaders });
1043
1295
  } catch (err) {
1044
1296
  throw wrapFetchError(err, "x402 settlement request");
1045
1297
  }
1046
- if (!settled.ok) {
1047
- if (settled.status >= 500 || settled.status === 429 || settled.status === 503) {
1298
+ if (!response.ok) {
1299
+ if (response.status >= 500 || response.status === 429 || response.status === 503) {
1048
1300
  throw httpStatusToError(
1049
- `x402 payment failed: HTTP ${settled.status}`,
1050
- settled.status,
1051
- settled
1301
+ `x402 payment failed: HTTP ${response.status}`,
1302
+ response.status,
1303
+ response
1052
1304
  );
1053
1305
  }
1054
- throw new RouteDockManifestError(`x402 payment failed: HTTP ${settled.status}`);
1306
+ throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
1055
1307
  }
1056
- let txHash = null;
1057
- const responseHeader = settled.headers.get("X-Payment-Response");
1058
- if (responseHeader) {
1059
- try {
1060
- const settleResponse = http.decodePaymentResponseHeader(responseHeader);
1061
- txHash = settleResponse.transaction ?? null;
1062
- } catch {
1063
- }
1064
- }
1065
- let data;
1308
+ return response;
1309
+ }, this.retryPolicy);
1310
+ let txHash = null;
1311
+ const responseHeader = settled.headers.get("X-Payment-Response");
1312
+ if (responseHeader) {
1066
1313
  try {
1067
- data = await settled.json();
1314
+ const settleResponse = http.decodePaymentResponseHeader(responseHeader);
1315
+ txHash = settleResponse.transaction ?? null;
1068
1316
  } catch {
1069
- throw new RouteDockManifestError("Failed to parse JSON from settled response");
1070
1317
  }
1071
- return { data, txHash, mode: "x402", amount: pricing.amount, timestamp: Date.now() };
1072
- }, this.retryPolicy);
1318
+ }
1319
+ let data;
1320
+ try {
1321
+ data = await settled.json();
1322
+ } catch {
1323
+ throw new RouteDockManifestError("Failed to parse JSON from settled response");
1324
+ }
1325
+ return { data, txHash, mode: "x402", amount: signedAmount, timestamp: Date.now() };
1073
1326
  }
1074
1327
  };
1075
1328
  var MppChargeClient = class {
@@ -1086,27 +1339,42 @@ var MppChargeClient = class {
1086
1339
  if (!pricing) {
1087
1340
  throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
1088
1341
  }
1089
- return withRetry(async () => {
1090
- let txHash = null;
1091
- const mppx = client$2.Mppx.create({
1092
- polyfill: false,
1093
- methods: [
1094
- client$3.stellar.charge({
1095
- keypair: this.keypair,
1096
- mode: "pull",
1097
- onProgress(event) {
1098
- if (event.type === "paid") {
1099
- txHash = event.hash;
1100
- }
1342
+ const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
1343
+ let txHash = null;
1344
+ let signedAmount;
1345
+ let credential;
1346
+ const mppx = client$2.Mppx.create({
1347
+ polyfill: false,
1348
+ methods: [
1349
+ client$3.stellar.charge({
1350
+ keypair: this.keypair,
1351
+ mode: "pull",
1352
+ onProgress(event) {
1353
+ if (event.type === "paid") {
1354
+ txHash = event.hash;
1101
1355
  }
1102
- })
1103
- ]
1104
- });
1356
+ }
1357
+ })
1358
+ ],
1359
+ onChallenge: async (challenge, { createCredential }) => {
1360
+ const request = challenge.request;
1361
+ const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
1362
+ if (rejection) throw rejection;
1363
+ const amount = request["amount"];
1364
+ if (typeof amount !== "string") {
1365
+ throw new RouteDockPolicyRejectError("challenge_amount_invalid");
1366
+ }
1367
+ signedAmount = nulthSdk.stroopsToUsdc(BigInt(amount));
1368
+ credential ??= await createCredential();
1369
+ return credential;
1370
+ }
1371
+ });
1372
+ return withRetry(async () => {
1105
1373
  let response;
1106
1374
  try {
1107
- response = await mppx.fetch(url);
1375
+ response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
1108
1376
  } catch (err) {
1109
- throw wrapFetchError(err, "MPP charge request");
1377
+ throw wrapMppError(err, "MPP charge request");
1110
1378
  }
1111
1379
  if (!response.ok) {
1112
1380
  if (response.status >= 500 || response.status === 429 || response.status === 503) {
@@ -1127,7 +1395,13 @@ var MppChargeClient = class {
1127
1395
  { cause }
1128
1396
  );
1129
1397
  }
1130
- return { data, txHash, mode: "mpp-charge", amount: pricing.amount, timestamp: Date.now() };
1398
+ return {
1399
+ data,
1400
+ txHash,
1401
+ mode: "mpp-charge",
1402
+ amount: signedAmount ?? pricing.amount,
1403
+ timestamp: Date.now()
1404
+ };
1131
1405
  }, this.retryPolicy);
1132
1406
  }
1133
1407
  };
@@ -1166,16 +1440,18 @@ function resolveStoreNetworkId(network) {
1166
1440
  return network === mpp.STELLAR_PUBNET ? mpp.STELLAR_PUBNET : mpp.STELLAR_TESTNET;
1167
1441
  }
1168
1442
  var MppSessionClient = class {
1169
- constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory) {
1443
+ constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory, logger = consoleLogger) {
1170
1444
  this.keypair = keypair;
1171
1445
  this.network = network;
1172
1446
  this.retryPolicy = retryPolicy;
1173
1447
  this.webSocketFactory = webSocketFactory;
1448
+ this.logger = logger;
1174
1449
  }
1175
1450
  keypair;
1176
1451
  network;
1177
1452
  retryPolicy;
1178
1453
  webSocketFactory;
1454
+ logger;
1179
1455
  async openSession(url, manifest, commitmentSecret, options, onSpend) {
1180
1456
  const mode = options?.mode ?? "mpp-session";
1181
1457
  const pricing = manifest.pricing[mode];
@@ -1400,7 +1676,7 @@ var MppSessionClient = class {
1400
1676
  if (abortController.signal.aborted) {
1401
1677
  throw err;
1402
1678
  }
1403
- throw wrapFetchError(err, "Voucher request");
1679
+ throw wrapMppError(err, "Voucher request");
1404
1680
  }
1405
1681
  if (!resp.ok) {
1406
1682
  if (resp.status >= 500 || resp.status === 429 || resp.status === 503) {
@@ -1693,9 +1969,10 @@ var MppSessionClient = class {
1693
1969
  emit("session:timeout", { maxDurationMs });
1694
1970
  void handle.close().catch((error) => {
1695
1971
  emit("session:close-failed", { maxDurationMs, error });
1696
- console.warn(
1972
+ this.logger(
1973
+ "warn",
1697
1974
  `RouteDock: maxDuration auto-close failed after ${maxDurationMs}ms \u2014 the channel may still hold collateral; retry close() or call requestRefund().`,
1698
- error
1975
+ { error }
1699
1976
  );
1700
1977
  });
1701
1978
  }, maxDurationMs);
@@ -1745,7 +2022,7 @@ var MppSessionClient = class {
1745
2022
  );
1746
2023
  onSigned?.();
1747
2024
  } catch (err) {
1748
- throw wrapFetchError(err, "Voucher credential");
2025
+ throw wrapMppError(err, "Voucher credential");
1749
2026
  }
1750
2027
  const wsUrl = url.replace(/^http/, "ws");
1751
2028
  const queue = [];
@@ -1922,11 +2199,6 @@ function encodeAuthSignature(proof) {
1922
2199
  const payload = { nulth: "zk-v1", proof };
1923
2200
  return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
1924
2201
  }
1925
- function usdcToStroops2(amount) {
1926
- const [whole = "0", frac = ""] = amount.split(".");
1927
- const padded = (frac + "0000000").slice(0, 7);
1928
- return BigInt(whole) * 10000000n + BigInt(padded);
1929
- }
1930
2202
  var NulthClient = class {
1931
2203
  constructor(config) {
1932
2204
  this.config = config;
@@ -1942,7 +2214,10 @@ var NulthClient = class {
1942
2214
  `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
1943
2215
  );
1944
2216
  }
1945
- console.warn("NulthClient is using the insecure mock prover; proofs are not cryptographically sound");
2217
+ resolveLogger(config.logger)(
2218
+ "warn",
2219
+ "NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
2220
+ );
1946
2221
  }
1947
2222
  config;
1948
2223
  policy;
@@ -1994,6 +2269,12 @@ var NulthClient = class {
1994
2269
  return proof;
1995
2270
  }
1996
2271
  enforcePolicy(context) {
2272
+ if (context.amountStroops < 0n) {
2273
+ throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
2274
+ }
2275
+ if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
2276
+ throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
2277
+ }
1997
2278
  if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
1998
2279
  throw new NulthPolicyError("session_expired", "Nulth session expired");
1999
2280
  }
@@ -2028,14 +2309,14 @@ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
2028
2309
  const pricing = manifest.pricing[mode];
2029
2310
  if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
2030
2311
  return {
2031
- payee: manifest.payee,
2032
- amountStroops: usdcToStroops2(pricing.amount),
2312
+ payee: pricing.payee ?? manifest.payee,
2313
+ amountStroops: nulthSdk.usdcToStroops(pricing.amount),
2033
2314
  assetContract: manifest.asset_contract,
2034
2315
  ledgerSequence
2035
2316
  };
2036
2317
  }
2037
2318
  function createPolicyState(input) {
2038
- const dailyCapStroops = usdcToStroops2(input.dailyCapUsdc);
2319
+ const dailyCapStroops = nulthSdk.usdcToStroops(input.dailyCapUsdc);
2039
2320
  return {
2040
2321
  dailyCapStroops,
2041
2322
  allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
@@ -2137,7 +2418,8 @@ var InMemorySpendStore = class {
2137
2418
  state = null;
2138
2419
  constructor(options = {}) {
2139
2420
  if (options.warn !== false) {
2140
- console.warn(
2421
+ resolveLogger(options.logger)(
2422
+ "warn",
2141
2423
  "[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."
2142
2424
  );
2143
2425
  }
@@ -2227,7 +2509,10 @@ var RouteDockClient = class _RouteDockClient {
2227
2509
  this.network = config.network;
2228
2510
  this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
2229
2511
  this.retryPolicy = config.retryPolicy;
2230
- this.spendStore = config.spendStore ?? new InMemorySpendStore({ warn: !!config.spendCap });
2512
+ this.spendStore = config.spendStore ?? new InMemorySpendStore({
2513
+ warn: !!config.spendCap,
2514
+ ...config.logger && { logger: config.logger }
2515
+ });
2231
2516
  this.logger = config.logger;
2232
2517
  this.manifestTimeoutMs = config.manifestTimeoutMs;
2233
2518
  this.expectedPayee = config.expectedPayee;
@@ -2239,9 +2524,10 @@ var RouteDockClient = class _RouteDockClient {
2239
2524
  _secrets.set(this, config.commitmentSecret);
2240
2525
  }
2241
2526
  const secretKey = this.keypair.secret();
2242
- this.x402 = new X402Client(secretKey, this.network, this.retryPolicy);
2243
- this.charge = new MppChargeClient(this.keypair, this.network, this.retryPolicy);
2244
- this.session = new MppSessionClient(this.keypair, this.network, this.retryPolicy);
2527
+ const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
2528
+ this.x402 = new X402Client(secretKey, this.network, retryPolicy);
2529
+ this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
2530
+ this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
2245
2531
  }
2246
2532
  /** Fetch manifest and select mode — shared by pay() and estimateCost(). */
2247
2533
  async _resolveManifest(url, options) {
@@ -2270,12 +2556,13 @@ var RouteDockClient = class _RouteDockClient {
2270
2556
  * call before committing to a payment — for approval gates and manual
2271
2557
  * trustline remediation.
2272
2558
  */
2273
- async preflight(manifest) {
2559
+ async preflight(manifest, mode, endpoint) {
2274
2560
  assertManifestValid(manifest);
2275
- await this._checkTrustline(manifest);
2561
+ const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
2562
+ await this._checkTrustline(manifest, selectedAsset);
2276
2563
  return {
2277
2564
  hasTrustline: true,
2278
- asset: manifest.asset,
2565
+ asset: selectedAsset,
2279
2566
  modes: manifest.modes,
2280
2567
  ...manifest.regions && { regions: manifest.regions },
2281
2568
  ...manifest.latency_hints && { latency_hints: manifest.latency_hints },
@@ -2289,8 +2576,8 @@ var RouteDockClient = class _RouteDockClient {
2289
2576
  * trustline is missing. Results are cached per (account, asset) so the
2290
2577
  * per-payment overhead is one RPC call amortized to zero after the first.
2291
2578
  */
2292
- async _checkTrustline(manifest) {
2293
- const cacheKey = `${this.network}:${this.keypair.publicKey()}:${manifest.asset}`;
2579
+ async _checkTrustline(manifest, targetAsset = manifest.asset) {
2580
+ const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
2294
2581
  const cached = _RouteDockClient._trustlineCache.get(cacheKey);
2295
2582
  if (cached && Date.now() < cached.expiresAt) return;
2296
2583
  const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
@@ -2298,13 +2585,13 @@ var RouteDockClient = class _RouteDockClient {
2298
2585
  try {
2299
2586
  const account = await server.loadAccount(this.keypair.publicKey());
2300
2587
  const balances = account.balances;
2301
- const expectedIssuer = getAssetIssuer(manifest.asset, this.network);
2588
+ const expectedIssuer = getAssetIssuer(targetAsset, this.network);
2302
2589
  const hasTrustline = balances.some(
2303
- (b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code === manifest.asset && (!expectedIssuer || b.asset_issuer === expectedIssuer)
2590
+ (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))
2304
2591
  );
2305
2592
  if (!hasTrustline) {
2306
- 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}`;
2307
- throw new RouteDockTrustlineError(manifest.asset, expectedIssuer || "unknown", remediation);
2593
+ 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}`;
2594
+ throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
2308
2595
  }
2309
2596
  _RouteDockClient._trustlineCache.set(cacheKey, {
2310
2597
  exists: true,
@@ -2313,7 +2600,8 @@ var RouteDockClient = class _RouteDockClient {
2313
2600
  } catch (err) {
2314
2601
  if (err instanceof RouteDockTrustlineError) throw err;
2315
2602
  this.logger?.(
2316
- `[RouteDock] Trustline preflight: could not verify trustline for ${manifest.asset} \u2014 continuing`
2603
+ "warn",
2604
+ `[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
2317
2605
  );
2318
2606
  }
2319
2607
  }
@@ -2321,18 +2609,18 @@ var RouteDockClient = class _RouteDockClient {
2321
2609
  * Pay for one request at `url`. Fetches manifest, selects payment mode,
2322
2610
  * runs trustline preflight, reserves local spend cap BEFORE executing the
2323
2611
  * payment, then commits the spend record on success. Rolls back the
2324
- * reservation if the on-chain payment fails. Concurrent pay() calls are
2325
- * serialized through a per-instance mutex to prevent spend-cap overrun.
2612
+ * reservation if the on-chain payment fails, and also when the call
2613
+ * completes without paying anything (an x402 endpoint answering a plain
2614
+ * 200 with no challenge). Concurrent pay() calls are serialized through a
2615
+ * per-instance mutex to prevent spend-cap overrun.
2326
2616
  */
2327
2617
  async pay(url, options) {
2328
2618
  const baseUrl = new URL(url).origin;
2329
2619
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2330
2620
  assertEndpointActive(manifest, url, this.logger);
2331
2621
  const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
2332
- await this._checkTrustline(manifest);
2333
- if (this.vault?.mode === "nulth") {
2334
- return this._payWithNulthVault(url, manifest, mode);
2335
- }
2622
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2623
+ await this._checkTrustline(manifest, selectedAsset);
2336
2624
  let amount;
2337
2625
  switch (mode) {
2338
2626
  case "x402":
@@ -2352,22 +2640,30 @@ var RouteDockClient = class _RouteDockClient {
2352
2640
  const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
2353
2641
  let result;
2354
2642
  try {
2355
- switch (mode) {
2356
- case "x402":
2357
- result = await this.x402.pay(url, manifest);
2358
- break;
2359
- case "mpp-charge":
2360
- result = await this.charge.pay(url, manifest);
2361
- break;
2362
- default:
2363
- throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2643
+ if (this.vault?.mode === "nulth") {
2644
+ result = await this._payWithNulthVault(url, manifest, mode);
2645
+ } else {
2646
+ switch (mode) {
2647
+ case "x402":
2648
+ result = await this.x402.pay(url, manifest);
2649
+ break;
2650
+ case "mpp-charge":
2651
+ result = await this.charge.pay(url, manifest);
2652
+ break;
2653
+ default:
2654
+ throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2655
+ }
2364
2656
  }
2365
2657
  } catch (err) {
2366
2658
  await this._rollbackSpend(reserveId).catch(() => {
2367
2659
  });
2368
2660
  throw err;
2369
2661
  }
2370
- await this._commitSpend(reserveId);
2662
+ if (result.txHash === null && result.amount === "0") {
2663
+ await this._rollbackSpend(reserveId);
2664
+ } else {
2665
+ await this._commitSpend(reserveId);
2666
+ }
2371
2667
  return result;
2372
2668
  }
2373
2669
  /** Nulth ZK vault path — proof built off-chain, attached as auth signature */
@@ -2417,9 +2713,10 @@ var RouteDockClient = class _RouteDockClient {
2417
2713
  default:
2418
2714
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2419
2715
  }
2716
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2420
2717
  return {
2421
2718
  amount,
2422
- asset: manifest.asset,
2719
+ asset: selectedAsset,
2423
2720
  mode,
2424
2721
  manifest,
2425
2722
  ...manifest.regions && { regions: manifest.regions },
@@ -2656,7 +2953,7 @@ var SupabaseSeenTxStore = class {
2656
2953
  }
2657
2954
  supabase;
2658
2955
  async get(key) {
2659
- const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
2956
+ const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
2660
2957
  if (error) {
2661
2958
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
2662
2959
  }
@@ -2664,7 +2961,8 @@ var SupabaseSeenTxStore = class {
2664
2961
  const headers = data.headers;
2665
2962
  return {
2666
2963
  txHash: data.tx_hash ?? null,
2667
- ...headers ? { headers } : {}
2964
+ ...headers ? { headers } : {},
2965
+ createdAt: Date.parse(data.created_at)
2668
2966
  };
2669
2967
  }
2670
2968
  async set(key, record) {
@@ -2672,7 +2970,8 @@ var SupabaseSeenTxStore = class {
2672
2970
  {
2673
2971
  key,
2674
2972
  tx_hash: record.txHash,
2675
- headers: record.headers ?? null
2973
+ headers: record.headers ?? null,
2974
+ created_at: new Date(record.createdAt ?? Date.now()).toISOString()
2676
2975
  },
2677
2976
  { onConflict: "key" }
2678
2977
  );
@@ -2680,21 +2979,47 @@ var SupabaseSeenTxStore = class {
2680
2979
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
2681
2980
  }
2682
2981
  }
2982
+ /**
2983
+ * Atomically increment the settlement's replay counter through the
2984
+ * `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
2985
+ * row lock gives concurrent claims distinct counts. Returns `Infinity` when
2986
+ * the key no longer exists, so the caller treats it as spent.
2987
+ */
2988
+ async claimReplay(key) {
2989
+ const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
2990
+ p_key: key
2991
+ });
2992
+ if (error) {
2993
+ throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
2994
+ }
2995
+ if (data == null) return Number.POSITIVE_INFINITY;
2996
+ return data;
2997
+ }
2683
2998
  };
2684
-
2685
- // src/provider/payee.ts
2686
- function resolvePayee(manifest, mode) {
2687
- const override = manifest.pricing[mode]?.payee;
2688
- return override ?? manifest.payee;
2689
- }
2690
2999
  function extractPayerAddress(key) {
2691
3000
  if (typeof key !== "string") return null;
2692
3001
  return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
2693
3002
  }
2694
3003
 
2695
3004
  // src/provider/SeenTxStore.ts
3005
+ var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
3006
+ var MAX_SETTLEMENT_REPLAYS = 1;
3007
+ async function checkSettlementReplay(store, key, now = Date.now()) {
3008
+ const record = await store.get(key);
3009
+ if (!record) return { kind: "miss" };
3010
+ const { createdAt } = record;
3011
+ if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
3012
+ return { kind: "spent" };
3013
+ }
3014
+ if (store.claimReplay) {
3015
+ const replayed = await store.claimReplay(key);
3016
+ if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
3017
+ }
3018
+ return { kind: "replay", record };
3019
+ }
2696
3020
  var InMemorySeenTxStore = class {
2697
3021
  map = /* @__PURE__ */ new Map();
3022
+ replayCounts = /* @__PURE__ */ new Map();
2698
3023
  order = [];
2699
3024
  constructor(maxEntriesOrOptions = 1e4) {
2700
3025
  const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
@@ -2702,7 +3027,8 @@ var InMemorySeenTxStore = class {
2702
3027
  if (options.warn !== false) {
2703
3028
  const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
2704
3029
  if (isServerless) {
2705
- console.warn(
3030
+ resolveLogger(options.logger)(
3031
+ "warn",
2706
3032
  "[RouteDock] Using in-memory SeenTxStore: settlement deduplication is NOT durable and resets on every isolate restart. Supply a SupabaseSeenTxStore (or other durable SeenTxStore) for production safety."
2707
3033
  );
2708
3034
  }
@@ -2717,21 +3043,43 @@ var InMemorySeenTxStore = class {
2717
3043
  this.order.push(key);
2718
3044
  if (this.order.length > this.maxEntries) {
2719
3045
  const evicted = this.order.shift();
2720
- if (evicted !== void 0) this.map.delete(evicted);
3046
+ if (evicted !== void 0) {
3047
+ this.map.delete(evicted);
3048
+ this.replayCounts.delete(evicted);
3049
+ }
2721
3050
  }
2722
3051
  }
2723
3052
  this.map.set(key, record);
2724
3053
  }
3054
+ claimReplay(key) {
3055
+ if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
3056
+ const next = (this.replayCounts.get(key) ?? 0) + 1;
3057
+ this.replayCounts.set(key, next);
3058
+ return next;
3059
+ }
2725
3060
  };
2726
3061
  async function sha256Hex2(input) {
2727
3062
  const buf = new TextEncoder().encode(input);
2728
3063
  const digest = await crypto.subtle.digest("SHA-256", buf);
2729
3064
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
2730
3065
  }
2731
- async function paymentIdempotencyKey(getHeader) {
3066
+ async function paymentIdempotencyKey(getHeader, scope) {
2732
3067
  const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
2733
3068
  if (!material) return null;
2734
- return sha256Hex2(material);
3069
+ return sha256Hex2(
3070
+ [scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
3071
+ );
3072
+ }
3073
+
3074
+ // src/provider/settleResult.ts
3075
+ function readSettleResult(result) {
3076
+ const settle = result;
3077
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
3078
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
3079
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
3080
+ return failed(settle.errorReason);
3081
+ }
3082
+ return { ok: true, txHash: settle.transaction };
2735
3083
  }
2736
3084
 
2737
3085
  // src/provider/x402Handler.ts
@@ -2744,7 +3092,8 @@ function createX402Handler(opts) {
2744
3092
  const caip2 = CAIP22[opts.network];
2745
3093
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
2746
3094
  const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
2747
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
3095
+ const logger = resolveLogger(opts.logger);
3096
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
2748
3097
  const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
2749
3098
  const localFacilitator = new facilitator.ExactStellarScheme([signer], {
2750
3099
  areFeesSponsored: true
@@ -2765,22 +3114,29 @@ function createX402Handler(opts) {
2765
3114
  }
2766
3115
  const amountInBaseUnits = String(usdcToUnits(opts.amount));
2767
3116
  const payTo = resolvePayee(opts.manifest, "x402");
2768
- const requirements = {
2769
- scheme: "exact",
2770
- network: caip2,
2771
- asset: opts.assetContract,
2772
- amount: amountInBaseUnits,
2773
- payTo,
2774
- maxTimeoutSeconds: 60,
2775
- extra: {
2776
- areFeesSponsored: true,
2777
- ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
2778
- }
2779
- };
2780
3117
  return async (req, res, next) => {
2781
3118
  try {
2782
- const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
2783
- if (!paymentHeader) {
3119
+ const endpoint = req.path || req.originalUrl;
3120
+ const assetContract = resolveAssetContract(
3121
+ opts.manifest,
3122
+ "x402",
3123
+ endpoint,
3124
+ opts.assetContract
3125
+ );
3126
+ const requirements = {
3127
+ scheme: "exact",
3128
+ network: caip2,
3129
+ asset: assetContract,
3130
+ amount: amountInBaseUnits,
3131
+ payTo,
3132
+ maxTimeoutSeconds: 60,
3133
+ extra: {
3134
+ areFeesSponsored: true,
3135
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
3136
+ }
3137
+ };
3138
+ const respondPaymentRequired = async (error, reason) => {
3139
+ const body = { error, ...reason ? { reason } : {} };
2784
3140
  if (ozServer) {
2785
3141
  const resourceInfo = {
2786
3142
  url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
@@ -2790,7 +3146,7 @@ function createX402Handler(opts) {
2790
3146
  [requirements],
2791
3147
  resourceInfo
2792
3148
  );
2793
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json({ error: "Payment Required" });
3149
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(body);
2794
3150
  } else {
2795
3151
  const x402Response = {
2796
3152
  x402Version: 2,
@@ -2800,19 +3156,35 @@ function createX402Handler(opts) {
2800
3156
  },
2801
3157
  accepts: [requirements]
2802
3158
  };
2803
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
3159
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(body);
2804
3160
  }
3161
+ };
3162
+ const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
3163
+ if (!paymentHeader) {
3164
+ await respondPaymentRequired("Payment Required");
2805
3165
  return;
2806
3166
  }
2807
- const idempotencyKey = await paymentIdempotencyKey((name) => {
2808
- const v = req.headers[name.toLowerCase()];
2809
- return Array.isArray(v) ? v[0] : v;
2810
- });
3167
+ const idempotencyKey = await paymentIdempotencyKey(
3168
+ (name) => {
3169
+ const v = req.headers[name.toLowerCase()];
3170
+ return Array.isArray(v) ? v[0] : v;
3171
+ },
3172
+ {
3173
+ method: req.method,
3174
+ path: req.originalUrl.split("?")[0],
3175
+ amount: requirements.amount,
3176
+ payTo: requirements.payTo
3177
+ }
3178
+ );
2811
3179
  if (idempotencyKey) {
2812
- const cached = await seenTxStore.get(idempotencyKey);
2813
- if (cached) {
2814
- if (cached.headers) {
2815
- for (const [k, val] of Object.entries(cached.headers)) {
3180
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
3181
+ if (replayCheck.kind === "spent") {
3182
+ res.status(402).json({ error: "Payment already used" });
3183
+ return;
3184
+ }
3185
+ if (replayCheck.kind === "replay") {
3186
+ if (replayCheck.record.headers) {
3187
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
2816
3188
  res.setHeader(k, val);
2817
3189
  }
2818
3190
  }
@@ -2831,15 +3203,18 @@ function createX402Handler(opts) {
2831
3203
  }
2832
3204
  if (ozServer) {
2833
3205
  const settleResult = await ozServer.settlePayment(payload, requirements);
2834
- txHash = settleResult.transaction ?? null;
2835
- if (settleResult) {
2836
- res.setHeader(
2837
- "X-Payment-Response",
2838
- http.encodePaymentResponseHeader(
2839
- settleResult
2840
- )
2841
- );
3206
+ const outcome = readSettleResult(settleResult);
3207
+ if (!outcome.ok) {
3208
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
3209
+ return;
2842
3210
  }
3211
+ txHash = outcome.txHash;
3212
+ res.setHeader(
3213
+ "X-Payment-Response",
3214
+ http.encodePaymentResponseHeader(
3215
+ settleResult
3216
+ )
3217
+ );
2843
3218
  } else {
2844
3219
  const verifyResult = await localFacilitator.verify(
2845
3220
  payload,
@@ -2856,15 +3231,18 @@ function createX402Handler(opts) {
2856
3231
  payload,
2857
3232
  requirements
2858
3233
  );
2859
- txHash = settleResult.transaction ?? null;
2860
- if (settleResult) {
2861
- res.setHeader(
2862
- "X-Payment-Response",
2863
- http.encodePaymentResponseHeader(
2864
- settleResult
2865
- )
2866
- );
3234
+ const outcome = readSettleResult(settleResult);
3235
+ if (!outcome.ok) {
3236
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
3237
+ return;
2867
3238
  }
3239
+ txHash = outcome.txHash;
3240
+ res.setHeader(
3241
+ "X-Payment-Response",
3242
+ http.encodePaymentResponseHeader(
3243
+ settleResult
3244
+ )
3245
+ );
2868
3246
  }
2869
3247
  if (idempotencyKey) {
2870
3248
  const headers = {};
@@ -2872,17 +3250,21 @@ function createX402Handler(opts) {
2872
3250
  if (typeof paymentResponse === "string") {
2873
3251
  headers["X-Payment-Response"] = paymentResponse;
2874
3252
  }
2875
- await seenTxStore.set(idempotencyKey, { txHash, headers });
3253
+ await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
2876
3254
  }
2877
3255
  if (txHash && opts.onSettled) {
2878
3256
  Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
2879
- console.error("[x402] onSettled callback error:", err);
3257
+ logger("error", "[x402] onSettled callback error", { error: err });
2880
3258
  opts.onCallbackError?.(err, "onSettled");
2881
3259
  });
2882
3260
  }
2883
3261
  next();
2884
3262
  } catch (err) {
2885
- console.error("[x402] Settlement error:", err);
3263
+ if (err instanceof RouteDockManifestError) {
3264
+ next(err);
3265
+ return;
3266
+ }
3267
+ logger("error", "[x402] Settlement error", { error: err });
2886
3268
  res.status(500).json({ error: "Payment settlement failed" });
2887
3269
  }
2888
3270
  };
@@ -2895,20 +3277,37 @@ function createMppChargeHandler(opts) {
2895
3277
  const networkId = MPP_NETWORK[opts.network];
2896
3278
  const amountHumanReadable = opts.amount;
2897
3279
  const recipient = resolvePayee(opts.manifest, "mpp-charge");
2898
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
2899
- const mppx = server$2.Mppx.create({
2900
- secretKey: opts.payeeSecretKey,
2901
- methods: [
2902
- server$3.stellar.charge({
2903
- recipient,
2904
- currency: opts.assetContract,
2905
- network: networkId,
2906
- feePayer: { envelopeSigner: opts.payeeSecretKey }
2907
- })
2908
- ]
2909
- });
3280
+ const logger = resolveLogger(opts.logger);
3281
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
3282
+ const mppxInstances = /* @__PURE__ */ new Map();
3283
+ function getMppx(contract) {
3284
+ let instance = mppxInstances.get(contract);
3285
+ if (!instance) {
3286
+ instance = server$2.Mppx.create({
3287
+ secretKey: opts.payeeSecretKey,
3288
+ methods: [
3289
+ server$4.stellar.charge({
3290
+ recipient,
3291
+ currency: contract,
3292
+ network: networkId,
3293
+ feePayer: { envelopeSigner: opts.payeeSecretKey }
3294
+ })
3295
+ ]
3296
+ });
3297
+ mppxInstances.set(contract, instance);
3298
+ }
3299
+ return instance;
3300
+ }
2910
3301
  return async (req, res, next) => {
2911
3302
  try {
3303
+ const endpoint = req.path || req.originalUrl;
3304
+ const assetContract = resolveAssetContract(
3305
+ opts.manifest,
3306
+ "mpp-charge",
3307
+ endpoint,
3308
+ opts.assetContract
3309
+ );
3310
+ const mppx = getMppx(assetContract);
2912
3311
  let payerAddress = null;
2913
3312
  try {
2914
3313
  const authHeader = req.headers["authorization"];
@@ -2924,15 +3323,27 @@ function createMppChargeHandler(opts) {
2924
3323
  }
2925
3324
  } catch {
2926
3325
  }
2927
- const idempotencyKey = await paymentIdempotencyKey((name) => {
2928
- const v = req.headers[name.toLowerCase()];
2929
- return Array.isArray(v) ? v[0] : v;
2930
- });
3326
+ const idempotencyKey = await paymentIdempotencyKey(
3327
+ (name) => {
3328
+ const v = req.headers[name.toLowerCase()];
3329
+ return Array.isArray(v) ? v[0] : v;
3330
+ },
3331
+ {
3332
+ method: req.method,
3333
+ path: req.originalUrl.split("?")[0],
3334
+ amount: amountHumanReadable,
3335
+ payTo: recipient
3336
+ }
3337
+ );
2931
3338
  if (idempotencyKey) {
2932
- const cached = await seenTxStore.get(idempotencyKey);
2933
- if (cached) {
2934
- if (cached.headers) {
2935
- for (const [k, val] of Object.entries(cached.headers)) {
3339
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
3340
+ if (replayCheck.kind === "spent") {
3341
+ res.status(402).json({ error: "Payment already used" });
3342
+ return;
3343
+ }
3344
+ if (replayCheck.kind === "replay") {
3345
+ if (replayCheck.record.headers) {
3346
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
2936
3347
  res.setHeader(k, val);
2937
3348
  }
2938
3349
  }
@@ -2944,7 +3355,7 @@ function createMppChargeHandler(opts) {
2944
3355
  const handler = mppx["stellar/charge"];
2945
3356
  const result = await handler({
2946
3357
  amount: amountHumanReadable,
2947
- currency: opts.assetContract,
3358
+ currency: assetContract,
2948
3359
  recipient,
2949
3360
  description: opts.manifest.name
2950
3361
  })(fetchReq);
@@ -2975,12 +3386,13 @@ function createMppChargeHandler(opts) {
2975
3386
  if (idempotencyKey) {
2976
3387
  await seenTxStore.set(idempotencyKey, {
2977
3388
  txHash: reference ?? null,
2978
- headers: receiptHeaders
3389
+ headers: receiptHeaders,
3390
+ createdAt: Date.now()
2979
3391
  });
2980
3392
  }
2981
3393
  if (reference && opts.onSettled) {
2982
3394
  Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
2983
- console.error("[mpp-charge] onSettled callback error:", err);
3395
+ logger("error", "[mpp-charge] onSettled callback error", { error: err });
2984
3396
  opts.onCallbackError?.(err, "onSettled");
2985
3397
  });
2986
3398
  }
@@ -3081,8 +3493,9 @@ function createMppSessionHandler(opts) {
3081
3493
  const networkId = MPP_NETWORK2[opts.network];
3082
3494
  const rateHuman = opts.rate;
3083
3495
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
3496
+ const logger = resolveLogger(opts.logger);
3084
3497
  const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
3085
- const innerStore = server$4.Store.memory();
3498
+ const innerStore = server$3.Store.memory();
3086
3499
  let record = null;
3087
3500
  let voucherCount = 0;
3088
3501
  let sessionOpened = false;
@@ -3132,7 +3545,7 @@ function createMppSessionHandler(opts) {
3132
3545
  reason
3133
3546
  });
3134
3547
  } catch (err) {
3135
- console.error("[mpp-session] onOrphaned handler failed:", err);
3548
+ logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
3136
3549
  }
3137
3550
  }
3138
3551
  }
@@ -3179,7 +3592,7 @@ function createMppSessionHandler(opts) {
3179
3592
  sessionOpened = true;
3180
3593
  if (opts.onSessionOpen) {
3181
3594
  Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
3182
- console.error("[mpp-session] onSessionOpen callback error:", err);
3595
+ logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
3183
3596
  opts.onCallbackError?.(err, "onSessionOpen");
3184
3597
  });
3185
3598
  }
@@ -3187,7 +3600,7 @@ function createMppSessionHandler(opts) {
3187
3600
  if (opts.onVoucher) {
3188
3601
  const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
3189
3602
  Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
3190
- console.error("[mpp-session] onVoucher callback error:", err);
3603
+ logger("error", "[mpp-session] onVoucher callback error", { error: err });
3191
3604
  opts.onCallbackError?.(err, "onVoucher");
3192
3605
  });
3193
3606
  }
@@ -3197,7 +3610,7 @@ function createMppSessionHandler(opts) {
3197
3610
  methods: [
3198
3611
  withTypedChannelErrors(
3199
3612
  onVerifiedCredential(
3200
- server$4.stellar.channel({
3613
+ server$3.stellar.channel({
3201
3614
  channel: opts.channelFactory,
3202
3615
  commitmentKey: opts.commitmentPublicKey,
3203
3616
  network: networkId,
@@ -3228,7 +3641,7 @@ function createMppSessionHandler(opts) {
3228
3641
  }
3229
3642
  if (closeAmount > 0n && closeSig) {
3230
3643
  const closePayer = record?.payer ?? null;
3231
- const closeTxHash = await server$4.close({
3644
+ const closeTxHash = await server$3.close({
3232
3645
  channel: opts.channelFactory,
3233
3646
  amount: closeAmount,
3234
3647
  signature: Buffer.from(closeSig, "hex"),
@@ -3240,7 +3653,7 @@ function createMppSessionHandler(opts) {
3240
3653
  if (opts.onSettled) {
3241
3654
  const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
3242
3655
  Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
3243
- console.error("[mpp-session] onSettled callback error:", err);
3656
+ logger("error", "[mpp-session] onSettled callback error", { error: err });
3244
3657
  opts.onCallbackError?.(err, "onSettled");
3245
3658
  });
3246
3659
  }
@@ -3251,7 +3664,7 @@ function createMppSessionHandler(opts) {
3251
3664
  payeeKeypair.publicKey()
3252
3665
  );
3253
3666
  if (!settlementAddresses) {
3254
- console.error("[mpp-session] skipped session_settled vault record: payer address unavailable");
3667
+ logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
3255
3668
  } else {
3256
3669
  try {
3257
3670
  const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
@@ -3274,7 +3687,7 @@ function createMppSessionHandler(opts) {
3274
3687
  preparedTx.sign(payeeKeypair);
3275
3688
  await server.sendTransaction(preparedTx);
3276
3689
  } catch (recordErr) {
3277
- console.error("[mpp-session] failed to record session_settled on vault:", recordErr);
3690
+ logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
3278
3691
  }
3279
3692
  }
3280
3693
  }
@@ -3331,12 +3744,13 @@ function routedock(opts) {
3331
3744
  payeeSecretKey: opts.payeeSecretKey,
3332
3745
  network: opts.network,
3333
3746
  amount: x402Price,
3334
- assetContract: opts.assetContract,
3747
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3335
3748
  ...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
3336
3749
  manifest: signedManifest,
3337
3750
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
3338
3751
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3339
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
3752
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
3753
+ ...opts.logger ? { logger: opts.logger } : {}
3340
3754
  })
3341
3755
  );
3342
3756
  }
@@ -3350,11 +3764,12 @@ function routedock(opts) {
3350
3764
  payeeSecretKey: opts.payeeSecretKey,
3351
3765
  network: opts.network,
3352
3766
  amount: chargePrice,
3353
- assetContract: opts.assetContract,
3767
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3354
3768
  manifest: signedManifest,
3355
3769
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
3356
3770
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3357
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
3771
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
3772
+ ...opts.logger ? { logger: opts.logger } : {}
3358
3773
  })
3359
3774
  );
3360
3775
  }
@@ -3372,7 +3787,7 @@ function routedock(opts) {
3372
3787
  network: opts.network,
3373
3788
  channelFactory: sessionPricing.channelFactory,
3374
3789
  rate: sessionPricing.rate,
3375
- assetContract: opts.assetContract,
3790
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3376
3791
  manifest: signedManifest,
3377
3792
  commitmentPublicKey: opts.commitmentPublicKey,
3378
3793
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
@@ -3380,7 +3795,8 @@ function routedock(opts) {
3380
3795
  ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
3381
3796
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3382
3797
  ...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
3383
- ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
3798
+ ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
3799
+ ...opts.logger ? { logger: opts.logger } : {}
3384
3800
  })
3385
3801
  );
3386
3802
  }
@@ -3544,25 +3960,26 @@ async function reconcileAbandonedSessions(opts) {
3544
3960
  }
3545
3961
  }
3546
3962
  async function runStartupReconciliation(opts, logger) {
3547
- const log = logger || console.log;
3548
- log("[SessionReconciler] Running startup reconciliation...");
3963
+ const log = resolveLogger(logger);
3964
+ log("info", "[SessionReconciler] Running startup reconciliation...");
3549
3965
  try {
3550
3966
  const stats = await reconcileAbandonedSessions(opts);
3551
3967
  if (stats.orphanedCount === 0) {
3552
- log("[SessionReconciler] No orphaned sessions found");
3968
+ log("info", "[SessionReconciler] No orphaned sessions found");
3553
3969
  return;
3554
3970
  }
3555
3971
  log(
3972
+ "info",
3556
3973
  `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
3557
3974
  );
3558
3975
  if (stats.errors.length > 0) {
3559
- log("[SessionReconciler] Reconciliation errors:");
3976
+ log("warn", "[SessionReconciler] Reconciliation errors:");
3560
3977
  for (const err of stats.errors) {
3561
- log(` - ${err.channelId}: ${err.reason}`);
3978
+ log("warn", ` - ${err.channelId}: ${err.reason}`);
3562
3979
  }
3563
3980
  }
3564
3981
  } catch (err) {
3565
- log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
3982
+ log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
3566
3983
  }
3567
3984
  }
3568
3985
  function tryDecodeBase64(value) {
@@ -3749,23 +4166,32 @@ exports.assertManifestValid = assertManifestValid;
3749
4166
  exports.assertNulthVaultManifest = assertNulthVaultManifest;
3750
4167
  exports.backoffDelayMs = backoffDelayMs;
3751
4168
  exports.configureManifestCache = configureManifestCache;
4169
+ exports.consoleLogger = consoleLogger;
4170
+ exports.createConsoleLogger = createConsoleLogger;
3752
4171
  exports.createNulthSigner = createNulthSigner;
3753
4172
  exports.createPolicyState = createPolicyState;
3754
4173
  exports.decimalToStroops = decimalToStroops;
3755
4174
  exports.decodeAuthSignature = decodeAuthSignature;
3756
4175
  exports.fetchLedgerSequence = fetchLedgerSequence;
3757
4176
  exports.fetchManifest = fetchManifest;
4177
+ exports.getEligibleAssets = getEligibleAssets;
3758
4178
  exports.httpStatusToError = httpStatusToError;
3759
4179
  exports.invalidateManifest = invalidateManifest;
4180
+ exports.isAssetEligible = isAssetEligible;
3760
4181
  exports.manifestDigest = manifestDigest;
4182
+ exports.noopLogger = noopLogger;
4183
+ exports.normalizeManifestAssets = normalizeManifestAssets;
3761
4184
  exports.parseRetryAfterMs = parseRetryAfterMs;
3762
4185
  exports.paymentContextFromManifest = paymentContextFromManifest;
3763
4186
  exports.prepareNulthSigner = prepareNulthSigner;
3764
4187
  exports.rankProvidersByLatency = rankProvidersByLatency;
3765
4188
  exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
3766
4189
  exports.registerProvider = registerProvider;
4190
+ exports.resolveAssetContract = resolveAssetContract;
4191
+ exports.resolveLogger = resolveLogger;
3767
4192
  exports.routedock = routedock;
3768
4193
  exports.runStartupReconciliation = runStartupReconciliation;
4194
+ exports.selectAsset = selectAsset;
3769
4195
  exports.selectMode = selectMode;
3770
4196
  exports.setNulthPaymentContext = setNulthPaymentContext;
3771
4197
  exports.signManifest = signManifest;
@@ -3774,3 +4200,4 @@ exports.usdcToUnits = usdcToUnits;
3774
4200
  exports.verifyManifestSignature = verifyManifestSignature;
3775
4201
  exports.withRetry = withRetry;
3776
4202
  exports.wrapFetchError = wrapFetchError;
4203
+ exports.wrapMppError = wrapMppError;