@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.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.0"};
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 = [];
@@ -1936,7 +2213,10 @@ var NulthClient = class {
1936
2213
  `NulthClient cannot use the insecure mock prover on mainnet; a production prover is required (network must be 'testnet', got ${String(config.network)})`
1937
2214
  );
1938
2215
  }
1939
- console.warn("NulthClient is using the insecure mock prover; proofs are not cryptographically sound");
2216
+ resolveLogger(config.logger)(
2217
+ "warn",
2218
+ "NulthClient is using the insecure mock prover; proofs are not cryptographically sound"
2219
+ );
1940
2220
  }
1941
2221
  config;
1942
2222
  policy;
@@ -1988,6 +2268,12 @@ var NulthClient = class {
1988
2268
  return proof;
1989
2269
  }
1990
2270
  enforcePolicy(context) {
2271
+ if (context.amountStroops < 0n) {
2272
+ throw new RangeError(`Invalid amountStroops: ${context.amountStroops}`);
2273
+ }
2274
+ if (!Number.isSafeInteger(context.ledgerSequence) || context.ledgerSequence < 0) {
2275
+ throw new RangeError(`Invalid ledgerSequence: ${context.ledgerSequence}`);
2276
+ }
1991
2277
  if (this.policy.expiryLedger !== void 0 && context.ledgerSequence > this.policy.expiryLedger) {
1992
2278
  throw new NulthPolicyError("session_expired", "Nulth session expired");
1993
2279
  }
@@ -2131,7 +2417,8 @@ var InMemorySpendStore = class {
2131
2417
  state = null;
2132
2418
  constructor(options = {}) {
2133
2419
  if (options.warn !== false) {
2134
- console.warn(
2420
+ resolveLogger(options.logger)(
2421
+ "warn",
2135
2422
  "[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
2423
  );
2137
2424
  }
@@ -2221,7 +2508,10 @@ var RouteDockClient = class _RouteDockClient {
2221
2508
  this.network = config.network;
2222
2509
  this.spendCap = config.spendCap ? normalizeSpendCap(config.spendCap) : void 0;
2223
2510
  this.retryPolicy = config.retryPolicy;
2224
- this.spendStore = config.spendStore ?? new InMemorySpendStore({ warn: !!config.spendCap });
2511
+ this.spendStore = config.spendStore ?? new InMemorySpendStore({
2512
+ warn: !!config.spendCap,
2513
+ ...config.logger && { logger: config.logger }
2514
+ });
2225
2515
  this.logger = config.logger;
2226
2516
  this.manifestTimeoutMs = config.manifestTimeoutMs;
2227
2517
  this.expectedPayee = config.expectedPayee;
@@ -2233,9 +2523,10 @@ var RouteDockClient = class _RouteDockClient {
2233
2523
  _secrets.set(this, config.commitmentSecret);
2234
2524
  }
2235
2525
  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);
2526
+ const retryPolicy = this.logger ? { ...this.retryPolicy, logger: this.logger } : this.retryPolicy;
2527
+ this.x402 = new X402Client(secretKey, this.network, retryPolicy);
2528
+ this.charge = new MppChargeClient(this.keypair, this.network, retryPolicy);
2529
+ this.session = new MppSessionClient(this.keypair, this.network, retryPolicy, void 0, this.logger);
2239
2530
  }
2240
2531
  /** Fetch manifest and select mode — shared by pay() and estimateCost(). */
2241
2532
  async _resolveManifest(url, options) {
@@ -2264,12 +2555,13 @@ var RouteDockClient = class _RouteDockClient {
2264
2555
  * call before committing to a payment — for approval gates and manual
2265
2556
  * trustline remediation.
2266
2557
  */
2267
- async preflight(manifest) {
2558
+ async preflight(manifest, mode, endpoint) {
2268
2559
  assertManifestValid(manifest);
2269
- await this._checkTrustline(manifest);
2560
+ const selectedAsset = mode ? selectAsset(manifest, mode, endpoint).asset : manifest.asset;
2561
+ await this._checkTrustline(manifest, selectedAsset);
2270
2562
  return {
2271
2563
  hasTrustline: true,
2272
- asset: manifest.asset,
2564
+ asset: selectedAsset,
2273
2565
  modes: manifest.modes,
2274
2566
  ...manifest.regions && { regions: manifest.regions },
2275
2567
  ...manifest.latency_hints && { latency_hints: manifest.latency_hints },
@@ -2283,8 +2575,8 @@ var RouteDockClient = class _RouteDockClient {
2283
2575
  * trustline is missing. Results are cached per (account, asset) so the
2284
2576
  * per-payment overhead is one RPC call amortized to zero after the first.
2285
2577
  */
2286
- async _checkTrustline(manifest) {
2287
- const cacheKey = `${this.network}:${this.keypair.publicKey()}:${manifest.asset}`;
2578
+ async _checkTrustline(manifest, targetAsset = manifest.asset) {
2579
+ const cacheKey = `${this.network}:${this.keypair.publicKey()}:${targetAsset}`;
2288
2580
  const cached = _RouteDockClient._trustlineCache.get(cacheKey);
2289
2581
  if (cached && Date.now() < cached.expiresAt) return;
2290
2582
  const horizonUrl = this.network === "testnet" ? "https://horizon-testnet.stellar.org" : "https://horizon.stellar.org";
@@ -2292,13 +2584,13 @@ var RouteDockClient = class _RouteDockClient {
2292
2584
  try {
2293
2585
  const account = await server.loadAccount(this.keypair.publicKey());
2294
2586
  const balances = account.balances;
2295
- const expectedIssuer = getAssetIssuer(manifest.asset, this.network);
2587
+ const expectedIssuer = getAssetIssuer(targetAsset, this.network);
2296
2588
  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)
2589
+ (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
2590
  );
2299
2591
  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);
2592
+ 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}`;
2593
+ throw new RouteDockTrustlineError(targetAsset, expectedIssuer || "unknown", remediation);
2302
2594
  }
2303
2595
  _RouteDockClient._trustlineCache.set(cacheKey, {
2304
2596
  exists: true,
@@ -2307,7 +2599,8 @@ var RouteDockClient = class _RouteDockClient {
2307
2599
  } catch (err) {
2308
2600
  if (err instanceof RouteDockTrustlineError) throw err;
2309
2601
  this.logger?.(
2310
- `[RouteDock] Trustline preflight: could not verify trustline for ${manifest.asset} \u2014 continuing`
2602
+ "warn",
2603
+ `[RouteDock] Trustline preflight: could not verify trustline for ${targetAsset} \u2014 continuing`
2311
2604
  );
2312
2605
  }
2313
2606
  }
@@ -2315,18 +2608,18 @@ var RouteDockClient = class _RouteDockClient {
2315
2608
  * Pay for one request at `url`. Fetches manifest, selects payment mode,
2316
2609
  * runs trustline preflight, reserves local spend cap BEFORE executing the
2317
2610
  * 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.
2611
+ * reservation if the on-chain payment fails, and also when the call
2612
+ * completes without paying anything (an x402 endpoint answering a plain
2613
+ * 200 with no challenge). Concurrent pay() calls are serialized through a
2614
+ * per-instance mutex to prevent spend-cap overrun.
2320
2615
  */
2321
2616
  async pay(url, options) {
2322
2617
  const baseUrl = new URL(url).origin;
2323
2618
  const manifest = await fetchManifest(baseUrl, this.retryPolicy, this.manifestTimeoutMs, this.expectedPayee);
2324
2619
  assertEndpointActive(manifest, url, this.logger);
2325
2620
  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
- }
2621
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2622
+ await this._checkTrustline(manifest, selectedAsset);
2330
2623
  let amount;
2331
2624
  switch (mode) {
2332
2625
  case "x402":
@@ -2346,22 +2639,30 @@ var RouteDockClient = class _RouteDockClient {
2346
2639
  const reserveId = await this._checkAndReserveSpend(amount, baseUrl);
2347
2640
  let result;
2348
2641
  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}`);
2642
+ if (this.vault?.mode === "nulth") {
2643
+ result = await this._payWithNulthVault(url, manifest, mode);
2644
+ } else {
2645
+ switch (mode) {
2646
+ case "x402":
2647
+ result = await this.x402.pay(url, manifest);
2648
+ break;
2649
+ case "mpp-charge":
2650
+ result = await this.charge.pay(url, manifest);
2651
+ break;
2652
+ default:
2653
+ throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2654
+ }
2358
2655
  }
2359
2656
  } catch (err) {
2360
2657
  await this._rollbackSpend(reserveId).catch(() => {
2361
2658
  });
2362
2659
  throw err;
2363
2660
  }
2364
- await this._commitSpend(reserveId);
2661
+ if (result.txHash === null && result.amount === "0") {
2662
+ await this._rollbackSpend(reserveId);
2663
+ } else {
2664
+ await this._commitSpend(reserveId);
2665
+ }
2365
2666
  return result;
2366
2667
  }
2367
2668
  /** Nulth ZK vault path — proof built off-chain, attached as auth signature */
@@ -2411,9 +2712,10 @@ var RouteDockClient = class _RouteDockClient {
2411
2712
  default:
2412
2713
  throw new RouteDockManifestError(`Unknown payment mode: ${mode}`);
2413
2714
  }
2715
+ const selectedAsset = selectAsset(manifest, mode, url).asset;
2414
2716
  return {
2415
2717
  amount,
2416
- asset: manifest.asset,
2718
+ asset: selectedAsset,
2417
2719
  mode,
2418
2720
  manifest,
2419
2721
  ...manifest.regions && { regions: manifest.regions },
@@ -2650,7 +2952,7 @@ var SupabaseSeenTxStore = class {
2650
2952
  }
2651
2953
  supabase;
2652
2954
  async get(key) {
2653
- const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
2955
+ const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
2654
2956
  if (error) {
2655
2957
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
2656
2958
  }
@@ -2658,7 +2960,8 @@ var SupabaseSeenTxStore = class {
2658
2960
  const headers = data.headers;
2659
2961
  return {
2660
2962
  txHash: data.tx_hash ?? null,
2661
- ...headers ? { headers } : {}
2963
+ ...headers ? { headers } : {},
2964
+ createdAt: Date.parse(data.created_at)
2662
2965
  };
2663
2966
  }
2664
2967
  async set(key, record) {
@@ -2666,7 +2969,8 @@ var SupabaseSeenTxStore = class {
2666
2969
  {
2667
2970
  key,
2668
2971
  tx_hash: record.txHash,
2669
- headers: record.headers ?? null
2972
+ headers: record.headers ?? null,
2973
+ created_at: new Date(record.createdAt ?? Date.now()).toISOString()
2670
2974
  },
2671
2975
  { onConflict: "key" }
2672
2976
  );
@@ -2674,21 +2978,47 @@ var SupabaseSeenTxStore = class {
2674
2978
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
2675
2979
  }
2676
2980
  }
2981
+ /**
2982
+ * Atomically increment the settlement's replay counter through the
2983
+ * `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
2984
+ * row lock gives concurrent claims distinct counts. Returns `Infinity` when
2985
+ * the key no longer exists, so the caller treats it as spent.
2986
+ */
2987
+ async claimReplay(key) {
2988
+ const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
2989
+ p_key: key
2990
+ });
2991
+ if (error) {
2992
+ throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
2993
+ }
2994
+ if (data == null) return Number.POSITIVE_INFINITY;
2995
+ return data;
2996
+ }
2677
2997
  };
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
2998
  function extractPayerAddress(key) {
2685
2999
  if (typeof key !== "string") return null;
2686
3000
  return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
2687
3001
  }
2688
3002
 
2689
3003
  // src/provider/SeenTxStore.ts
3004
+ var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
3005
+ var MAX_SETTLEMENT_REPLAYS = 1;
3006
+ async function checkSettlementReplay(store, key, now = Date.now()) {
3007
+ const record = await store.get(key);
3008
+ if (!record) return { kind: "miss" };
3009
+ const { createdAt } = record;
3010
+ if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
3011
+ return { kind: "spent" };
3012
+ }
3013
+ if (store.claimReplay) {
3014
+ const replayed = await store.claimReplay(key);
3015
+ if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
3016
+ }
3017
+ return { kind: "replay", record };
3018
+ }
2690
3019
  var InMemorySeenTxStore = class {
2691
3020
  map = /* @__PURE__ */ new Map();
3021
+ replayCounts = /* @__PURE__ */ new Map();
2692
3022
  order = [];
2693
3023
  constructor(maxEntriesOrOptions = 1e4) {
2694
3024
  const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
@@ -2696,7 +3026,8 @@ var InMemorySeenTxStore = class {
2696
3026
  if (options.warn !== false) {
2697
3027
  const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
2698
3028
  if (isServerless) {
2699
- console.warn(
3029
+ resolveLogger(options.logger)(
3030
+ "warn",
2700
3031
  "[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
3032
  );
2702
3033
  }
@@ -2711,21 +3042,43 @@ var InMemorySeenTxStore = class {
2711
3042
  this.order.push(key);
2712
3043
  if (this.order.length > this.maxEntries) {
2713
3044
  const evicted = this.order.shift();
2714
- if (evicted !== void 0) this.map.delete(evicted);
3045
+ if (evicted !== void 0) {
3046
+ this.map.delete(evicted);
3047
+ this.replayCounts.delete(evicted);
3048
+ }
2715
3049
  }
2716
3050
  }
2717
3051
  this.map.set(key, record);
2718
3052
  }
3053
+ claimReplay(key) {
3054
+ if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
3055
+ const next = (this.replayCounts.get(key) ?? 0) + 1;
3056
+ this.replayCounts.set(key, next);
3057
+ return next;
3058
+ }
2719
3059
  };
2720
3060
  async function sha256Hex2(input) {
2721
3061
  const buf = new TextEncoder().encode(input);
2722
3062
  const digest = await crypto.subtle.digest("SHA-256", buf);
2723
3063
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
2724
3064
  }
2725
- async function paymentIdempotencyKey(getHeader) {
3065
+ async function paymentIdempotencyKey(getHeader, scope) {
2726
3066
  const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
2727
3067
  if (!material) return null;
2728
- return sha256Hex2(material);
3068
+ return sha256Hex2(
3069
+ [scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
3070
+ );
3071
+ }
3072
+
3073
+ // src/provider/settleResult.ts
3074
+ function readSettleResult(result) {
3075
+ const settle = result;
3076
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
3077
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
3078
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
3079
+ return failed(settle.errorReason);
3080
+ }
3081
+ return { ok: true, txHash: settle.transaction };
2729
3082
  }
2730
3083
 
2731
3084
  // src/provider/x402Handler.ts
@@ -2738,7 +3091,8 @@ function createX402Handler(opts) {
2738
3091
  const caip2 = CAIP22[opts.network];
2739
3092
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
2740
3093
  const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
2741
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
3094
+ const logger = resolveLogger(opts.logger);
3095
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
2742
3096
  const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
2743
3097
  const localFacilitator = new ExactStellarScheme$1([signer], {
2744
3098
  areFeesSponsored: true
@@ -2759,22 +3113,29 @@ function createX402Handler(opts) {
2759
3113
  }
2760
3114
  const amountInBaseUnits = String(usdcToUnits(opts.amount));
2761
3115
  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
3116
  return async (req, res, next) => {
2775
3117
  try {
2776
- const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
2777
- if (!paymentHeader) {
3118
+ const endpoint = req.path || req.originalUrl;
3119
+ const assetContract = resolveAssetContract(
3120
+ opts.manifest,
3121
+ "x402",
3122
+ endpoint,
3123
+ opts.assetContract
3124
+ );
3125
+ const requirements = {
3126
+ scheme: "exact",
3127
+ network: caip2,
3128
+ asset: assetContract,
3129
+ amount: amountInBaseUnits,
3130
+ payTo,
3131
+ maxTimeoutSeconds: 60,
3132
+ extra: {
3133
+ areFeesSponsored: true,
3134
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
3135
+ }
3136
+ };
3137
+ const respondPaymentRequired = async (error, reason) => {
3138
+ const body = { error, ...reason ? { reason } : {} };
2778
3139
  if (ozServer) {
2779
3140
  const resourceInfo = {
2780
3141
  url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
@@ -2784,7 +3145,7 @@ function createX402Handler(opts) {
2784
3145
  [requirements],
2785
3146
  resourceInfo
2786
3147
  );
2787
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json({ error: "Payment Required" });
3148
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired)).json(body);
2788
3149
  } else {
2789
3150
  const x402Response = {
2790
3151
  x402Version: 2,
@@ -2794,19 +3155,35 @@ function createX402Handler(opts) {
2794
3155
  },
2795
3156
  accepts: [requirements]
2796
3157
  };
2797
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
3158
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response)).json(body);
2798
3159
  }
3160
+ };
3161
+ const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
3162
+ if (!paymentHeader) {
3163
+ await respondPaymentRequired("Payment Required");
2799
3164
  return;
2800
3165
  }
2801
- const idempotencyKey = await paymentIdempotencyKey((name) => {
2802
- const v = req.headers[name.toLowerCase()];
2803
- return Array.isArray(v) ? v[0] : v;
2804
- });
3166
+ const idempotencyKey = await paymentIdempotencyKey(
3167
+ (name) => {
3168
+ const v = req.headers[name.toLowerCase()];
3169
+ return Array.isArray(v) ? v[0] : v;
3170
+ },
3171
+ {
3172
+ method: req.method,
3173
+ path: req.originalUrl.split("?")[0],
3174
+ amount: requirements.amount,
3175
+ payTo: requirements.payTo
3176
+ }
3177
+ );
2805
3178
  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)) {
3179
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
3180
+ if (replayCheck.kind === "spent") {
3181
+ res.status(402).json({ error: "Payment already used" });
3182
+ return;
3183
+ }
3184
+ if (replayCheck.kind === "replay") {
3185
+ if (replayCheck.record.headers) {
3186
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
2810
3187
  res.setHeader(k, val);
2811
3188
  }
2812
3189
  }
@@ -2825,15 +3202,18 @@ function createX402Handler(opts) {
2825
3202
  }
2826
3203
  if (ozServer) {
2827
3204
  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
- );
3205
+ const outcome = readSettleResult(settleResult);
3206
+ if (!outcome.ok) {
3207
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
3208
+ return;
2836
3209
  }
3210
+ txHash = outcome.txHash;
3211
+ res.setHeader(
3212
+ "X-Payment-Response",
3213
+ encodePaymentResponseHeader(
3214
+ settleResult
3215
+ )
3216
+ );
2837
3217
  } else {
2838
3218
  const verifyResult = await localFacilitator.verify(
2839
3219
  payload,
@@ -2850,15 +3230,18 @@ function createX402Handler(opts) {
2850
3230
  payload,
2851
3231
  requirements
2852
3232
  );
2853
- txHash = settleResult.transaction ?? null;
2854
- if (settleResult) {
2855
- res.setHeader(
2856
- "X-Payment-Response",
2857
- encodePaymentResponseHeader(
2858
- settleResult
2859
- )
2860
- );
3233
+ const outcome = readSettleResult(settleResult);
3234
+ if (!outcome.ok) {
3235
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
3236
+ return;
2861
3237
  }
3238
+ txHash = outcome.txHash;
3239
+ res.setHeader(
3240
+ "X-Payment-Response",
3241
+ encodePaymentResponseHeader(
3242
+ settleResult
3243
+ )
3244
+ );
2862
3245
  }
2863
3246
  if (idempotencyKey) {
2864
3247
  const headers = {};
@@ -2866,17 +3249,21 @@ function createX402Handler(opts) {
2866
3249
  if (typeof paymentResponse === "string") {
2867
3250
  headers["X-Payment-Response"] = paymentResponse;
2868
3251
  }
2869
- await seenTxStore.set(idempotencyKey, { txHash, headers });
3252
+ await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
2870
3253
  }
2871
3254
  if (txHash && opts.onSettled) {
2872
3255
  Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
2873
- console.error("[x402] onSettled callback error:", err);
3256
+ logger("error", "[x402] onSettled callback error", { error: err });
2874
3257
  opts.onCallbackError?.(err, "onSettled");
2875
3258
  });
2876
3259
  }
2877
3260
  next();
2878
3261
  } catch (err) {
2879
- console.error("[x402] Settlement error:", err);
3262
+ if (err instanceof RouteDockManifestError) {
3263
+ next(err);
3264
+ return;
3265
+ }
3266
+ logger("error", "[x402] Settlement error", { error: err });
2880
3267
  res.status(500).json({ error: "Payment settlement failed" });
2881
3268
  }
2882
3269
  };
@@ -2889,20 +3276,37 @@ function createMppChargeHandler(opts) {
2889
3276
  const networkId = MPP_NETWORK[opts.network];
2890
3277
  const amountHumanReadable = opts.amount;
2891
3278
  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
- });
3279
+ const logger = resolveLogger(opts.logger);
3280
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
3281
+ const mppxInstances = /* @__PURE__ */ new Map();
3282
+ function getMppx(contract) {
3283
+ let instance = mppxInstances.get(contract);
3284
+ if (!instance) {
3285
+ instance = Mppx$1.create({
3286
+ secretKey: opts.payeeSecretKey,
3287
+ methods: [
3288
+ stellar$3.charge({
3289
+ recipient,
3290
+ currency: contract,
3291
+ network: networkId,
3292
+ feePayer: { envelopeSigner: opts.payeeSecretKey }
3293
+ })
3294
+ ]
3295
+ });
3296
+ mppxInstances.set(contract, instance);
3297
+ }
3298
+ return instance;
3299
+ }
2904
3300
  return async (req, res, next) => {
2905
3301
  try {
3302
+ const endpoint = req.path || req.originalUrl;
3303
+ const assetContract = resolveAssetContract(
3304
+ opts.manifest,
3305
+ "mpp-charge",
3306
+ endpoint,
3307
+ opts.assetContract
3308
+ );
3309
+ const mppx = getMppx(assetContract);
2906
3310
  let payerAddress = null;
2907
3311
  try {
2908
3312
  const authHeader = req.headers["authorization"];
@@ -2918,15 +3322,27 @@ function createMppChargeHandler(opts) {
2918
3322
  }
2919
3323
  } catch {
2920
3324
  }
2921
- const idempotencyKey = await paymentIdempotencyKey((name) => {
2922
- const v = req.headers[name.toLowerCase()];
2923
- return Array.isArray(v) ? v[0] : v;
2924
- });
3325
+ const idempotencyKey = await paymentIdempotencyKey(
3326
+ (name) => {
3327
+ const v = req.headers[name.toLowerCase()];
3328
+ return Array.isArray(v) ? v[0] : v;
3329
+ },
3330
+ {
3331
+ method: req.method,
3332
+ path: req.originalUrl.split("?")[0],
3333
+ amount: amountHumanReadable,
3334
+ payTo: recipient
3335
+ }
3336
+ );
2925
3337
  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)) {
3338
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
3339
+ if (replayCheck.kind === "spent") {
3340
+ res.status(402).json({ error: "Payment already used" });
3341
+ return;
3342
+ }
3343
+ if (replayCheck.kind === "replay") {
3344
+ if (replayCheck.record.headers) {
3345
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
2930
3346
  res.setHeader(k, val);
2931
3347
  }
2932
3348
  }
@@ -2938,7 +3354,7 @@ function createMppChargeHandler(opts) {
2938
3354
  const handler = mppx["stellar/charge"];
2939
3355
  const result = await handler({
2940
3356
  amount: amountHumanReadable,
2941
- currency: opts.assetContract,
3357
+ currency: assetContract,
2942
3358
  recipient,
2943
3359
  description: opts.manifest.name
2944
3360
  })(fetchReq);
@@ -2969,12 +3385,13 @@ function createMppChargeHandler(opts) {
2969
3385
  if (idempotencyKey) {
2970
3386
  await seenTxStore.set(idempotencyKey, {
2971
3387
  txHash: reference ?? null,
2972
- headers: receiptHeaders
3388
+ headers: receiptHeaders,
3389
+ createdAt: Date.now()
2973
3390
  });
2974
3391
  }
2975
3392
  if (reference && opts.onSettled) {
2976
3393
  Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
2977
- console.error("[mpp-charge] onSettled callback error:", err);
3394
+ logger("error", "[mpp-charge] onSettled callback error", { error: err });
2978
3395
  opts.onCallbackError?.(err, "onSettled");
2979
3396
  });
2980
3397
  }
@@ -3075,6 +3492,7 @@ function createMppSessionHandler(opts) {
3075
3492
  const networkId = MPP_NETWORK2[opts.network];
3076
3493
  const rateHuman = opts.rate;
3077
3494
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
3495
+ const logger = resolveLogger(opts.logger);
3078
3496
  const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
3079
3497
  const innerStore = Store.memory();
3080
3498
  let record = null;
@@ -3126,7 +3544,7 @@ function createMppSessionHandler(opts) {
3126
3544
  reason
3127
3545
  });
3128
3546
  } catch (err) {
3129
- console.error("[mpp-session] onOrphaned handler failed:", err);
3547
+ logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
3130
3548
  }
3131
3549
  }
3132
3550
  }
@@ -3173,7 +3591,7 @@ function createMppSessionHandler(opts) {
3173
3591
  sessionOpened = true;
3174
3592
  if (opts.onSessionOpen) {
3175
3593
  Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
3176
- console.error("[mpp-session] onSessionOpen callback error:", err);
3594
+ logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
3177
3595
  opts.onCallbackError?.(err, "onSessionOpen");
3178
3596
  });
3179
3597
  }
@@ -3181,7 +3599,7 @@ function createMppSessionHandler(opts) {
3181
3599
  if (opts.onVoucher) {
3182
3600
  const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
3183
3601
  Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
3184
- console.error("[mpp-session] onVoucher callback error:", err);
3602
+ logger("error", "[mpp-session] onVoucher callback error", { error: err });
3185
3603
  opts.onCallbackError?.(err, "onVoucher");
3186
3604
  });
3187
3605
  }
@@ -3191,7 +3609,7 @@ function createMppSessionHandler(opts) {
3191
3609
  methods: [
3192
3610
  withTypedChannelErrors(
3193
3611
  onVerifiedCredential(
3194
- stellar$3.channel({
3612
+ stellar$2.channel({
3195
3613
  channel: opts.channelFactory,
3196
3614
  commitmentKey: opts.commitmentPublicKey,
3197
3615
  network: networkId,
@@ -3234,7 +3652,7 @@ function createMppSessionHandler(opts) {
3234
3652
  if (opts.onSettled) {
3235
3653
  const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
3236
3654
  Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
3237
- console.error("[mpp-session] onSettled callback error:", err);
3655
+ logger("error", "[mpp-session] onSettled callback error", { error: err });
3238
3656
  opts.onCallbackError?.(err, "onSettled");
3239
3657
  });
3240
3658
  }
@@ -3245,7 +3663,7 @@ function createMppSessionHandler(opts) {
3245
3663
  payeeKeypair.publicKey()
3246
3664
  );
3247
3665
  if (!settlementAddresses) {
3248
- console.error("[mpp-session] skipped session_settled vault record: payer address unavailable");
3666
+ logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
3249
3667
  } else {
3250
3668
  try {
3251
3669
  const { Networks: Networks2 } = await import('@stellar/stellar-sdk');
@@ -3268,7 +3686,7 @@ function createMppSessionHandler(opts) {
3268
3686
  preparedTx.sign(payeeKeypair);
3269
3687
  await server.sendTransaction(preparedTx);
3270
3688
  } catch (recordErr) {
3271
- console.error("[mpp-session] failed to record session_settled on vault:", recordErr);
3689
+ logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
3272
3690
  }
3273
3691
  }
3274
3692
  }
@@ -3325,12 +3743,13 @@ function routedock(opts) {
3325
3743
  payeeSecretKey: opts.payeeSecretKey,
3326
3744
  network: opts.network,
3327
3745
  amount: x402Price,
3328
- assetContract: opts.assetContract,
3746
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3329
3747
  ...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
3330
3748
  manifest: signedManifest,
3331
3749
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
3332
3750
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3333
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
3751
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
3752
+ ...opts.logger ? { logger: opts.logger } : {}
3334
3753
  })
3335
3754
  );
3336
3755
  }
@@ -3344,11 +3763,12 @@ function routedock(opts) {
3344
3763
  payeeSecretKey: opts.payeeSecretKey,
3345
3764
  network: opts.network,
3346
3765
  amount: chargePrice,
3347
- assetContract: opts.assetContract,
3766
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3348
3767
  manifest: signedManifest,
3349
3768
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
3350
3769
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3351
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
3770
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
3771
+ ...opts.logger ? { logger: opts.logger } : {}
3352
3772
  })
3353
3773
  );
3354
3774
  }
@@ -3366,7 +3786,7 @@ function routedock(opts) {
3366
3786
  network: opts.network,
3367
3787
  channelFactory: sessionPricing.channelFactory,
3368
3788
  rate: sessionPricing.rate,
3369
- assetContract: opts.assetContract,
3789
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
3370
3790
  manifest: signedManifest,
3371
3791
  commitmentPublicKey: opts.commitmentPublicKey,
3372
3792
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
@@ -3374,7 +3794,8 @@ function routedock(opts) {
3374
3794
  ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
3375
3795
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
3376
3796
  ...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
3377
- ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
3797
+ ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
3798
+ ...opts.logger ? { logger: opts.logger } : {}
3378
3799
  })
3379
3800
  );
3380
3801
  }
@@ -3538,25 +3959,26 @@ async function reconcileAbandonedSessions(opts) {
3538
3959
  }
3539
3960
  }
3540
3961
  async function runStartupReconciliation(opts, logger) {
3541
- const log = logger || console.log;
3542
- log("[SessionReconciler] Running startup reconciliation...");
3962
+ const log = resolveLogger(logger);
3963
+ log("info", "[SessionReconciler] Running startup reconciliation...");
3543
3964
  try {
3544
3965
  const stats = await reconcileAbandonedSessions(opts);
3545
3966
  if (stats.orphanedCount === 0) {
3546
- log("[SessionReconciler] No orphaned sessions found");
3967
+ log("info", "[SessionReconciler] No orphaned sessions found");
3547
3968
  return;
3548
3969
  }
3549
3970
  log(
3971
+ "info",
3550
3972
  `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
3551
3973
  );
3552
3974
  if (stats.errors.length > 0) {
3553
- log("[SessionReconciler] Reconciliation errors:");
3975
+ log("warn", "[SessionReconciler] Reconciliation errors:");
3554
3976
  for (const err of stats.errors) {
3555
- log(` - ${err.channelId}: ${err.reason}`);
3977
+ log("warn", ` - ${err.channelId}: ${err.reason}`);
3556
3978
  }
3557
3979
  }
3558
3980
  } catch (err) {
3559
- log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
3981
+ log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
3560
3982
  }
3561
3983
  }
3562
3984
  function tryDecodeBase64(value) {
@@ -3701,4 +4123,4 @@ var ProviderRegistry = class {
3701
4123
  }
3702
4124
  };
3703
4125
 
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 };
4126
+ 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 };