@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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { usdcToStroops } from '@routedock/nulth-sdk';
1
+ import { usdcToStroops, stroopsToUsdc } from '@routedock/nulth-sdk';
2
2
  export { USDC_DECIMALS, usdcToStroops } from '@routedock/nulth-sdk';
3
3
  import { Keypair, rpc, Horizon, xdr, Address, scValToNative, hash, Networks, StrKey, Contract, nativeToScVal, TransactionBuilder, BASE_FEE } from '@stellar/stellar-sdk';
4
4
  import { Validator } from '@cfworker/json-schema';
@@ -17,9 +17,9 @@ import path from 'path';
17
17
  import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/facilitator';
18
18
  import { ExactStellarScheme as ExactStellarScheme$2 } from '@x402/stellar/exact/server';
19
19
  import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
20
- import { stellar as stellar$2 } from '@stellar/mpp/charge/server';
21
- import { Mppx as Mppx$1, Request } from 'mppx/server';
22
- import { Store, stellar as stellar$3, close } from '@stellar/mpp/channel/server';
20
+ import { stellar as stellar$3 } from '@stellar/mpp/charge/server';
21
+ import { Request, Mppx as Mppx$1 } from 'mppx/server';
22
+ import { Store, stellar as stellar$2, close } from '@stellar/mpp/channel/server';
23
23
 
24
24
  // src/errors.ts
25
25
  var RouteDockError = class extends Error {
@@ -165,10 +165,35 @@ function wrapFetchError(err, context) {
165
165
  if (err instanceof RouteDockError) return err;
166
166
  return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
167
167
  }
168
+ function wrapMppError(err, context) {
169
+ if (err instanceof RouteDockError) return err;
170
+ if (err instanceof TypeError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
171
+ return new RouteDockNetworkError(`${context}: ${String(err)}`, { cause: err });
172
+ }
173
+ return new RouteDockSignatureError(`${context}: ${String(err)}`, { cause: err });
174
+ }
168
175
 
169
176
  // src/types.ts
170
177
  var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
171
178
 
179
+ // src/internal/logger.ts
180
+ function createConsoleLogger() {
181
+ return (level, message, fields) => {
182
+ const values = fields ? Object.values(fields) : [];
183
+ if (values.length > 0) {
184
+ console[level](message, ...values);
185
+ } else {
186
+ console[level](message);
187
+ }
188
+ };
189
+ }
190
+ var consoleLogger = createConsoleLogger();
191
+ var noopLogger = () => {
192
+ };
193
+ function resolveLogger(logger, fallback = consoleLogger) {
194
+ return logger ?? fallback;
195
+ }
196
+
172
197
  // src/internal/retry.ts
173
198
  var DEFAULT_RETRY_POLICY = {
174
199
  maxAttempts: 4,
@@ -183,10 +208,11 @@ function sleep(ms) {
183
208
  return new Promise((resolve) => setTimeout(resolve, ms));
184
209
  }
185
210
  async function withRetry(fn, policy = {}) {
186
- const { maxAttempts, baseDelayMs, maxDelayMs, onRetry } = {
211
+ const { maxAttempts, baseDelayMs, maxDelayMs, onRetry, logger } = {
187
212
  ...DEFAULT_RETRY_POLICY,
188
213
  ...policy
189
214
  };
215
+ const log = resolveLogger(logger);
190
216
  let lastError;
191
217
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
192
218
  try {
@@ -204,7 +230,7 @@ async function withRetry(fn, policy = {}) {
204
230
  try {
205
231
  await onRetry(attempt, err, delay);
206
232
  } catch (cbErr) {
207
- console.error("[withRetry] onRetry callback threw:", cbErr);
233
+ log("error", "[withRetry] onRetry callback threw:", { error: cbErr });
208
234
  }
209
235
  }
210
236
  await sleep(delay);
@@ -218,6 +244,84 @@ var USDC_ISSUERS = {
218
244
  };
219
245
  var usdcToUnits = usdcToStroops;
220
246
 
247
+ // src/internal/assetUtils.ts
248
+ function normalizeManifestAssets(manifest) {
249
+ if (manifest.assets && manifest.assets.length > 0) {
250
+ return manifest.assets;
251
+ }
252
+ if (manifest.asset && manifest.asset_contract) {
253
+ return [
254
+ {
255
+ asset: manifest.asset,
256
+ asset_contract: manifest.asset_contract
257
+ }
258
+ ];
259
+ }
260
+ throw new RouteDockManifestError(
261
+ "Manifest must define either assets[] or root asset/asset_contract fields"
262
+ );
263
+ }
264
+ function getEligibleAssets(manifest, mode, endpoint) {
265
+ const assets = normalizeManifestAssets(manifest);
266
+ return assets.filter((asset) => {
267
+ if (asset.modes && !asset.modes.includes(mode)) {
268
+ return false;
269
+ }
270
+ if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
271
+ const trimmedEndpoint = endpoint.replace(/^\//, "");
272
+ if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
273
+ return true;
274
+ }
275
+ if (manifest.endpoints) {
276
+ let pathname = endpoint;
277
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
278
+ try {
279
+ pathname = new URL(endpoint).pathname;
280
+ } catch {
281
+ }
282
+ }
283
+ const trimmedPathname = pathname.replace(/^\//, "");
284
+ const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
285
+ const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
286
+ const keyMatches = key === endpoint || key === trimmedEndpoint;
287
+ if (pathMatches || keyMatches) {
288
+ return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
289
+ }
290
+ return false;
291
+ });
292
+ if (!matchesEndpoint) {
293
+ return false;
294
+ }
295
+ } else {
296
+ return false;
297
+ }
298
+ }
299
+ return true;
300
+ });
301
+ }
302
+ function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
303
+ if (manifest.assets && manifest.assets.length > 0) {
304
+ return selectAsset(manifest, mode, endpoint).asset_contract;
305
+ }
306
+ return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
307
+ }
308
+ function selectAsset(manifest, mode, endpoint) {
309
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
310
+ if (eligible.length === 0) {
311
+ const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
312
+ throw new RouteDockManifestError(
313
+ `No eligible assets found for mode '${mode}'${endpointMsg}`
314
+ );
315
+ }
316
+ return eligible[0];
317
+ }
318
+ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
319
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
320
+ return eligible.some(
321
+ (asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
322
+ );
323
+ }
324
+
221
325
  // src/schemas/routedock.schema.json
222
326
  var routedock_schema_default = {
223
327
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -297,6 +401,14 @@ var routedock_schema_default = {
297
401
  type: "string",
298
402
  description: "Stellar Asset Contract (SAC) address for the payment asset"
299
403
  },
404
+ assets: {
405
+ type: "array",
406
+ minItems: 1,
407
+ items: {
408
+ $ref: "#/definitions/AssetConfig"
409
+ },
410
+ description: "Payment assets accepted by this provider. Each asset can be scoped to specific modes and endpoints. When present, assets[0] must match the root-level asset and asset_contract."
411
+ },
300
412
  payee: {
301
413
  type: "string",
302
414
  description: "Stellar address (G...) that receives payments"
@@ -429,6 +541,38 @@ var routedock_schema_default = {
429
541
  enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
430
542
  description: "A supported RouteDock payment mode"
431
543
  },
544
+ AssetConfig: {
545
+ type: "object",
546
+ description: "Payment asset configuration with optional mode and endpoint scoping",
547
+ required: ["asset", "asset_contract"],
548
+ additionalProperties: false,
549
+ properties: {
550
+ asset: {
551
+ type: "string",
552
+ description: "Asset ticker symbol (e.g. 'USDC' or 'XLM')"
553
+ },
554
+ asset_contract: {
555
+ type: "string",
556
+ description: "Stellar Asset Contract (SAC) address for the payment asset"
557
+ },
558
+ modes: {
559
+ type: "array",
560
+ items: {
561
+ $ref: "#/definitions/PaymentMode"
562
+ },
563
+ uniqueItems: true,
564
+ description: "Optional: restrict this asset to specific payment modes. If omitted, available for all modes."
565
+ },
566
+ endpoints: {
567
+ type: "array",
568
+ items: {
569
+ type: "string"
570
+ },
571
+ uniqueItems: true,
572
+ description: "Optional: restrict this asset to specific endpoints (by name). If omitted, available for all endpoints."
573
+ }
574
+ }
575
+ },
432
576
  PricingConfig: {
433
577
  type: "object",
434
578
  description: "Per-request pricing for x402 or mpp-charge modes",
@@ -574,7 +718,7 @@ var routedock_schema_default = {
574
718
 
575
719
  // package.json
576
720
  var package_default = {
577
- version: "0.2.0"};
721
+ version: "0.3.1"};
578
722
  var MANIFEST_SIGNATURE_VERSION = "2";
579
723
  function canonicalize(value) {
580
724
  if (Array.isArray(value)) {
@@ -698,6 +842,7 @@ function assertEndpointActive(manifest, url, logger) {
698
842
  const [key, descriptor] = deprecated;
699
843
  const sunsetSuffix = descriptor.sunset_at ? `; sunset_at ${descriptor.sunset_at}` : "";
700
844
  logger(
845
+ "warn",
701
846
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${pathname}; endpoint '${key}' is deprecated${sunsetSuffix}`
702
847
  );
703
848
  }
@@ -719,6 +864,27 @@ function assertManifestValid(manifest, baseUrl) {
719
864
  }
720
865
  }
721
866
  }
867
+ if (manifest.assets !== void 0) {
868
+ if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) {
869
+ throw new RouteDockManifestError(
870
+ `Invalid manifest${context}: assets must be a non-empty array when defined`
871
+ );
872
+ }
873
+ for (let i = 0; i < manifest.assets.length; i++) {
874
+ const a = manifest.assets[i];
875
+ if (!a || typeof a.asset !== "string" || typeof a.asset_contract !== "string") {
876
+ throw new RouteDockManifestError(
877
+ `Invalid manifest${context}: assets[${i}] must define asset and asset_contract strings`
878
+ );
879
+ }
880
+ }
881
+ const first = manifest.assets[0];
882
+ if (first.asset !== manifest.asset || first.asset_contract !== manifest.asset_contract) {
883
+ throw new RouteDockManifestError(
884
+ `Invalid manifest${context}: assets[0] (${first.asset}:${first.asset_contract}) must match root asset fields (${manifest.asset}:${manifest.asset_contract})`
885
+ );
886
+ }
887
+ }
722
888
  }
723
889
  var CACHE_TTL_MS = 6e4;
724
890
  var DEFAULT_MANIFEST_CACHE_MAX_SIZE = 512;
@@ -898,16 +1064,16 @@ function logSelection(manifest, selection, log, deprecated) {
898
1064
  const reason = selection.reason ? ` (${selection.reason})` : "";
899
1065
  if (deprecated) {
900
1066
  log(
1067
+ "warn",
901
1068
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${selection.mode}${reason}; selected deprecated mode because no active supported mode is available`
902
1069
  );
903
1070
  return;
904
1071
  }
905
- log(`[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
1072
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${selection.mode}${reason}`);
906
1073
  }
907
1074
  function selectMode(manifest, options = {}) {
908
1075
  const modes = manifest.modes;
909
- const log = options.logger ?? (() => {
910
- });
1076
+ const log = options.logger ?? noopLogger;
911
1077
  const deprecatedSet = new Set(manifest.deprecated_modes ?? []);
912
1078
  if (options.forceMode) {
913
1079
  if (!modes.includes(options.forceMode)) {
@@ -917,10 +1083,11 @@ function selectMode(manifest, options = {}) {
917
1083
  }
918
1084
  if (deprecatedSet.has(options.forceMode)) {
919
1085
  log(
1086
+ "warn",
920
1087
  `[RouteDock] WARNING: ${manifest.name} \u2192 ${options.forceMode} (forced); selected mode is deprecated`
921
1088
  );
922
1089
  } else {
923
- log(`[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
1090
+ log("info", `[RouteDock] ${manifest.name} \u2192 ${options.forceMode} (forced)`);
924
1091
  }
925
1092
  return options.forceMode;
926
1093
  }
@@ -956,6 +1123,69 @@ function rankProvidersByLatency(manifests, targetRegion) {
956
1123
  return 0;
957
1124
  });
958
1125
  }
1126
+
1127
+ // src/internal/payee.ts
1128
+ function resolvePayee(manifest, mode) {
1129
+ const override = manifest.pricing[mode]?.payee;
1130
+ return override ?? manifest.payee;
1131
+ }
1132
+
1133
+ // src/client/challenge.ts
1134
+ function isRecord(value) {
1135
+ return typeof value === "object" && value !== null;
1136
+ }
1137
+ function isPositiveIntegerString(value) {
1138
+ return typeof value === "string" && /^[1-9]\d*$/.test(value);
1139
+ }
1140
+ function checkAmount(amount, maxAmount) {
1141
+ if (!isPositiveIntegerString(amount)) {
1142
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1143
+ }
1144
+ let max;
1145
+ try {
1146
+ max = usdcToStroops(maxAmount);
1147
+ } catch {
1148
+ return new RouteDockPolicyRejectError("challenge_amount_invalid");
1149
+ }
1150
+ if (BigInt(amount) > max) {
1151
+ return new RouteDockPolicyRejectError("challenge_amount_exceeds_manifest");
1152
+ }
1153
+ return null;
1154
+ }
1155
+ function checkX402Accept(accept, manifest, network, maxAmount) {
1156
+ if (accept.network !== network) {
1157
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1158
+ }
1159
+ if (accept.payTo !== resolvePayee(manifest, "x402")) {
1160
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1161
+ }
1162
+ if (accept.asset !== manifest.asset_contract) {
1163
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1164
+ }
1165
+ return checkAmount(accept.amount, maxAmount);
1166
+ }
1167
+ function filterX402Accepts(accepts, manifest, network, maxAmount) {
1168
+ return accepts.filter((accept) => checkX402Accept(accept, manifest, network, maxAmount) === null);
1169
+ }
1170
+ function checkChargeChallenge(request, manifest, network, maxAmount) {
1171
+ const methodDetails = request["methodDetails"];
1172
+ const rawNetwork = isRecord(methodDetails) ? methodDetails["network"] : void 0;
1173
+ const challengeNetwork = rawNetwork === void 0 ? "stellar:testnet" : rawNetwork;
1174
+ if (challengeNetwork !== network) {
1175
+ return new RouteDockPolicyRejectError("challenge_network_mismatch");
1176
+ }
1177
+ const recipient = request["recipient"];
1178
+ if (typeof recipient !== "string" || recipient !== resolvePayee(manifest, "mpp-charge")) {
1179
+ return new RouteDockPolicyRejectError("challenge_payee_mismatch");
1180
+ }
1181
+ const currency = request["currency"];
1182
+ if (typeof currency !== "string" || currency !== manifest.asset_contract) {
1183
+ return new RouteDockPolicyRejectError("challenge_asset_mismatch");
1184
+ }
1185
+ return checkAmount(request["amount"], maxAmount);
1186
+ }
1187
+
1188
+ // src/client/x402Client.ts
959
1189
  var CAIP2 = {
960
1190
  testnet: "stellar:testnet",
961
1191
  mainnet: "stellar:pubnet"
@@ -987,83 +1217,106 @@ var X402Client = class _X402Client {
987
1217
  if (!pricing.facilitator) {
988
1218
  throw new RouteDockManifestError("manifest.pricing.x402.facilitator missing");
989
1219
  }
990
- return withRetry(async () => {
991
- let init;
1220
+ const caip2 = CAIP2[this.network];
1221
+ const probe = await withRetry(async () => {
1222
+ let init2;
992
1223
  try {
993
- init = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
1224
+ init2 = await fetch(url, { headers: { "X-Preferred-Mode": "x402" } });
994
1225
  } catch (err) {
995
1226
  throw wrapFetchError(err, "x402 initial request");
996
1227
  }
997
- if (init.status !== 402) {
998
- if (!init.ok) {
1228
+ if (init2.status !== 402) {
1229
+ if (!init2.ok) {
999
1230
  throw httpStatusToError(
1000
- `x402 initial request failed: HTTP ${init.status}`,
1001
- init.status,
1002
- init
1231
+ `x402 initial request failed: HTTP ${init2.status}`,
1232
+ init2.status,
1233
+ init2
1003
1234
  );
1004
1235
  }
1005
1236
  let data2;
1006
1237
  try {
1007
- data2 = await init.json();
1238
+ data2 = await init2.json();
1008
1239
  } catch {
1009
- throw new RouteDockManifestError(`Failed to parse JSON from non-402 response (HTTP ${init.status})`);
1240
+ throw new RouteDockManifestError(
1241
+ `Failed to parse JSON from non-402 response (HTTP ${init2.status})`
1242
+ );
1010
1243
  }
1011
- return { data: data2, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
1012
- }
1013
- const reqHeader = init.headers.get("X-Payment-Requirements");
1014
- if (!reqHeader) {
1015
- throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1244
+ return { kind: "free", data: data2 };
1016
1245
  }
1017
- let paymentRequired;
1018
- try {
1019
- paymentRequired = decodePaymentRequiredHeader(reqHeader);
1020
- } catch (err) {
1021
- throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
1022
- cause: err
1023
- });
1024
- }
1025
- let paymentPayload;
1026
- try {
1027
- paymentPayload = await this.httpClient.createPaymentPayload(paymentRequired);
1028
- } catch (err) {
1029
- throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
1030
- cause: err
1031
- });
1032
- }
1033
- const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1034
- let settled;
1246
+ return { kind: "payment-required", init: init2 };
1247
+ }, this.retryPolicy);
1248
+ if (probe.kind === "free") {
1249
+ return { data: probe.data, txHash: null, mode: "x402", amount: "0", timestamp: Date.now() };
1250
+ }
1251
+ const init = probe.init;
1252
+ const reqHeader = init.headers.get("X-Payment-Requirements");
1253
+ if (!reqHeader) {
1254
+ throw new RouteDockManifestError("402 response missing X-Payment-Requirements header");
1255
+ }
1256
+ let paymentRequired;
1257
+ try {
1258
+ paymentRequired = decodePaymentRequiredHeader(reqHeader);
1259
+ } catch (err) {
1260
+ throw new RouteDockManifestError("402 X-Payment-Requirements header is malformed", {
1261
+ cause: err
1262
+ });
1263
+ }
1264
+ const firstAccept = paymentRequired.accepts[0];
1265
+ if (!firstAccept) {
1266
+ throw new RouteDockManifestError("402 response carries no payment accepts");
1267
+ }
1268
+ const allowed = filterX402Accepts(paymentRequired.accepts, manifest, caip2, pricing.amount);
1269
+ if (allowed.length === 0) {
1270
+ throw checkX402Accept(firstAccept, manifest, caip2, pricing.amount);
1271
+ }
1272
+ let paymentPayload;
1273
+ try {
1274
+ paymentPayload = await this.httpClient.createPaymentPayload({
1275
+ ...paymentRequired,
1276
+ accepts: allowed
1277
+ });
1278
+ } catch (err) {
1279
+ throw new RouteDockSignatureError(`x402 payment signing failed: ${String(err)}`, {
1280
+ cause: err
1281
+ });
1282
+ }
1283
+ const paymentHeaders = this.httpClient.encodePaymentSignatureHeader(paymentPayload);
1284
+ const signedAmount = stroopsToUsdc(BigInt(paymentPayload.accepted.amount));
1285
+ const settled = await withRetry(async () => {
1286
+ let response;
1035
1287
  try {
1036
- settled = await fetch(url, { headers: paymentHeaders });
1288
+ response = await fetch(url, { headers: paymentHeaders });
1037
1289
  } catch (err) {
1038
1290
  throw wrapFetchError(err, "x402 settlement request");
1039
1291
  }
1040
- if (!settled.ok) {
1041
- if (settled.status >= 500 || settled.status === 429 || settled.status === 503) {
1292
+ if (!response.ok) {
1293
+ if (response.status >= 500 || response.status === 429 || response.status === 503) {
1042
1294
  throw httpStatusToError(
1043
- `x402 payment failed: HTTP ${settled.status}`,
1044
- settled.status,
1045
- settled
1295
+ `x402 payment failed: HTTP ${response.status}`,
1296
+ response.status,
1297
+ response
1046
1298
  );
1047
1299
  }
1048
- throw new RouteDockManifestError(`x402 payment failed: HTTP ${settled.status}`);
1300
+ throw new RouteDockManifestError(`x402 payment failed: HTTP ${response.status}`);
1049
1301
  }
1050
- let txHash = null;
1051
- const responseHeader = settled.headers.get("X-Payment-Response");
1052
- if (responseHeader) {
1053
- try {
1054
- const settleResponse = decodePaymentResponseHeader(responseHeader);
1055
- txHash = settleResponse.transaction ?? null;
1056
- } catch {
1057
- }
1058
- }
1059
- let data;
1302
+ return response;
1303
+ }, this.retryPolicy);
1304
+ let txHash = null;
1305
+ const responseHeader = settled.headers.get("X-Payment-Response");
1306
+ if (responseHeader) {
1060
1307
  try {
1061
- data = await settled.json();
1308
+ const settleResponse = decodePaymentResponseHeader(responseHeader);
1309
+ txHash = settleResponse.transaction ?? null;
1062
1310
  } catch {
1063
- throw new RouteDockManifestError("Failed to parse JSON from settled response");
1064
1311
  }
1065
- return { data, txHash, mode: "x402", amount: pricing.amount, timestamp: Date.now() };
1066
- }, this.retryPolicy);
1312
+ }
1313
+ let data;
1314
+ try {
1315
+ data = await settled.json();
1316
+ } catch {
1317
+ throw new RouteDockManifestError("Failed to parse JSON from settled response");
1318
+ }
1319
+ return { data, txHash, mode: "x402", amount: signedAmount, timestamp: Date.now() };
1067
1320
  }
1068
1321
  };
1069
1322
  var MppChargeClient = class {
@@ -1080,27 +1333,42 @@ var MppChargeClient = class {
1080
1333
  if (!pricing) {
1081
1334
  throw new RouteDockManifestError("manifest.pricing.mpp-charge missing");
1082
1335
  }
1083
- return withRetry(async () => {
1084
- let txHash = null;
1085
- const mppx = Mppx.create({
1086
- polyfill: false,
1087
- methods: [
1088
- stellar.charge({
1089
- keypair: this.keypair,
1090
- mode: "pull",
1091
- onProgress(event) {
1092
- if (event.type === "paid") {
1093
- txHash = event.hash;
1094
- }
1336
+ const caip2 = this.network === "mainnet" ? "stellar:pubnet" : "stellar:testnet";
1337
+ let txHash = null;
1338
+ let signedAmount;
1339
+ let credential;
1340
+ const mppx = Mppx.create({
1341
+ polyfill: false,
1342
+ methods: [
1343
+ stellar.charge({
1344
+ keypair: this.keypair,
1345
+ mode: "pull",
1346
+ onProgress(event) {
1347
+ if (event.type === "paid") {
1348
+ txHash = event.hash;
1095
1349
  }
1096
- })
1097
- ]
1098
- });
1350
+ }
1351
+ })
1352
+ ],
1353
+ onChallenge: async (challenge, { createCredential }) => {
1354
+ const request = challenge.request;
1355
+ const rejection = checkChargeChallenge(request, manifest, caip2, pricing.amount);
1356
+ if (rejection) throw rejection;
1357
+ const amount = request["amount"];
1358
+ if (typeof amount !== "string") {
1359
+ throw new RouteDockPolicyRejectError("challenge_amount_invalid");
1360
+ }
1361
+ signedAmount = stroopsToUsdc(BigInt(amount));
1362
+ credential ??= await createCredential();
1363
+ return credential;
1364
+ }
1365
+ });
1366
+ return withRetry(async () => {
1099
1367
  let response;
1100
1368
  try {
1101
- response = await mppx.fetch(url);
1369
+ response = credential ? await mppx.rawFetch(url, { headers: { Authorization: credential } }) : await mppx.fetch(url);
1102
1370
  } catch (err) {
1103
- throw wrapFetchError(err, "MPP charge request");
1371
+ throw wrapMppError(err, "MPP charge request");
1104
1372
  }
1105
1373
  if (!response.ok) {
1106
1374
  if (response.status >= 500 || response.status === 429 || response.status === 503) {
@@ -1121,7 +1389,13 @@ var MppChargeClient = class {
1121
1389
  { cause }
1122
1390
  );
1123
1391
  }
1124
- return { data, txHash, mode: "mpp-charge", amount: pricing.amount, timestamp: Date.now() };
1392
+ return {
1393
+ data,
1394
+ txHash,
1395
+ mode: "mpp-charge",
1396
+ amount: signedAmount ?? pricing.amount,
1397
+ timestamp: Date.now()
1398
+ };
1125
1399
  }, this.retryPolicy);
1126
1400
  }
1127
1401
  };
@@ -1160,16 +1434,18 @@ function resolveStoreNetworkId(network) {
1160
1434
  return network === STELLAR_PUBNET ? STELLAR_PUBNET : STELLAR_TESTNET;
1161
1435
  }
1162
1436
  var MppSessionClient = class {
1163
- constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory) {
1437
+ constructor(keypair, network, retryPolicy, webSocketFactory = defaultWebSocketFactory, logger = consoleLogger) {
1164
1438
  this.keypair = keypair;
1165
1439
  this.network = network;
1166
1440
  this.retryPolicy = retryPolicy;
1167
1441
  this.webSocketFactory = webSocketFactory;
1442
+ this.logger = logger;
1168
1443
  }
1169
1444
  keypair;
1170
1445
  network;
1171
1446
  retryPolicy;
1172
1447
  webSocketFactory;
1448
+ logger;
1173
1449
  async openSession(url, manifest, commitmentSecret, options, onSpend) {
1174
1450
  const mode = options?.mode ?? "mpp-session";
1175
1451
  const pricing = manifest.pricing[mode];
@@ -1394,7 +1670,7 @@ var MppSessionClient = class {
1394
1670
  if (abortController.signal.aborted) {
1395
1671
  throw err;
1396
1672
  }
1397
- throw wrapFetchError(err, "Voucher request");
1673
+ throw wrapMppError(err, "Voucher request");
1398
1674
  }
1399
1675
  if (!resp.ok) {
1400
1676
  if (resp.status >= 500 || resp.status === 429 || resp.status === 503) {
@@ -1687,9 +1963,10 @@ var MppSessionClient = class {
1687
1963
  emit("session:timeout", { maxDurationMs });
1688
1964
  void handle.close().catch((error) => {
1689
1965
  emit("session:close-failed", { maxDurationMs, error });
1690
- console.warn(
1966
+ this.logger(
1967
+ "warn",
1691
1968
  `RouteDock: maxDuration auto-close failed after ${maxDurationMs}ms \u2014 the channel may still hold collateral; retry close() or call requestRefund().`,
1692
- error
1969
+ { error }
1693
1970
  );
1694
1971
  });
1695
1972
  }, maxDurationMs);
@@ -1739,7 +2016,7 @@ var MppSessionClient = class {
1739
2016
  );
1740
2017
  onSigned?.();
1741
2018
  } catch (err) {
1742
- throw wrapFetchError(err, "Voucher credential");
2019
+ throw wrapMppError(err, "Voucher credential");
1743
2020
  }
1744
2021
  const wsUrl = url.replace(/^http/, "ws");
1745
2022
  const queue = [];
@@ -1916,11 +2193,6 @@ function encodeAuthSignature(proof) {
1916
2193
  const payload = { nulth: "zk-v1", proof };
1917
2194
  return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
1918
2195
  }
1919
- function usdcToStroops2(amount) {
1920
- const [whole = "0", frac = ""] = amount.split(".");
1921
- const padded = (frac + "0000000").slice(0, 7);
1922
- return BigInt(whole) * 10000000n + BigInt(padded);
1923
- }
1924
2196
  var NulthClient = class {
1925
2197
  constructor(config) {
1926
2198
  this.config = config;
@@ -1936,7 +2208,10 @@ var NulthClient = class {
1936
2208
  `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
1937
2209
  );
1938
2210
  }
1939
- console.warn("NulthClient is using the insecure mock prover; proofs are not cryptographically sound");
2211
+ resolveLogger(config.logger)(
2212
+ "warn",
2213
+ "NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
2214
+ );
1940
2215
  }
1941
2216
  config;
1942
2217
  policy;
@@ -1988,6 +2263,12 @@ var NulthClient = class {
1988
2263
  return proof;
1989
2264
  }
1990
2265
  enforcePolicy(context) {
2266
+ if (context.amountStroops < 0n) {
2267
+ throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
2268
+ }
2269
+ if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
2270
+ throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
2271
+ }
1991
2272
  if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
1992
2273
  throw new NulthPolicyError("session_expired", "Nulth session expired");
1993
2274
  }
@@ -2022,14 +2303,14 @@ function paymentContextFromManifest(manifest, mode, ledgerSequence) {
2022
2303
  const pricing = manifest.pricing[mode];
2023
2304
  if (!pricing) throw new Error(`manifest.pricing.${mode} missing`);
2024
2305
  return {
2025
- payee: manifest.payee,
2026
- amountStroops: usdcToStroops2(pricing.amount),
2306
+ payee: pricing.payee ?? manifest.payee,
2307
+ amountStroops: usdcToStroops(pricing.amount),
2027
2308
  assetContract: manifest.asset_contract,
2028
2309
  ledgerSequence
2029
2310
  };
2030
2311
  }
2031
2312
  function createPolicyState(input) {
2032
- const dailyCapStroops = usdcToStroops2(input.dailyCapUsdc);
2313
+ const dailyCapStroops = usdcToStroops(input.dailyCapUsdc);
2033
2314
  return {
2034
2315
  dailyCapStroops,
2035
2316
  allowlistCommitment: commitAllowlist(input.allowedPayees, input.witnessSecret),
@@ -2131,7 +2412,8 @@ var InMemorySpendStore = class {
2131
2412
  state = null;
2132
2413
  constructor(options = {}) {
2133
2414
  if (options.warn !== false) {
2134
- console.warn(
2415
+ resolveLogger(options.logger)(
2416
+ "warn",
2135
2417
  "[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."
2136
2418
  );
2137
2419
  }
@@ -2221,7 +2503,10 @@ var RouteDockClient = class _RouteDockClient {
2221
2503
  this.network = config.network;
2222
2504
  this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
2223
2505
  this.retryPolicy = config.retryPolicy;
2224
- this.spendStore = config.spendStore ?? new InMemorySpendStore({ warn: !!config.spendCap });
2506
+ this.spendStore = config.spendStore ?? new InMemorySpendStore({
2507
+ warn: !!config.spendCap,
2508
+ ...config.logger && { logger: config.logger }
2509
+ });
2225
2510
  this.logger = config.logger;
2226
2511
  this.manifestTimeoutMs = config.manifestTimeoutMs;
2227
2512
  this.expectedPayee = config.expectedPayee;
@@ -2233,9 +2518,10 @@ var RouteDockClient = class _RouteDockClient {
2233
2518
  _secrets.set(this, config.commitmentSecret);
2234
2519
  }
2235
2520
  const secretKey = this.keypair.secret();
2236
- this.x402 = new X402Client(secretKey, this.network, this.retryPolicy);
2237
- this.charge = new MppChargeClient(this.keypair, this.network, this.retryPolicy);
2238
- this.session = new MppSessionClient(this.keypair, this.network, this.retryPolicy);
2521
+ const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
2522
+ this.x402 = new X402Client(secretKey, this.network, retryPolicy);
2523
+ this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
2524
+ this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
2239
2525
  }
2240
2526
  /** Fetch manifest and select mode — shared by pay() and estimateCost(). */
2241
2527
  async _resolveManifest(url, options) {
@@ -2264,12 +2550,13 @@ var RouteDockClient = class _RouteDockClient {
2264
2550
  * call before committing to a payment — for approval gates and manual
2265
2551
  * trustline remediation.
2266
2552
  */
2267
- async preflight(manifest) {
2553
+ async preflight(manifest, mode, endpoint) {
2268
2554
  assertManifestValid(manifest);
2269
- await this._checkTrustline(manifest);
2555
+ const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
2556
+ await this._checkTrustline(manifest, selectedAsset);
2270
2557
  return {
2271
2558
  hasTrustline: true,
2272
- asset: manifest.asset,
2559
+ asset: selectedAsset,
2273
2560
  modes: manifest.modes,
2274
2561
  ...manifest.regions && { regions: manifest.regions },
2275
2562
  ...manifest.latency_hints && { latency_hints: manifest.latency_hints },
@@ -2283,8 +2570,8 @@ var RouteDockClient = class _RouteDockClient {
2283
2570
  * trustline is missing. Results are cached per (account, asset) so the
2284
2571
  * per-payment overhead is one RPC call amortized to zero after the first.
2285
2572
  */
2286
- async _checkTrustline(manifest) {
2287
- const cacheKey = `${this.network}:${this.keypair.publicKey()}:${manifest.asset}`;
2573
+ async _checkTrustline(manifest, targetAsset = manifest.asset) {
2574
+ const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
2288
2575
  const cached = _RouteDockClient._trustlineCache.get(cacheKey);
2289
2576
  if (cached && Date.now() < cached.expiresAt) return;
2290
2577
  const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
@@ -2292,13 +2579,13 @@ var RouteDockClient = class _RouteDockClient {
2292
2579
  try {
2293
2580
  const account = await server.loadAccount(this.keypair.publicKey());
2294
2581
  const balances = account.balances;
2295
- const expectedIssuer = getAssetIssuer(manifest.asset, this.network);
2582
+ const expectedIssuer = getAssetIssuer(targetAsset, this.network);
2296
2583
  const hasTrustline = balances.some(
2297
- (b) => typeof b === "object" && b !== null && "asset_code" in b && b.asset_code === manifest.asset && (!expectedIssuer || b.asset_issuer === expectedIssuer)
2584
+ (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))
2298
2585
  );
2299
2586
  if (!hasTrustline) {
2300
- 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}`;
2301
- throw new RouteDockTrustlineError(manifest.asset, expectedIssuer || "unknown", remediation);
2587
+ 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}`;
2588
+ throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
2302
2589
  }
2303
2590
  _RouteDockClient._trustlineCache.set(cacheKey, {
2304
2591
  exists: true,
@@ -2307,7 +2594,8 @@ var RouteDockClient = class _RouteDockClient {
2307
2594
  } catch (err) {
2308
2595
  if (err instanceof RouteDockTrustlineError) throw err;
2309
2596
  this.logger?.(
2310
- `[RouteDock] Trustline preflight: could not verify trustline for ${manifest.asset} \u2014 continuing`
2597
+ "warn",
2598
+ `[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
2311
2599
  );
2312
2600
  }
2313
2601
  }
@@ -2315,18 +2603,18 @@ var RouteDockClient = class _RouteDockClient {
2315
2603
  * Pay for one request at `url`. Fetches manifest, selects payment mode,
2316
2604
  * runs trustline preflight, reserves local spend cap BEFORE executing the
2317
2605
  * payment, then commits the spend record on success. Rolls back the
2318
- * reservation if the on-chain payment fails. Concurrent pay() calls are
2319
- * serialized through a per-instance mutex to prevent spend-cap overrun.
2606
+ * reservation if the on-chain payment fails, and also when the call
2607
+ * completes without paying anything (an x402 endpoint answering a plain
2608
+ * 200 with no challenge). Concurrent pay() calls are serialized through a
2609
+ * per-instance mutex to prevent spend-cap overrun.
2320
2610
  */
2321
2611
  async pay(url, options) {
2322
2612
  const baseUrl = new URL(url).origin;
2323
2613
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2324
2614
  assertEndpointActive(manifest, url, this.logger);
2325
2615
  const mode = selectMode(manifest, { ...options, ...this.logger && { logger: this.logger } });
2326
- await this._checkTrustline(manifest);
2327
- if (this.vault?.mode === "nulth") {
2328
- return this._payWithNulthVault(url, manifest, mode);
2329
- }
2616
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2617
+ await this._checkTrustline(manifest, selectedAsset);
2330
2618
  let amount;
2331
2619
  switch (mode) {
2332
2620
  case "x402":
@@ -2346,22 +2634,30 @@ var RouteDockClient = class _RouteDockClient {
2346
2634
  const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
2347
2635
  let result;
2348
2636
  try {
2349
- switch (mode) {
2350
- case "x402":
2351
- result = await this.x402.pay(url, manifest);
2352
- break;
2353
- case "mpp-charge":
2354
- result = await this.charge.pay(url, manifest);
2355
- break;
2356
- default:
2357
- throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2637
+ if (this.vault?.mode === "nulth") {
2638
+ result = await this._payWithNulthVault(url, manifest, mode);
2639
+ } else {
2640
+ switch (mode) {
2641
+ case "x402":
2642
+ result = await this.x402.pay(url, manifest);
2643
+ break;
2644
+ case "mpp-charge":
2645
+ result = await this.charge.pay(url, manifest);
2646
+ break;
2647
+ default:
2648
+ throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2649
+ }
2358
2650
  }
2359
2651
  } catch (err) {
2360
2652
  await this._rollbackSpend(reserveId).catch(() => {
2361
2653
  });
2362
2654
  throw err;
2363
2655
  }
2364
- await this._commitSpend(reserveId);
2656
+ if (result.txHash === null && result.amount === "0") {
2657
+ await this._rollbackSpend(reserveId);
2658
+ } else {
2659
+ await this._commitSpend(reserveId);
2660
+ }
2365
2661
  return result;
2366
2662
  }
2367
2663
  /** Nulth ZK vault path — proof built off-chain, attached as auth signature */
@@ -2411,9 +2707,10 @@ var RouteDockClient = class _RouteDockClient {
2411
2707
  default:
2412
2708
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2413
2709
  }
2710
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2414
2711
  return {
2415
2712
  amount,
2416
- asset: manifest.asset,
2713
+ asset: selectedAsset,
2417
2714
  mode,
2418
2715
  manifest,
2419
2716
  ...manifest.regions && { regions: manifest.regions },
@@ -2650,7 +2947,7 @@ var SupabaseSeenTxStore = class {
2650
2947
  }
2651
2948
  supabase;
2652
2949
  async get(key) {
2653
- const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
2950
+ const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
2654
2951
  if (error) {
2655
2952
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
2656
2953
  }
@@ -2658,7 +2955,8 @@ var SupabaseSeenTxStore = class {
2658
2955
  const headers = data.headers;
2659
2956
  return {
2660
2957
  txHash: data.tx_hash ?? null,
2661
- ...headers ? { headers } : {}
2958
+ ...headers ? { headers } : {},
2959
+ createdAt: Date.parse(data.created_at)
2662
2960
  };
2663
2961
  }
2664
2962
  async set(key, record) {
@@ -2666,7 +2964,8 @@ var SupabaseSeenTxStore = class {
2666
2964
  {
2667
2965
  key,
2668
2966
  tx_hash: record.txHash,
2669
- headers: record.headers ?? null
2967
+ headers: record.headers ?? null,
2968
+ created_at: new Date(record.createdAt ?? Date.now()).toISOString()
2670
2969
  },
2671
2970
  { onConflict: "key" }
2672
2971
  );
@@ -2674,21 +2973,47 @@ var SupabaseSeenTxStore = class {
2674
2973
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
2675
2974
  }
2676
2975
  }
2976
+ /**
2977
+ * Atomically increment the settlement's replay counter through the
2978
+ * `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
2979
+ * row lock gives concurrent claims distinct counts. Returns `Infinity` when
2980
+ * the key no longer exists, so the caller treats it as spent.
2981
+ */
2982
+ async claimReplay(key) {
2983
+ const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
2984
+ p_key: key
2985
+ });
2986
+ if (error) {
2987
+ throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
2988
+ }
2989
+ if (data == null) return Number.POSITIVE_INFINITY;
2990
+ return data;
2991
+ }
2677
2992
  };
2678
-
2679
- // src/provider/payee.ts
2680
- function resolvePayee(manifest, mode) {
2681
- const override = manifest.pricing[mode]?.payee;
2682
- return override ?? manifest.payee;
2683
- }
2684
2993
  function extractPayerAddress(key) {
2685
2994
  if (typeof key !== "string") return null;
2686
2995
  return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
2687
2996
  }
2688
2997
 
2689
2998
  // src/provider/SeenTxStore.ts
2999
+ var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
3000
+ var MAX_SETTLEMENT_REPLAYS = 1;
3001
+ async function checkSettlementReplay(store, key, now = Date.now()) {
3002
+ const record = await store.get(key);
3003
+ if (!record) return { kind: "miss" };
3004
+ const { createdAt } = record;
3005
+ if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
3006
+ return { kind: "spent" };
3007
+ }
3008
+ if (store.claimReplay) {
3009
+ const replayed = await store.claimReplay(key);
3010
+ if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
3011
+ }
3012
+ return { kind: "replay", record };
3013
+ }
2690
3014
  var InMemorySeenTxStore = class {
2691
3015
  map = /* @__PURE__ */ new Map();
3016
+ replayCounts = /* @__PURE__ */ new Map();
2692
3017
  order = [];
2693
3018
  constructor(maxEntriesOrOptions = 1e4) {
2694
3019
  const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
@@ -2696,7 +3021,8 @@ var InMemorySeenTxStore = class {
2696
3021
  if (options.warn !== false) {
2697
3022
  const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
2698
3023
  if (isServerless) {
2699
- console.warn(
3024
+ resolveLogger(options.logger)(
3025
+ "warn",
2700
3026
  "[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."
2701
3027
  );
2702
3028
  }
@@ -2711,21 +3037,43 @@ var InMemorySeenTxStore = class {
2711
3037
  this.order.push(key);
2712
3038
  if (this.order.length > this.maxEntries) {
2713
3039
  const evicted = this.order.shift();
2714
- if (evicted !== void 0) this.map.delete(evicted);
3040
+ if (evicted !== void 0) {
3041
+ this.map.delete(evicted);
3042
+ this.replayCounts.delete(evicted);
3043
+ }
2715
3044
  }
2716
3045
  }
2717
3046
  this.map.set(key, record);
2718
3047
  }
3048
+ claimReplay(key) {
3049
+ if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
3050
+ const next = (this.replayCounts.get(key) ?? 0) + 1;
3051
+ this.replayCounts.set(key, next);
3052
+ return next;
3053
+ }
2719
3054
  };
2720
3055
  async function sha256Hex2(input) {
2721
3056
  const buf = new TextEncoder().encode(input);
2722
3057
  const digest = await crypto.subtle.digest("SHA-256", buf);
2723
3058
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
2724
3059
  }
2725
- async function paymentIdempotencyKey(getHeader) {
3060
+ async function paymentIdempotencyKey(getHeader, scope) {
2726
3061
  const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
2727
3062
  if (!material) return null;
2728
- return sha256Hex2(material);
3063
+ return sha256Hex2(
3064
+ [scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
3065
+ );
3066
+ }
3067
+
3068
+ // src/provider/settleResult.ts
3069
+ function readSettleResult(result) {
3070
+ const settle = result;
3071
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
3072
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
3073
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
3074
+ return failed(settle.errorReason);
3075
+ }
3076
+ return { ok: true, txHash: settle.transaction };
2729
3077
  }
2730
3078
 
2731
3079
  // src/provider/x402Handler.ts
@@ -2738,7 +3086,8 @@ function createX402Handler(opts) {
2738
3086
  const caip2 = CAIP22[opts.network];
2739
3087
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
2740
3088
  const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
2741
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
3089
+ const logger = resolveLogger(opts.logger);
3090
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
2742
3091
  const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
2743
3092
  const localFacilitator = new ExactStellarScheme$1([signer], {
2744
3093
  areFeesSponsored: true
@@ -2759,22 +3108,29 @@ function createX402Handler(opts) {
2759
3108
  }
2760
3109
  const amountInBaseUnits = String(usdcToUnits(opts.amount));
2761
3110
  const payTo = resolvePayee(opts.manifest, "x402");
2762
- const requirements = {
2763
- scheme: "exact",
2764
- network: caip2,
2765
- asset: opts.assetContract,
2766
- amount: amountInBaseUnits,
2767
- payTo,
2768
- maxTimeoutSeconds: 60,
2769
- extra: {
2770
- areFeesSponsored: true,
2771
- ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
2772
- }
2773
- };
2774
3111
  return async (req, res, next) => {
2775
3112
  try {
2776
- const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
2777
- if (!paymentHeader) {
3113
+ const endpoint = req.path || req.originalUrl;
3114
+ const assetContract = resolveAssetContract(
3115
+ opts.manifest,
3116
+ "x402",
3117
+ endpoint,
3118
+ opts.assetContract
3119
+ );
3120
+ const requirements = {
3121
+ scheme: "exact",
3122
+ network: caip2,
3123
+ asset: assetContract,
3124
+ amount: amountInBaseUnits,
3125
+ payTo,
3126
+ maxTimeoutSeconds: 60,
3127
+ extra: {
3128
+ areFeesSponsored: true,
3129
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
3130
+ }
3131
+ };
3132
+ const respondPaymentRequired = async (error, reason) => {
3133
+ const body = { error, ...reason ? { reason } : {} };
2778
3134
  if (ozServer) {
2779
3135
  const resourceInfo = {
2780
3136
  url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
@@ -2784,7 +3140,7 @@ function createX402Handler(opts) {
2784
3140
  [requirements],
2785
3141
  resourceInfo
2786
3142
  );
2787
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json({ error: "Payment Required" });
3143
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json(body);
2788
3144
  } else {
2789
3145
  const x402Response = {
2790
3146
  x402Version: 2,
@@ -2794,19 +3150,35 @@ function createX402Handler(opts) {
2794
3150
  },
2795
3151
  accepts: [requirements]
2796
3152
  };
2797
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
3153
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json(body);
2798
3154
  }
3155
+ };
3156
+ const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
3157
+ if (!paymentHeader) {
3158
+ await respondPaymentRequired("Payment Required");
2799
3159
  return;
2800
3160
  }
2801
- const idempotencyKey = await paymentIdempotencyKey((name) => {
2802
- const v = req.headers[name.toLowerCase()];
2803
- return Array.isArray(v) ? v[0] : v;
2804
- });
3161
+ const idempotencyKey = await paymentIdempotencyKey(
3162
+ (name) => {
3163
+ const v = req.headers[name.toLowerCase()];
3164
+ return Array.isArray(v) ? v[0] : v;
3165
+ },
3166
+ {
3167
+ method: req.method,
3168
+ path: req.originalUrl.split("?")[0],
3169
+ amount: requirements.amount,
3170
+ payTo: requirements.payTo
3171
+ }
3172
+ );
2805
3173
  if (idempotencyKey) {
2806
- const cached = await seenTxStore.get(idempotencyKey);
2807
- if (cached) {
2808
- if (cached.headers) {
2809
- for (const [k, val] of Object.entries(cached.headers)) {
3174
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
3175
+ if (replayCheck.kind === "spent") {
3176
+ res.status(402).json({ error: "Payment already used" });
3177
+ return;
3178
+ }
3179
+ if (replayCheck.kind === "replay") {
3180
+ if (replayCheck.record.headers) {
3181
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
2810
3182
  res.setHeader(k, val);
2811
3183
  }
2812
3184
  }
@@ -2825,15 +3197,18 @@ function createX402Handler(opts) {
2825
3197
  }
2826
3198
  if (ozServer) {
2827
3199
  const settleResult = await ozServer.settlePayment(payload, requirements);
2828
- txHash = settleResult.transaction ?? null;
2829
- if (settleResult) {
2830
- res.setHeader(
2831
- "X-Payment-Response",
2832
- encodePaymentResponseHeader(
2833
- settleResult
2834
- )
2835
- );
3200
+ const outcome = readSettleResult(settleResult);
3201
+ if (!outcome.ok) {
3202
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
3203
+ return;
2836
3204
  }
3205
+ txHash = outcome.txHash;
3206
+ res.setHeader(
3207
+ "X-Payment-Response",
3208
+ encodePaymentResponseHeader(
3209
+ settleResult
3210
+ )
3211
+ );
2837
3212
  } else {
2838
3213
  const verifyResult = await localFacilitator.verify(
2839
3214
  payload,
@@ -2850,15 +3225,18 @@ function createX402Handler(opts) {
2850
3225
  payload,
2851
3226
  requirements
2852
3227
  );
2853
- txHash = settleResult.transaction ?? null;
2854
- if (settleResult) {
2855
- res.setHeader(
2856
- "X-Payment-Response",
2857
- encodePaymentResponseHeader(
2858
- settleResult
2859
- )
2860
- );
3228
+ const outcome = readSettleResult(settleResult);
3229
+ if (!outcome.ok) {
3230
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
3231
+ return;
2861
3232
  }
3233
+ txHash = outcome.txHash;
3234
+ res.setHeader(
3235
+ "X-Payment-Response",
3236
+ encodePaymentResponseHeader(
3237
+ settleResult
3238
+ )
3239
+ );
2862
3240
  }
2863
3241
  if (idempotencyKey) {
2864
3242
  const headers = {};
@@ -2866,17 +3244,21 @@ function createX402Handler(opts) {
2866
3244
  if (typeof paymentResponse === "string") {
2867
3245
  headers["X-Payment-Response"] = paymentResponse;
2868
3246
  }
2869
- await seenTxStore.set(idempotencyKey, { txHash, headers });
3247
+ await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
2870
3248
  }
2871
3249
  if (txHash && opts.onSettled) {
2872
3250
  Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
2873
- console.error("[x402] onSettled callback error:", err);
3251
+ logger("error", "[x402] onSettled callback error", { error: err });
2874
3252
  opts.onCallbackError?.(err, "onSettled");
2875
3253
  });
2876
3254
  }
2877
3255
  next();
2878
3256
  } catch (err) {
2879
- console.error("[x402] Settlement error:", err);
3257
+ if (err instanceof RouteDockManifestError) {
3258
+ next(err);
3259
+ return;
3260
+ }
3261
+ logger("error", "[x402] Settlement error", { error: err });
2880
3262
  res.status(500).json({ error: "Payment settlement failed" });
2881
3263
  }
2882
3264
  };
@@ -2889,20 +3271,37 @@ function createMppChargeHandler(opts) {
2889
3271
  const networkId = MPP_NETWORK[opts.network];
2890
3272
  const amountHumanReadable = opts.amount;
2891
3273
  const recipient = resolvePayee(opts.manifest, "mpp-charge");
2892
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
2893
- const mppx = Mppx$1.create({
2894
- secretKey: opts.payeeSecretKey,
2895
- methods: [
2896
- stellar$2.charge({
2897
- recipient,
2898
- currency: opts.assetContract,
2899
- network: networkId,
2900
- feePayer: { envelopeSigner: opts.payeeSecretKey }
2901
- })
2902
- ]
2903
- });
3274
+ const logger = resolveLogger(opts.logger);
3275
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
3276
+ const mppxInstances = /* @__PURE__ */ new Map();
3277
+ function getMppx(contract) {
3278
+ let instance = mppxInstances.get(contract);
3279
+ if (!instance) {
3280
+ instance = Mppx$1.create({
3281
+ secretKey: opts.payeeSecretKey,
3282
+ methods: [
3283
+ stellar$3.charge({
3284
+ recipient,
3285
+ currency: contract,
3286
+ network: networkId,
3287
+ feePayer: { envelopeSigner: opts.payeeSecretKey }
3288
+ })
3289
+ ]
3290
+ });
3291
+ mppxInstances.set(contract, instance);
3292
+ }
3293
+ return instance;
3294
+ }
2904
3295
  return async (req, res, next) => {
2905
3296
  try {
3297
+ const endpoint = req.path || req.originalUrl;
3298
+ const assetContract = resolveAssetContract(
3299
+ opts.manifest,
3300
+ "mpp-charge",
3301
+ endpoint,
3302
+ opts.assetContract
3303
+ );
3304
+ const mppx = getMppx(assetContract);
2906
3305
  let payerAddress = null;
2907
3306
  try {
2908
3307
  const authHeader = req.headers["authorization"];
@@ -2918,15 +3317,27 @@ function createMppChargeHandler(opts) {
2918
3317
  }
2919
3318
  } catch {
2920
3319
  }
2921
- const idempotencyKey = await paymentIdempotencyKey((name) => {
2922
- const v = req.headers[name.toLowerCase()];
2923
- return Array.isArray(v) ? v[0] : v;
2924
- });
3320
+ const idempotencyKey = await paymentIdempotencyKey(
3321
+ (name) => {
3322
+ const v = req.headers[name.toLowerCase()];
3323
+ return Array.isArray(v) ? v[0] : v;
3324
+ },
3325
+ {
3326
+ method: req.method,
3327
+ path: req.originalUrl.split("?")[0],
3328
+ amount: amountHumanReadable,
3329
+ payTo: recipient
3330
+ }
3331
+ );
2925
3332
  if (idempotencyKey) {
2926
- const cached = await seenTxStore.get(idempotencyKey);
2927
- if (cached) {
2928
- if (cached.headers) {
2929
- for (const [k, val] of Object.entries(cached.headers)) {
3333
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
3334
+ if (replayCheck.kind === "spent") {
3335
+ res.status(402).json({ error: "Payment already used" });
3336
+ return;
3337
+ }
3338
+ if (replayCheck.kind === "replay") {
3339
+ if (replayCheck.record.headers) {
3340
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
2930
3341
  res.setHeader(k, val);
2931
3342
  }
2932
3343
  }
@@ -2938,7 +3349,7 @@ function createMppChargeHandler(opts) {
2938
3349
  const handler = mppx["stellar/charge"];
2939
3350
  const result = await handler({
2940
3351
  amount: amountHumanReadable,
2941
- currency: opts.assetContract,
3352
+ currency: assetContract,
2942
3353
  recipient,
2943
3354
  description: opts.manifest.name
2944
3355
  })(fetchReq);
@@ -2969,12 +3380,13 @@ function createMppChargeHandler(opts) {
2969
3380
  if (idempotencyKey) {
2970
3381
  await seenTxStore.set(idempotencyKey, {
2971
3382
  txHash: reference ?? null,
2972
- headers: receiptHeaders
3383
+ headers: receiptHeaders,
3384
+ createdAt: Date.now()
2973
3385
  });
2974
3386
  }
2975
3387
  if (reference && opts.onSettled) {
2976
3388
  Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
2977
- console.error("[mpp-charge] onSettled callback error:", err);
3389
+ logger("error", "[mpp-charge] onSettled callback error", { error: err });
2978
3390
  opts.onCallbackError?.(err, "onSettled");
2979
3391
  });
2980
3392
  }
@@ -3075,6 +3487,7 @@ function createMppSessionHandler(opts) {
3075
3487
  const networkId = MPP_NETWORK2[opts.network];
3076
3488
  const rateHuman = opts.rate;
3077
3489
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
3490
+ const logger = resolveLogger(opts.logger);
3078
3491
  const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
3079
3492
  const innerStore = Store.memory();
3080
3493
  let record = null;
@@ -3126,7 +3539,7 @@ function createMppSessionHandler(opts) {
3126
3539
  reason
3127
3540
  });
3128
3541
  } catch (err) {
3129
- console.error("[mpp-session] onOrphaned handler failed:", err);
3542
+ logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
3130
3543
  }
3131
3544
  }
3132
3545
  }
@@ -3173,7 +3586,7 @@ function createMppSessionHandler(opts) {
3173
3586
  sessionOpened = true;
3174
3587
  if (opts.onSessionOpen) {
3175
3588
  Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
3176
- console.error("[mpp-session] onSessionOpen callback error:", err);
3589
+ logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
3177
3590
  opts.onCallbackError?.(err, "onSessionOpen");
3178
3591
  });
3179
3592
  }
@@ -3181,7 +3594,7 @@ function createMppSessionHandler(opts) {
3181
3594
  if (opts.onVoucher) {
3182
3595
  const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
3183
3596
  Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
3184
- console.error("[mpp-session] onVoucher callback error:", err);
3597
+ logger("error", "[mpp-session] onVoucher callback error", { error: err });
3185
3598
  opts.onCallbackError?.(err, "onVoucher");
3186
3599
  });
3187
3600
  }
@@ -3191,7 +3604,7 @@ function createMppSessionHandler(opts) {
3191
3604
  methods: [
3192
3605
  withTypedChannelErrors(
3193
3606
  onVerifiedCredential(
3194
- stellar$3.channel({
3607
+ stellar$2.channel({
3195
3608
  channel: opts.channelFactory,
3196
3609
  commitmentKey: opts.commitmentPublicKey,
3197
3610
  network: networkId,
@@ -3234,7 +3647,7 @@ function createMppSessionHandler(opts) {
3234
3647
  if (opts.onSettled) {
3235
3648
  const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
3236
3649
  Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
3237
- console.error("[mpp-session] onSettled callback error:", err);
3650
+ logger("error", "[mpp-session] onSettled callback error", { error: err });
3238
3651
  opts.onCallbackError?.(err, "onSettled");
3239
3652
  });
3240
3653
  }
@@ -3245,7 +3658,7 @@ function createMppSessionHandler(opts) {
3245
3658
  payeeKeypair.publicKey()
3246
3659
  );
3247
3660
  if (!settlementAddresses) {
3248
- console.error("[mpp-session] skipped session_settled vault record: payer address unavailable");
3661
+ logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
3249
3662
  } else {
3250
3663
  try {
3251
3664
  const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
@@ -3268,7 +3681,7 @@ function createMppSessionHandler(opts) {
3268
3681
  preparedTx.sign(payeeKeypair);
3269
3682
  await server.sendTransaction(preparedTx);
3270
3683
  } catch (recordErr) {
3271
- console.error("[mpp-session] failed to record session_settled on vault:", recordErr);
3684
+ logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
3272
3685
  }
3273
3686
  }
3274
3687
  }
@@ -3325,12 +3738,13 @@ function routedock(opts) {
3325
3738
  payeeSecretKey: opts.payeeSecretKey,
3326
3739
  network: opts.network,
3327
3740
  amount: x402Price,
3328
- assetContract: opts.assetContract,
3741
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3329
3742
  ...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
3330
3743
  manifest: signedManifest,
3331
3744
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
3332
3745
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3333
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
3746
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
3747
+ ...opts.logger ? { logger: opts.logger } : {}
3334
3748
  })
3335
3749
  );
3336
3750
  }
@@ -3344,11 +3758,12 @@ function routedock(opts) {
3344
3758
  payeeSecretKey: opts.payeeSecretKey,
3345
3759
  network: opts.network,
3346
3760
  amount: chargePrice,
3347
- assetContract: opts.assetContract,
3761
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3348
3762
  manifest: signedManifest,
3349
3763
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
3350
3764
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3351
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
3765
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
3766
+ ...opts.logger ? { logger: opts.logger } : {}
3352
3767
  })
3353
3768
  );
3354
3769
  }
@@ -3366,7 +3781,7 @@ function routedock(opts) {
3366
3781
  network: opts.network,
3367
3782
  channelFactory: sessionPricing.channelFactory,
3368
3783
  rate: sessionPricing.rate,
3369
- assetContract: opts.assetContract,
3784
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3370
3785
  manifest: signedManifest,
3371
3786
  commitmentPublicKey: opts.commitmentPublicKey,
3372
3787
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
@@ -3374,7 +3789,8 @@ function routedock(opts) {
3374
3789
  ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
3375
3790
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3376
3791
  ...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
3377
- ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
3792
+ ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
3793
+ ...opts.logger ? { logger: opts.logger } : {}
3378
3794
  })
3379
3795
  );
3380
3796
  }
@@ -3538,25 +3954,26 @@ async function reconcileAbandonedSessions(opts) {
3538
3954
  }
3539
3955
  }
3540
3956
  async function runStartupReconciliation(opts, logger) {
3541
- const log = logger || console.log;
3542
- log("[SessionReconciler] Running startup reconciliation...");
3957
+ const log = resolveLogger(logger);
3958
+ log("info", "[SessionReconciler] Running startup reconciliation...");
3543
3959
  try {
3544
3960
  const stats = await reconcileAbandonedSessions(opts);
3545
3961
  if (stats.orphanedCount === 0) {
3546
- log("[SessionReconciler] No orphaned sessions found");
3962
+ log("info", "[SessionReconciler] No orphaned sessions found");
3547
3963
  return;
3548
3964
  }
3549
3965
  log(
3966
+ "info",
3550
3967
  `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
3551
3968
  );
3552
3969
  if (stats.errors.length > 0) {
3553
- log("[SessionReconciler] Reconciliation errors:");
3970
+ log("warn", "[SessionReconciler] Reconciliation errors:");
3554
3971
  for (const err of stats.errors) {
3555
- log(` - ${err.channelId}: ${err.reason}`);
3972
+ log("warn", ` - ${err.channelId}: ${err.reason}`);
3556
3973
  }
3557
3974
  }
3558
3975
  } catch (err) {
3559
- log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
3976
+ log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
3560
3977
  }
3561
3978
  }
3562
3979
  function tryDecodeBase64(value) {
@@ -3701,4 +4118,4 @@ var ProviderRegistry = class {
3701
4118
  }
3702
4119
  };
3703
4120
 
3704
- export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient, NulthPolicyError, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSeenTxStore, SupabaseSessionStore, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, createNulthSigner, createPolicyState, decimalToStroops, decodeAuthSignature, fetchLedgerSequence, fetchManifest, httpStatusToError, invalidateManifest, manifestDigest, parseRetryAfterMs, paymentContextFromManifest, prepareNulthSigner, rankProvidersByLatency, reconcileAbandonedSessions, registerProvider, routedock, runStartupReconciliation, selectMode, setNulthPaymentContext, signManifest, usdcToMicros, usdcToUnits, verifyManifestSignature, withRetry, wrapFetchError };
4121
+ export { DEFAULT_MAX_SESSION_DURATION_MS, DEFAULT_RETRY_POLICY, FileSpendStore, InMemorySpendStore, MppChargeClient, NulthPolicyError, OnChainRegistry, ProviderRegistry, RouteDockChannelStateError, RouteDockClient, RouteDockClientVersionError, RouteDockDisputeError, RouteDockError, RouteDockFacilitatorError, RouteDockManifestError, RouteDockManifestSunsetError, RouteDockManifestTimeoutError, RouteDockNetworkError, RouteDockNoSupportedModeError, RouteDockPolicyRejectError, RouteDockPolicyRejectedError, RouteDockRefundWindowError, RouteDockSessionError, RouteDockSignatureError, RouteDockTrustlineError, RouteDockVoucherMonotonicityError, SupabaseSeenTxStore, SupabaseSessionStore, X402Client, assertEndpointActive, assertManifestValid, assertNulthVaultManifest, backoffDelayMs, configureManifestCache, consoleLogger, createConsoleLogger, createNulthSigner, createPolicyState, decimalToStroops, decodeAuthSignature, fetchLedgerSequence, fetchManifest, getEligibleAssets, httpStatusToError, invalidateManifest, isAssetEligible, manifestDigest, noopLogger, normalizeManifestAssets, parseRetryAfterMs, paymentContextFromManifest, prepareNulthSigner, rankProvidersByLatency, reconcileAbandonedSessions, registerProvider, resolveAssetContract, resolveLogger, routedock, runStartupReconciliation, selectAsset, selectMode, setNulthPaymentContext, signManifest, usdcToMicros, usdcToUnits, verifyManifestSignature, withRetry, wrapFetchError, wrapMppError };