@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/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.0"};
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 = [];
@@ -1942,7 +2219,10 @@ var NulthClient = class {
1942
2219
  `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
1943
2220
  );
1944
2221
  }
1945
- console.warn("NulthClient is using the insecure mock prover; proofs are not cryptographically sound");
2222
+ resolveLogger(config.logger)(
2223
+ "warn",
2224
+ "NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
2225
+ );
1946
2226
  }
1947
2227
  config;
1948
2228
  policy;
@@ -1994,6 +2274,12 @@ var NulthClient = class {
1994
2274
  return proof;
1995
2275
  }
1996
2276
  enforcePolicy(context) {
2277
+ if (context.amountStroops < 0n) {
2278
+ throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
2279
+ }
2280
+ if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
2281
+ throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
2282
+ }
1997
2283
  if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
1998
2284
  throw new NulthPolicyError("session_expired", "Nulth session expired");
1999
2285
  }
@@ -2137,7 +2423,8 @@ var InMemorySpendStore = class {
2137
2423
  state = null;
2138
2424
  constructor(options = {}) {
2139
2425
  if (options.warn !== false) {
2140
- console.warn(
2426
+ resolveLogger(options.logger)(
2427
+ "warn",
2141
2428
  "[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
2429
  );
2143
2430
  }
@@ -2227,7 +2514,10 @@ var RouteDockClient = class _RouteDockClient {
2227
2514
  this.network = config.network;
2228
2515
  this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
2229
2516
  this.retryPolicy = config.retryPolicy;
2230
- this.spendStore = config.spendStore ?? new InMemorySpendStore({ warn: !!config.spendCap });
2517
+ this.spendStore = config.spendStore ?? new InMemorySpendStore({
2518
+ warn: !!config.spendCap,
2519
+ ...config.logger && { logger: config.logger }
2520
+ });
2231
2521
  this.logger = config.logger;
2232
2522
  this.manifestTimeoutMs = config.manifestTimeoutMs;
2233
2523
  this.expectedPayee = config.expectedPayee;
@@ -2239,9 +2529,10 @@ var RouteDockClient = class _RouteDockClient {
2239
2529
  _secrets.set(this, config.commitmentSecret);
2240
2530
  }
2241
2531
  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);
2532
+ const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
2533
+ this.x402 = new X402Client(secretKey, this.network, retryPolicy);
2534
+ this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
2535
+ this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
2245
2536
  }
2246
2537
  /** Fetch manifest and select mode — shared by pay() and estimateCost(). */
2247
2538
  async _resolveManifest(url, options) {
@@ -2270,12 +2561,13 @@ var RouteDockClient = class _RouteDockClient {
2270
2561
  * call before committing to a payment — for approval gates and manual
2271
2562
  * trustline remediation.
2272
2563
  */
2273
- async preflight(manifest) {
2564
+ async preflight(manifest, mode, endpoint) {
2274
2565
  assertManifestValid(manifest);
2275
- await this._checkTrustline(manifest);
2566
+ const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
2567
+ await this._checkTrustline(manifest, selectedAsset);
2276
2568
  return {
2277
2569
  hasTrustline: true,
2278
- asset: manifest.asset,
2570
+ asset: selectedAsset,
2279
2571
  modes: manifest.modes,
2280
2572
  ...manifest.regions && { regions: manifest.regions },
2281
2573
  ...manifest.latency_hints && { latency_hints: manifest.latency_hints },
@@ -2289,8 +2581,8 @@ var RouteDockClient = class _RouteDockClient {
2289
2581
  * trustline is missing. Results are cached per (account, asset) so the
2290
2582
  * per-payment overhead is one RPC call amortized to zero after the first.
2291
2583
  */
2292
- async _checkTrustline(manifest) {
2293
- const cacheKey = `${this.network}:${this.keypair.publicKey()}:${manifest.asset}`;
2584
+ async _checkTrustline(manifest, targetAsset = manifest.asset) {
2585
+ const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
2294
2586
  const cached = _RouteDockClient._trustlineCache.get(cacheKey);
2295
2587
  if (cached && Date.now() < cached.expiresAt) return;
2296
2588
  const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
@@ -2298,13 +2590,13 @@ var RouteDockClient = class _RouteDockClient {
2298
2590
  try {
2299
2591
  const account = await server.loadAccount(this.keypair.publicKey());
2300
2592
  const balances = account.balances;
2301
- const expectedIssuer = getAssetIssuer(manifest.asset, this.network);
2593
+ const expectedIssuer = getAssetIssuer(targetAsset, this.network);
2302
2594
  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)
2595
+ (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
2596
  );
2305
2597
  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);
2598
+ 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}`;
2599
+ throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
2308
2600
  }
2309
2601
  _RouteDockClient._trustlineCache.set(cacheKey, {
2310
2602
  exists: true,
@@ -2313,7 +2605,8 @@ var RouteDockClient = class _RouteDockClient {
2313
2605
  } catch (err) {
2314
2606
  if (err instanceof RouteDockTrustlineError) throw err;
2315
2607
  this.logger?.(
2316
- `[RouteDock] Trustline preflight: could not verify trustline for ${manifest.asset} \u2014 continuing`
2608
+ "warn",
2609
+ `[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
2317
2610
  );
2318
2611
  }
2319
2612
  }
@@ -2321,18 +2614,18 @@ var RouteDockClient = class _RouteDockClient {
2321
2614
  * Pay for one request at `url`. Fetches manifest, selects payment mode,
2322
2615
  * runs trustline preflight, reserves local spend cap BEFORE executing the
2323
2616
  * 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.
2617
+ * reservation if the on-chain payment fails, and also when the call
2618
+ * completes without paying anything (an x402 endpoint answering a plain
2619
+ * 200 with no challenge). Concurrent pay() calls are serialized through a
2620
+ * per-instance mutex to prevent spend-cap overrun.
2326
2621
  */
2327
2622
  async pay(url, options) {
2328
2623
  const baseUrl = new URL(url).origin;
2329
2624
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2330
2625
  assertEndpointActive(manifest, url, this.logger);
2331
2626
  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
- }
2627
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2628
+ await this._checkTrustline(manifest, selectedAsset);
2336
2629
  let amount;
2337
2630
  switch (mode) {
2338
2631
  case "x402":
@@ -2352,22 +2645,30 @@ var RouteDockClient = class _RouteDockClient {
2352
2645
  const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
2353
2646
  let result;
2354
2647
  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}`);
2648
+ if (this.vault?.mode === "nulth") {
2649
+ result = await this._payWithNulthVault(url, manifest, mode);
2650
+ } else {
2651
+ switch (mode) {
2652
+ case "x402":
2653
+ result = await this.x402.pay(url, manifest);
2654
+ break;
2655
+ case "mpp-charge":
2656
+ result = await this.charge.pay(url, manifest);
2657
+ break;
2658
+ default:
2659
+ throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2660
+ }
2364
2661
  }
2365
2662
  } catch (err) {
2366
2663
  await this._rollbackSpend(reserveId).catch(() => {
2367
2664
  });
2368
2665
  throw err;
2369
2666
  }
2370
- await this._commitSpend(reserveId);
2667
+ if (result.txHash === null && result.amount === "0") {
2668
+ await this._rollbackSpend(reserveId);
2669
+ } else {
2670
+ await this._commitSpend(reserveId);
2671
+ }
2371
2672
  return result;
2372
2673
  }
2373
2674
  /** Nulth ZK vault path — proof built off-chain, attached as auth signature */
@@ -2417,9 +2718,10 @@ var RouteDockClient = class _RouteDockClient {
2417
2718
  default:
2418
2719
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2419
2720
  }
2721
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2420
2722
  return {
2421
2723
  amount,
2422
- asset: manifest.asset,
2724
+ asset: selectedAsset,
2423
2725
  mode,
2424
2726
  manifest,
2425
2727
  ...manifest.regions && { regions: manifest.regions },
@@ -2656,7 +2958,7 @@ var SupabaseSeenTxStore = class {
2656
2958
  }
2657
2959
  supabase;
2658
2960
  async get(key) {
2659
- const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
2961
+ const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
2660
2962
  if (error) {
2661
2963
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
2662
2964
  }
@@ -2664,7 +2966,8 @@ var SupabaseSeenTxStore = class {
2664
2966
  const headers = data.headers;
2665
2967
  return {
2666
2968
  txHash: data.tx_hash ?? null,
2667
- ...headers ? { headers } : {}
2969
+ ...headers ? { headers } : {},
2970
+ createdAt: Date.parse(data.created_at)
2668
2971
  };
2669
2972
  }
2670
2973
  async set(key, record) {
@@ -2672,7 +2975,8 @@ var SupabaseSeenTxStore = class {
2672
2975
  {
2673
2976
  key,
2674
2977
  tx_hash: record.txHash,
2675
- headers: record.headers ?? null
2978
+ headers: record.headers ?? null,
2979
+ created_at: new Date(record.createdAt ?? Date.now()).toISOString()
2676
2980
  },
2677
2981
  { onConflict: "key" }
2678
2982
  );
@@ -2680,21 +2984,47 @@ var SupabaseSeenTxStore = class {
2680
2984
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
2681
2985
  }
2682
2986
  }
2987
+ /**
2988
+ * Atomically increment the settlement's replay counter through the
2989
+ * `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
2990
+ * row lock gives concurrent claims distinct counts. Returns `Infinity` when
2991
+ * the key no longer exists, so the caller treats it as spent.
2992
+ */
2993
+ async claimReplay(key) {
2994
+ const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
2995
+ p_key: key
2996
+ });
2997
+ if (error) {
2998
+ throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
2999
+ }
3000
+ if (data == null) return Number.POSITIVE_INFINITY;
3001
+ return data;
3002
+ }
2683
3003
  };
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
3004
  function extractPayerAddress(key) {
2691
3005
  if (typeof key !== "string") return null;
2692
3006
  return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
2693
3007
  }
2694
3008
 
2695
3009
  // src/provider/SeenTxStore.ts
3010
+ var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
3011
+ var MAX_SETTLEMENT_REPLAYS = 1;
3012
+ async function checkSettlementReplay(store, key, now = Date.now()) {
3013
+ const record = await store.get(key);
3014
+ if (!record) return { kind: "miss" };
3015
+ const { createdAt } = record;
3016
+ if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
3017
+ return { kind: "spent" };
3018
+ }
3019
+ if (store.claimReplay) {
3020
+ const replayed = await store.claimReplay(key);
3021
+ if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
3022
+ }
3023
+ return { kind: "replay", record };
3024
+ }
2696
3025
  var InMemorySeenTxStore = class {
2697
3026
  map = /* @__PURE__ */ new Map();
3027
+ replayCounts = /* @__PURE__ */ new Map();
2698
3028
  order = [];
2699
3029
  constructor(maxEntriesOrOptions = 1e4) {
2700
3030
  const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
@@ -2702,7 +3032,8 @@ var InMemorySeenTxStore = class {
2702
3032
  if (options.warn !== false) {
2703
3033
  const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
2704
3034
  if (isServerless) {
2705
- console.warn(
3035
+ resolveLogger(options.logger)(
3036
+ "warn",
2706
3037
  "[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
3038
  );
2708
3039
  }
@@ -2717,21 +3048,43 @@ var InMemorySeenTxStore = class {
2717
3048
  this.order.push(key);
2718
3049
  if (this.order.length > this.maxEntries) {
2719
3050
  const evicted = this.order.shift();
2720
- if (evicted !== void 0) this.map.delete(evicted);
3051
+ if (evicted !== void 0) {
3052
+ this.map.delete(evicted);
3053
+ this.replayCounts.delete(evicted);
3054
+ }
2721
3055
  }
2722
3056
  }
2723
3057
  this.map.set(key, record);
2724
3058
  }
3059
+ claimReplay(key) {
3060
+ if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
3061
+ const next = (this.replayCounts.get(key) ?? 0) + 1;
3062
+ this.replayCounts.set(key, next);
3063
+ return next;
3064
+ }
2725
3065
  };
2726
3066
  async function sha256Hex2(input) {
2727
3067
  const buf = new TextEncoder().encode(input);
2728
3068
  const digest = await crypto.subtle.digest("SHA-256", buf);
2729
3069
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
2730
3070
  }
2731
- async function paymentIdempotencyKey(getHeader) {
3071
+ async function paymentIdempotencyKey(getHeader, scope) {
2732
3072
  const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
2733
3073
  if (!material) return null;
2734
- return sha256Hex2(material);
3074
+ return sha256Hex2(
3075
+ [scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
3076
+ );
3077
+ }
3078
+
3079
+ // src/provider/settleResult.ts
3080
+ function readSettleResult(result) {
3081
+ const settle = result;
3082
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
3083
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
3084
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
3085
+ return failed(settle.errorReason);
3086
+ }
3087
+ return { ok: true, txHash: settle.transaction };
2735
3088
  }
2736
3089
 
2737
3090
  // src/provider/x402Handler.ts
@@ -2744,7 +3097,8 @@ function createX402Handler(opts) {
2744
3097
  const caip2 = CAIP22[opts.network];
2745
3098
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
2746
3099
  const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
2747
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
3100
+ const logger = resolveLogger(opts.logger);
3101
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
2748
3102
  const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
2749
3103
  const localFacilitator = new facilitator.ExactStellarScheme([signer], {
2750
3104
  areFeesSponsored: true
@@ -2765,22 +3119,29 @@ function createX402Handler(opts) {
2765
3119
  }
2766
3120
  const amountInBaseUnits = String(usdcToUnits(opts.amount));
2767
3121
  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
3122
  return async (req, res, next) => {
2781
3123
  try {
2782
- const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
2783
- if (!paymentHeader) {
3124
+ const endpoint = req.path || req.originalUrl;
3125
+ const assetContract = resolveAssetContract(
3126
+ opts.manifest,
3127
+ "x402",
3128
+ endpoint,
3129
+ opts.assetContract
3130
+ );
3131
+ const requirements = {
3132
+ scheme: "exact",
3133
+ network: caip2,
3134
+ asset: assetContract,
3135
+ amount: amountInBaseUnits,
3136
+ payTo,
3137
+ maxTimeoutSeconds: 60,
3138
+ extra: {
3139
+ areFeesSponsored: true,
3140
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
3141
+ }
3142
+ };
3143
+ const respondPaymentRequired = async (error, reason) => {
3144
+ const body = { error, ...reason ? { reason } : {} };
2784
3145
  if (ozServer) {
2785
3146
  const resourceInfo = {
2786
3147
  url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
@@ -2790,7 +3151,7 @@ function createX402Handler(opts) {
2790
3151
  [requirements],
2791
3152
  resourceInfo
2792
3153
  );
2793
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json({ error: "Payment Required" });
3154
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(body);
2794
3155
  } else {
2795
3156
  const x402Response = {
2796
3157
  x402Version: 2,
@@ -2800,19 +3161,35 @@ function createX402Handler(opts) {
2800
3161
  },
2801
3162
  accepts: [requirements]
2802
3163
  };
2803
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
3164
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(body);
2804
3165
  }
3166
+ };
3167
+ const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
3168
+ if (!paymentHeader) {
3169
+ await respondPaymentRequired("Payment Required");
2805
3170
  return;
2806
3171
  }
2807
- const idempotencyKey = await paymentIdempotencyKey((name) => {
2808
- const v = req.headers[name.toLowerCase()];
2809
- return Array.isArray(v) ? v[0] : v;
2810
- });
3172
+ const idempotencyKey = await paymentIdempotencyKey(
3173
+ (name) => {
3174
+ const v = req.headers[name.toLowerCase()];
3175
+ return Array.isArray(v) ? v[0] : v;
3176
+ },
3177
+ {
3178
+ method: req.method,
3179
+ path: req.originalUrl.split("?")[0],
3180
+ amount: requirements.amount,
3181
+ payTo: requirements.payTo
3182
+ }
3183
+ );
2811
3184
  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)) {
3185
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
3186
+ if (replayCheck.kind === "spent") {
3187
+ res.status(402).json({ error: "Payment already used" });
3188
+ return;
3189
+ }
3190
+ if (replayCheck.kind === "replay") {
3191
+ if (replayCheck.record.headers) {
3192
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
2816
3193
  res.setHeader(k, val);
2817
3194
  }
2818
3195
  }
@@ -2831,15 +3208,18 @@ function createX402Handler(opts) {
2831
3208
  }
2832
3209
  if (ozServer) {
2833
3210
  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
- );
3211
+ const outcome = readSettleResult(settleResult);
3212
+ if (!outcome.ok) {
3213
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
3214
+ return;
2842
3215
  }
3216
+ txHash = outcome.txHash;
3217
+ res.setHeader(
3218
+ "X-Payment-Response",
3219
+ http.encodePaymentResponseHeader(
3220
+ settleResult
3221
+ )
3222
+ );
2843
3223
  } else {
2844
3224
  const verifyResult = await localFacilitator.verify(
2845
3225
  payload,
@@ -2856,15 +3236,18 @@ function createX402Handler(opts) {
2856
3236
  payload,
2857
3237
  requirements
2858
3238
  );
2859
- txHash = settleResult.transaction ?? null;
2860
- if (settleResult) {
2861
- res.setHeader(
2862
- "X-Payment-Response",
2863
- http.encodePaymentResponseHeader(
2864
- settleResult
2865
- )
2866
- );
3239
+ const outcome = readSettleResult(settleResult);
3240
+ if (!outcome.ok) {
3241
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
3242
+ return;
2867
3243
  }
3244
+ txHash = outcome.txHash;
3245
+ res.setHeader(
3246
+ "X-Payment-Response",
3247
+ http.encodePaymentResponseHeader(
3248
+ settleResult
3249
+ )
3250
+ );
2868
3251
  }
2869
3252
  if (idempotencyKey) {
2870
3253
  const headers = {};
@@ -2872,17 +3255,21 @@ function createX402Handler(opts) {
2872
3255
  if (typeof paymentResponse === "string") {
2873
3256
  headers["X-Payment-Response"] = paymentResponse;
2874
3257
  }
2875
- await seenTxStore.set(idempotencyKey, { txHash, headers });
3258
+ await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
2876
3259
  }
2877
3260
  if (txHash && opts.onSettled) {
2878
3261
  Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
2879
- console.error("[x402] onSettled callback error:", err);
3262
+ logger("error", "[x402] onSettled callback error", { error: err });
2880
3263
  opts.onCallbackError?.(err, "onSettled");
2881
3264
  });
2882
3265
  }
2883
3266
  next();
2884
3267
  } catch (err) {
2885
- console.error("[x402] Settlement error:", err);
3268
+ if (err instanceof RouteDockManifestError) {
3269
+ next(err);
3270
+ return;
3271
+ }
3272
+ logger("error", "[x402] Settlement error", { error: err });
2886
3273
  res.status(500).json({ error: "Payment settlement failed" });
2887
3274
  }
2888
3275
  };
@@ -2895,20 +3282,37 @@ function createMppChargeHandler(opts) {
2895
3282
  const networkId = MPP_NETWORK[opts.network];
2896
3283
  const amountHumanReadable = opts.amount;
2897
3284
  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
- });
3285
+ const logger = resolveLogger(opts.logger);
3286
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
3287
+ const mppxInstances = /* @__PURE__ */ new Map();
3288
+ function getMppx(contract) {
3289
+ let instance = mppxInstances.get(contract);
3290
+ if (!instance) {
3291
+ instance = server$2.Mppx.create({
3292
+ secretKey: opts.payeeSecretKey,
3293
+ methods: [
3294
+ server$4.stellar.charge({
3295
+ recipient,
3296
+ currency: contract,
3297
+ network: networkId,
3298
+ feePayer: { envelopeSigner: opts.payeeSecretKey }
3299
+ })
3300
+ ]
3301
+ });
3302
+ mppxInstances.set(contract, instance);
3303
+ }
3304
+ return instance;
3305
+ }
2910
3306
  return async (req, res, next) => {
2911
3307
  try {
3308
+ const endpoint = req.path || req.originalUrl;
3309
+ const assetContract = resolveAssetContract(
3310
+ opts.manifest,
3311
+ "mpp-charge",
3312
+ endpoint,
3313
+ opts.assetContract
3314
+ );
3315
+ const mppx = getMppx(assetContract);
2912
3316
  let payerAddress = null;
2913
3317
  try {
2914
3318
  const authHeader = req.headers["authorization"];
@@ -2924,15 +3328,27 @@ function createMppChargeHandler(opts) {
2924
3328
  }
2925
3329
  } catch {
2926
3330
  }
2927
- const idempotencyKey = await paymentIdempotencyKey((name) => {
2928
- const v = req.headers[name.toLowerCase()];
2929
- return Array.isArray(v) ? v[0] : v;
2930
- });
3331
+ const idempotencyKey = await paymentIdempotencyKey(
3332
+ (name) => {
3333
+ const v = req.headers[name.toLowerCase()];
3334
+ return Array.isArray(v) ? v[0] : v;
3335
+ },
3336
+ {
3337
+ method: req.method,
3338
+ path: req.originalUrl.split("?")[0],
3339
+ amount: amountHumanReadable,
3340
+ payTo: recipient
3341
+ }
3342
+ );
2931
3343
  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)) {
3344
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
3345
+ if (replayCheck.kind === "spent") {
3346
+ res.status(402).json({ error: "Payment already used" });
3347
+ return;
3348
+ }
3349
+ if (replayCheck.kind === "replay") {
3350
+ if (replayCheck.record.headers) {
3351
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
2936
3352
  res.setHeader(k, val);
2937
3353
  }
2938
3354
  }
@@ -2944,7 +3360,7 @@ function createMppChargeHandler(opts) {
2944
3360
  const handler = mppx["stellar/charge"];
2945
3361
  const result = await handler({
2946
3362
  amount: amountHumanReadable,
2947
- currency: opts.assetContract,
3363
+ currency: assetContract,
2948
3364
  recipient,
2949
3365
  description: opts.manifest.name
2950
3366
  })(fetchReq);
@@ -2975,12 +3391,13 @@ function createMppChargeHandler(opts) {
2975
3391
  if (idempotencyKey) {
2976
3392
  await seenTxStore.set(idempotencyKey, {
2977
3393
  txHash: reference ?? null,
2978
- headers: receiptHeaders
3394
+ headers: receiptHeaders,
3395
+ createdAt: Date.now()
2979
3396
  });
2980
3397
  }
2981
3398
  if (reference && opts.onSettled) {
2982
3399
  Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
2983
- console.error("[mpp-charge] onSettled callback error:", err);
3400
+ logger("error", "[mpp-charge] onSettled callback error", { error: err });
2984
3401
  opts.onCallbackError?.(err, "onSettled");
2985
3402
  });
2986
3403
  }
@@ -3081,8 +3498,9 @@ function createMppSessionHandler(opts) {
3081
3498
  const networkId = MPP_NETWORK2[opts.network];
3082
3499
  const rateHuman = opts.rate;
3083
3500
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
3501
+ const logger = resolveLogger(opts.logger);
3084
3502
  const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
3085
- const innerStore = server$4.Store.memory();
3503
+ const innerStore = server$3.Store.memory();
3086
3504
  let record = null;
3087
3505
  let voucherCount = 0;
3088
3506
  let sessionOpened = false;
@@ -3132,7 +3550,7 @@ function createMppSessionHandler(opts) {
3132
3550
  reason
3133
3551
  });
3134
3552
  } catch (err) {
3135
- console.error("[mpp-session] onOrphaned handler failed:", err);
3553
+ logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
3136
3554
  }
3137
3555
  }
3138
3556
  }
@@ -3179,7 +3597,7 @@ function createMppSessionHandler(opts) {
3179
3597
  sessionOpened = true;
3180
3598
  if (opts.onSessionOpen) {
3181
3599
  Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
3182
- console.error("[mpp-session] onSessionOpen callback error:", err);
3600
+ logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
3183
3601
  opts.onCallbackError?.(err, "onSessionOpen");
3184
3602
  });
3185
3603
  }
@@ -3187,7 +3605,7 @@ function createMppSessionHandler(opts) {
3187
3605
  if (opts.onVoucher) {
3188
3606
  const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
3189
3607
  Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
3190
- console.error("[mpp-session] onVoucher callback error:", err);
3608
+ logger("error", "[mpp-session] onVoucher callback error", { error: err });
3191
3609
  opts.onCallbackError?.(err, "onVoucher");
3192
3610
  });
3193
3611
  }
@@ -3197,7 +3615,7 @@ function createMppSessionHandler(opts) {
3197
3615
  methods: [
3198
3616
  withTypedChannelErrors(
3199
3617
  onVerifiedCredential(
3200
- server$4.stellar.channel({
3618
+ server$3.stellar.channel({
3201
3619
  channel: opts.channelFactory,
3202
3620
  commitmentKey: opts.commitmentPublicKey,
3203
3621
  network: networkId,
@@ -3228,7 +3646,7 @@ function createMppSessionHandler(opts) {
3228
3646
  }
3229
3647
  if (closeAmount > 0n && closeSig) {
3230
3648
  const closePayer = record?.payer ?? null;
3231
- const closeTxHash = await server$4.close({
3649
+ const closeTxHash = await server$3.close({
3232
3650
  channel: opts.channelFactory,
3233
3651
  amount: closeAmount,
3234
3652
  signature: Buffer.from(closeSig, "hex"),
@@ -3240,7 +3658,7 @@ function createMppSessionHandler(opts) {
3240
3658
  if (opts.onSettled) {
3241
3659
  const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
3242
3660
  Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
3243
- console.error("[mpp-session] onSettled callback error:", err);
3661
+ logger("error", "[mpp-session] onSettled callback error", { error: err });
3244
3662
  opts.onCallbackError?.(err, "onSettled");
3245
3663
  });
3246
3664
  }
@@ -3251,7 +3669,7 @@ function createMppSessionHandler(opts) {
3251
3669
  payeeKeypair.publicKey()
3252
3670
  );
3253
3671
  if (!settlementAddresses) {
3254
- console.error("[mpp-session] skipped session_settled vault record: payer address unavailable");
3672
+ logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
3255
3673
  } else {
3256
3674
  try {
3257
3675
  const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
@@ -3274,7 +3692,7 @@ function createMppSessionHandler(opts) {
3274
3692
  preparedTx.sign(payeeKeypair);
3275
3693
  await server.sendTransaction(preparedTx);
3276
3694
  } catch (recordErr) {
3277
- console.error("[mpp-session] failed to record session_settled on vault:", recordErr);
3695
+ logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
3278
3696
  }
3279
3697
  }
3280
3698
  }
@@ -3331,12 +3749,13 @@ function routedock(opts) {
3331
3749
  payeeSecretKey: opts.payeeSecretKey,
3332
3750
  network: opts.network,
3333
3751
  amount: x402Price,
3334
- assetContract: opts.assetContract,
3752
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3335
3753
  ...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
3336
3754
  manifest: signedManifest,
3337
3755
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
3338
3756
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3339
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
3757
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
3758
+ ...opts.logger ? { logger: opts.logger } : {}
3340
3759
  })
3341
3760
  );
3342
3761
  }
@@ -3350,11 +3769,12 @@ function routedock(opts) {
3350
3769
  payeeSecretKey: opts.payeeSecretKey,
3351
3770
  network: opts.network,
3352
3771
  amount: chargePrice,
3353
- assetContract: opts.assetContract,
3772
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3354
3773
  manifest: signedManifest,
3355
3774
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
3356
3775
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3357
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
3776
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
3777
+ ...opts.logger ? { logger: opts.logger } : {}
3358
3778
  })
3359
3779
  );
3360
3780
  }
@@ -3372,7 +3792,7 @@ function routedock(opts) {
3372
3792
  network: opts.network,
3373
3793
  channelFactory: sessionPricing.channelFactory,
3374
3794
  rate: sessionPricing.rate,
3375
- assetContract: opts.assetContract,
3795
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3376
3796
  manifest: signedManifest,
3377
3797
  commitmentPublicKey: opts.commitmentPublicKey,
3378
3798
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
@@ -3380,7 +3800,8 @@ function routedock(opts) {
3380
3800
  ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
3381
3801
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3382
3802
  ...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
3383
- ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
3803
+ ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
3804
+ ...opts.logger ? { logger: opts.logger } : {}
3384
3805
  })
3385
3806
  );
3386
3807
  }
@@ -3544,25 +3965,26 @@ async function reconcileAbandonedSessions(opts) {
3544
3965
  }
3545
3966
  }
3546
3967
  async function runStartupReconciliation(opts, logger) {
3547
- const log = logger || console.log;
3548
- log("[SessionReconciler] Running startup reconciliation...");
3968
+ const log = resolveLogger(logger);
3969
+ log("info", "[SessionReconciler] Running startup reconciliation...");
3549
3970
  try {
3550
3971
  const stats = await reconcileAbandonedSessions(opts);
3551
3972
  if (stats.orphanedCount === 0) {
3552
- log("[SessionReconciler] No orphaned sessions found");
3973
+ log("info", "[SessionReconciler] No orphaned sessions found");
3553
3974
  return;
3554
3975
  }
3555
3976
  log(
3977
+ "info",
3556
3978
  `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
3557
3979
  );
3558
3980
  if (stats.errors.length > 0) {
3559
- log("[SessionReconciler] Reconciliation errors:");
3981
+ log("warn", "[SessionReconciler] Reconciliation errors:");
3560
3982
  for (const err of stats.errors) {
3561
- log(` - ${err.channelId}: ${err.reason}`);
3983
+ log("warn", ` - ${err.channelId}: ${err.reason}`);
3562
3984
  }
3563
3985
  }
3564
3986
  } catch (err) {
3565
- log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
3987
+ log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
3566
3988
  }
3567
3989
  }
3568
3990
  function tryDecodeBase64(value) {
@@ -3749,23 +4171,32 @@ exports.assertManifestValid = assertManifestValid;
3749
4171
  exports.assertNulthVaultManifest = assertNulthVaultManifest;
3750
4172
  exports.backoffDelayMs = backoffDelayMs;
3751
4173
  exports.configureManifestCache = configureManifestCache;
4174
+ exports.consoleLogger = consoleLogger;
4175
+ exports.createConsoleLogger = createConsoleLogger;
3752
4176
  exports.createNulthSigner = createNulthSigner;
3753
4177
  exports.createPolicyState = createPolicyState;
3754
4178
  exports.decimalToStroops = decimalToStroops;
3755
4179
  exports.decodeAuthSignature = decodeAuthSignature;
3756
4180
  exports.fetchLedgerSequence = fetchLedgerSequence;
3757
4181
  exports.fetchManifest = fetchManifest;
4182
+ exports.getEligibleAssets = getEligibleAssets;
3758
4183
  exports.httpStatusToError = httpStatusToError;
3759
4184
  exports.invalidateManifest = invalidateManifest;
4185
+ exports.isAssetEligible = isAssetEligible;
3760
4186
  exports.manifestDigest = manifestDigest;
4187
+ exports.noopLogger = noopLogger;
4188
+ exports.normalizeManifestAssets = normalizeManifestAssets;
3761
4189
  exports.parseRetryAfterMs = parseRetryAfterMs;
3762
4190
  exports.paymentContextFromManifest = paymentContextFromManifest;
3763
4191
  exports.prepareNulthSigner = prepareNulthSigner;
3764
4192
  exports.rankProvidersByLatency = rankProvidersByLatency;
3765
4193
  exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
3766
4194
  exports.registerProvider = registerProvider;
4195
+ exports.resolveAssetContract = resolveAssetContract;
4196
+ exports.resolveLogger = resolveLogger;
3767
4197
  exports.routedock = routedock;
3768
4198
  exports.runStartupReconciliation = runStartupReconciliation;
4199
+ exports.selectAsset = selectAsset;
3769
4200
  exports.selectMode = selectMode;
3770
4201
  exports.setNulthPaymentContext = setNulthPaymentContext;
3771
4202
  exports.signManifest = signManifest;
@@ -3774,3 +4205,4 @@ exports.usdcToUnits = usdcToUnits;
3774
4205
  exports.verifyManifestSignature = verifyManifestSignature;
3775
4206
  exports.withRetry = withRetry;
3776
4207
  exports.wrapFetchError = wrapFetchError;
4208
+ exports.wrapMppError = wrapMppError;