@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/provider.cjs CHANGED
@@ -7,9 +7,9 @@ var server = require('@x402/core/server');
7
7
  var stellar = require('@x402/stellar');
8
8
  var http = require('@x402/core/http');
9
9
  var nulthSdk = require('@routedock/nulth-sdk');
10
- var server$3 = require('@stellar/mpp/charge/server');
10
+ var server$4 = require('@stellar/mpp/charge/server');
11
11
  var server$2 = require('mppx/server');
12
- var server$4 = require('@stellar/mpp/channel/server');
12
+ var server$3 = require('@stellar/mpp/channel/server');
13
13
  var mppx = require('mppx');
14
14
  var crypto$1 = require('crypto');
15
15
 
@@ -121,7 +121,85 @@ var RouteDockSessionError = RouteDockVoucherMonotonicityError;
121
121
  // src/types.ts
122
122
  var DEFAULT_MAX_SESSION_DURATION_MS = 36e5;
123
123
 
124
- // src/provider/payee.ts
124
+ // src/internal/assetUtils.ts
125
+ function normalizeManifestAssets(manifest) {
126
+ if (manifest.assets && manifest.assets.length > 0) {
127
+ return manifest.assets;
128
+ }
129
+ if (manifest.asset && manifest.asset_contract) {
130
+ return [
131
+ {
132
+ asset: manifest.asset,
133
+ asset_contract: manifest.asset_contract
134
+ }
135
+ ];
136
+ }
137
+ throw new RouteDockManifestError(
138
+ "Manifest must define either assets[] or root asset/asset_contract fields"
139
+ );
140
+ }
141
+ function getEligibleAssets(manifest, mode, endpoint) {
142
+ const assets = normalizeManifestAssets(manifest);
143
+ return assets.filter((asset) => {
144
+ if (asset.modes && !asset.modes.includes(mode)) {
145
+ return false;
146
+ }
147
+ if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
148
+ const trimmedEndpoint = endpoint.replace(/^\//, "");
149
+ if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
150
+ return true;
151
+ }
152
+ if (manifest.endpoints) {
153
+ let pathname = endpoint;
154
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
155
+ try {
156
+ pathname = new URL(endpoint).pathname;
157
+ } catch {
158
+ }
159
+ }
160
+ const trimmedPathname = pathname.replace(/^\//, "");
161
+ const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
162
+ const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
163
+ const keyMatches = key === endpoint || key === trimmedEndpoint;
164
+ if (pathMatches || keyMatches) {
165
+ return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
166
+ }
167
+ return false;
168
+ });
169
+ if (!matchesEndpoint) {
170
+ return false;
171
+ }
172
+ } else {
173
+ return false;
174
+ }
175
+ }
176
+ return true;
177
+ });
178
+ }
179
+ function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
180
+ if (manifest.assets && manifest.assets.length > 0) {
181
+ return selectAsset(manifest, mode, endpoint).asset_contract;
182
+ }
183
+ return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
184
+ }
185
+ function selectAsset(manifest, mode, endpoint) {
186
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
187
+ if (eligible.length === 0) {
188
+ const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
189
+ throw new RouteDockManifestError(
190
+ `No eligible assets found for mode '${mode}'${endpointMsg}`
191
+ );
192
+ }
193
+ return eligible[0];
194
+ }
195
+ function isAssetEligible(manifest, assetIdentifier, mode, endpoint) {
196
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
197
+ return eligible.some(
198
+ (asset) => asset.asset === assetIdentifier || asset.asset_contract === assetIdentifier
199
+ );
200
+ }
201
+
202
+ // src/internal/payee.ts
125
203
  function resolvePayee(manifest, mode) {
126
204
  const override = manifest.pricing[mode]?.payee;
127
205
  return override ?? manifest.payee;
@@ -132,9 +210,41 @@ function extractPayerAddress(key) {
132
210
  return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
133
211
  }
134
212
 
213
+ // src/internal/logger.ts
214
+ function createConsoleLogger() {
215
+ return (level, message, fields) => {
216
+ const values = fields ? Object.values(fields) : [];
217
+ if (values.length > 0) {
218
+ console[level](message, ...values);
219
+ } else {
220
+ console[level](message);
221
+ }
222
+ };
223
+ }
224
+ var consoleLogger = createConsoleLogger();
225
+ function resolveLogger(logger, fallback = consoleLogger) {
226
+ return logger ?? fallback;
227
+ }
228
+
135
229
  // src/provider/SeenTxStore.ts
230
+ var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
231
+ var MAX_SETTLEMENT_REPLAYS = 1;
232
+ async function checkSettlementReplay(store, key, now = Date.now()) {
233
+ const record = await store.get(key);
234
+ if (!record) return { kind: "miss" };
235
+ const { createdAt } = record;
236
+ if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
237
+ return { kind: "spent" };
238
+ }
239
+ if (store.claimReplay) {
240
+ const replayed = await store.claimReplay(key);
241
+ if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
242
+ }
243
+ return { kind: "replay", record };
244
+ }
136
245
  var InMemorySeenTxStore = class {
137
246
  map = /* @__PURE__ */ new Map();
247
+ replayCounts = /* @__PURE__ */ new Map();
138
248
  order = [];
139
249
  constructor(maxEntriesOrOptions = 1e4) {
140
250
  const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
@@ -142,7 +252,8 @@ var InMemorySeenTxStore = class {
142
252
  if (options.warn !== false) {
143
253
  const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
144
254
  if (isServerless) {
145
- console.warn(
255
+ resolveLogger(options.logger)(
256
+ "warn",
146
257
  "[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."
147
258
  );
148
259
  }
@@ -157,21 +268,43 @@ var InMemorySeenTxStore = class {
157
268
  this.order.push(key);
158
269
  if (this.order.length > this.maxEntries) {
159
270
  const evicted = this.order.shift();
160
- if (evicted !== void 0) this.map.delete(evicted);
271
+ if (evicted !== void 0) {
272
+ this.map.delete(evicted);
273
+ this.replayCounts.delete(evicted);
274
+ }
161
275
  }
162
276
  }
163
277
  this.map.set(key, record);
164
278
  }
279
+ claimReplay(key) {
280
+ if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
281
+ const next = (this.replayCounts.get(key) ?? 0) + 1;
282
+ this.replayCounts.set(key, next);
283
+ return next;
284
+ }
165
285
  };
166
286
  async function sha256Hex(input) {
167
287
  const buf = new TextEncoder().encode(input);
168
288
  const digest = await crypto.subtle.digest("SHA-256", buf);
169
289
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
170
290
  }
171
- async function paymentIdempotencyKey(getHeader) {
291
+ async function paymentIdempotencyKey(getHeader, scope) {
172
292
  const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
173
293
  if (!material) return null;
174
- return sha256Hex(material);
294
+ return sha256Hex(
295
+ [scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
296
+ );
297
+ }
298
+
299
+ // src/provider/settleResult.ts
300
+ function readSettleResult(result) {
301
+ const settle = result;
302
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
303
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
304
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
305
+ return failed(settle.errorReason);
306
+ }
307
+ return { ok: true, txHash: settle.transaction };
175
308
  }
176
309
 
177
310
  // src/provider/x402Handler.ts
@@ -184,7 +317,8 @@ function createX402Handler(opts) {
184
317
  const caip2 = CAIP2[opts.network];
185
318
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
186
319
  const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
187
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
320
+ const logger = resolveLogger(opts.logger);
321
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
188
322
  const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
189
323
  const localFacilitator = new facilitator.ExactStellarScheme([signer], {
190
324
  areFeesSponsored: true
@@ -205,22 +339,29 @@ function createX402Handler(opts) {
205
339
  }
206
340
  const amountInBaseUnits = String(usdcToUnits(opts.amount));
207
341
  const payTo = resolvePayee(opts.manifest, "x402");
208
- const requirements = {
209
- scheme: "exact",
210
- network: caip2,
211
- asset: opts.assetContract,
212
- amount: amountInBaseUnits,
213
- payTo,
214
- maxTimeoutSeconds: 60,
215
- extra: {
216
- areFeesSponsored: true,
217
- ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
218
- }
219
- };
220
342
  return async (req, res, next) => {
221
343
  try {
222
- const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
223
- if (!paymentHeader) {
344
+ const endpoint = req.path || req.originalUrl;
345
+ const assetContract = resolveAssetContract(
346
+ opts.manifest,
347
+ "x402",
348
+ endpoint,
349
+ opts.assetContract
350
+ );
351
+ const requirements = {
352
+ scheme: "exact",
353
+ network: caip2,
354
+ asset: assetContract,
355
+ amount: amountInBaseUnits,
356
+ payTo,
357
+ maxTimeoutSeconds: 60,
358
+ extra: {
359
+ areFeesSponsored: true,
360
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
361
+ }
362
+ };
363
+ const respondPaymentRequired = async (error, reason) => {
364
+ const body = { error, ...reason ? { reason } : {} };
224
365
  if (ozServer) {
225
366
  const resourceInfo = {
226
367
  url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
@@ -230,7 +371,7 @@ function createX402Handler(opts) {
230
371
  [requirements],
231
372
  resourceInfo
232
373
  );
233
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json({ error: "Payment Required" });
374
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(body);
234
375
  } else {
235
376
  const x402Response = {
236
377
  x402Version: 2,
@@ -240,19 +381,35 @@ function createX402Handler(opts) {
240
381
  },
241
382
  accepts: [requirements]
242
383
  };
243
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
384
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(body);
244
385
  }
386
+ };
387
+ const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
388
+ if (!paymentHeader) {
389
+ await respondPaymentRequired("Payment Required");
245
390
  return;
246
391
  }
247
- const idempotencyKey = await paymentIdempotencyKey((name) => {
248
- const v = req.headers[name.toLowerCase()];
249
- return Array.isArray(v) ? v[0] : v;
250
- });
392
+ const idempotencyKey = await paymentIdempotencyKey(
393
+ (name) => {
394
+ const v = req.headers[name.toLowerCase()];
395
+ return Array.isArray(v) ? v[0] : v;
396
+ },
397
+ {
398
+ method: req.method,
399
+ path: req.originalUrl.split("?")[0],
400
+ amount: requirements.amount,
401
+ payTo: requirements.payTo
402
+ }
403
+ );
251
404
  if (idempotencyKey) {
252
- const cached = await seenTxStore.get(idempotencyKey);
253
- if (cached) {
254
- if (cached.headers) {
255
- for (const [k, val] of Object.entries(cached.headers)) {
405
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
406
+ if (replayCheck.kind === "spent") {
407
+ res.status(402).json({ error: "Payment already used" });
408
+ return;
409
+ }
410
+ if (replayCheck.kind === "replay") {
411
+ if (replayCheck.record.headers) {
412
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
256
413
  res.setHeader(k, val);
257
414
  }
258
415
  }
@@ -271,15 +428,18 @@ function createX402Handler(opts) {
271
428
  }
272
429
  if (ozServer) {
273
430
  const settleResult = await ozServer.settlePayment(payload, requirements);
274
- txHash = settleResult.transaction ?? null;
275
- if (settleResult) {
276
- res.setHeader(
277
- "X-Payment-Response",
278
- http.encodePaymentResponseHeader(
279
- settleResult
280
- )
281
- );
431
+ const outcome = readSettleResult(settleResult);
432
+ if (!outcome.ok) {
433
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
434
+ return;
282
435
  }
436
+ txHash = outcome.txHash;
437
+ res.setHeader(
438
+ "X-Payment-Response",
439
+ http.encodePaymentResponseHeader(
440
+ settleResult
441
+ )
442
+ );
283
443
  } else {
284
444
  const verifyResult = await localFacilitator.verify(
285
445
  payload,
@@ -296,15 +456,18 @@ function createX402Handler(opts) {
296
456
  payload,
297
457
  requirements
298
458
  );
299
- txHash = settleResult.transaction ?? null;
300
- if (settleResult) {
301
- res.setHeader(
302
- "X-Payment-Response",
303
- http.encodePaymentResponseHeader(
304
- settleResult
305
- )
306
- );
459
+ const outcome = readSettleResult(settleResult);
460
+ if (!outcome.ok) {
461
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
462
+ return;
307
463
  }
464
+ txHash = outcome.txHash;
465
+ res.setHeader(
466
+ "X-Payment-Response",
467
+ http.encodePaymentResponseHeader(
468
+ settleResult
469
+ )
470
+ );
308
471
  }
309
472
  if (idempotencyKey) {
310
473
  const headers = {};
@@ -312,17 +475,21 @@ function createX402Handler(opts) {
312
475
  if (typeof paymentResponse === "string") {
313
476
  headers["X-Payment-Response"] = paymentResponse;
314
477
  }
315
- await seenTxStore.set(idempotencyKey, { txHash, headers });
478
+ await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
316
479
  }
317
480
  if (txHash && opts.onSettled) {
318
481
  Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
319
- console.error("[x402] onSettled callback error:", err);
482
+ logger("error", "[x402] onSettled callback error", { error: err });
320
483
  opts.onCallbackError?.(err, "onSettled");
321
484
  });
322
485
  }
323
486
  next();
324
487
  } catch (err) {
325
- console.error("[x402] Settlement error:", err);
488
+ if (err instanceof RouteDockManifestError) {
489
+ next(err);
490
+ return;
491
+ }
492
+ logger("error", "[x402] Settlement error", { error: err });
326
493
  res.status(500).json({ error: "Payment settlement failed" });
327
494
  }
328
495
  };
@@ -335,20 +502,37 @@ function createMppChargeHandler(opts) {
335
502
  const networkId = MPP_NETWORK[opts.network];
336
503
  const amountHumanReadable = opts.amount;
337
504
  const recipient = resolvePayee(opts.manifest, "mpp-charge");
338
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
339
- const mppx = server$2.Mppx.create({
340
- secretKey: opts.payeeSecretKey,
341
- methods: [
342
- server$3.stellar.charge({
343
- recipient,
344
- currency: opts.assetContract,
345
- network: networkId,
346
- feePayer: { envelopeSigner: opts.payeeSecretKey }
347
- })
348
- ]
349
- });
505
+ const logger = resolveLogger(opts.logger);
506
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
507
+ const mppxInstances = /* @__PURE__ */ new Map();
508
+ function getMppx(contract) {
509
+ let instance = mppxInstances.get(contract);
510
+ if (!instance) {
511
+ instance = server$2.Mppx.create({
512
+ secretKey: opts.payeeSecretKey,
513
+ methods: [
514
+ server$4.stellar.charge({
515
+ recipient,
516
+ currency: contract,
517
+ network: networkId,
518
+ feePayer: { envelopeSigner: opts.payeeSecretKey }
519
+ })
520
+ ]
521
+ });
522
+ mppxInstances.set(contract, instance);
523
+ }
524
+ return instance;
525
+ }
350
526
  return async (req, res, next) => {
351
527
  try {
528
+ const endpoint = req.path || req.originalUrl;
529
+ const assetContract = resolveAssetContract(
530
+ opts.manifest,
531
+ "mpp-charge",
532
+ endpoint,
533
+ opts.assetContract
534
+ );
535
+ const mppx = getMppx(assetContract);
352
536
  let payerAddress = null;
353
537
  try {
354
538
  const authHeader = req.headers["authorization"];
@@ -364,15 +548,27 @@ function createMppChargeHandler(opts) {
364
548
  }
365
549
  } catch {
366
550
  }
367
- const idempotencyKey = await paymentIdempotencyKey((name) => {
368
- const v = req.headers[name.toLowerCase()];
369
- return Array.isArray(v) ? v[0] : v;
370
- });
551
+ const idempotencyKey = await paymentIdempotencyKey(
552
+ (name) => {
553
+ const v = req.headers[name.toLowerCase()];
554
+ return Array.isArray(v) ? v[0] : v;
555
+ },
556
+ {
557
+ method: req.method,
558
+ path: req.originalUrl.split("?")[0],
559
+ amount: amountHumanReadable,
560
+ payTo: recipient
561
+ }
562
+ );
371
563
  if (idempotencyKey) {
372
- const cached = await seenTxStore.get(idempotencyKey);
373
- if (cached) {
374
- if (cached.headers) {
375
- for (const [k, val] of Object.entries(cached.headers)) {
564
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
565
+ if (replayCheck.kind === "spent") {
566
+ res.status(402).json({ error: "Payment already used" });
567
+ return;
568
+ }
569
+ if (replayCheck.kind === "replay") {
570
+ if (replayCheck.record.headers) {
571
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
376
572
  res.setHeader(k, val);
377
573
  }
378
574
  }
@@ -384,7 +580,7 @@ function createMppChargeHandler(opts) {
384
580
  const handler = mppx["stellar/charge"];
385
581
  const result = await handler({
386
582
  amount: amountHumanReadable,
387
- currency: opts.assetContract,
583
+ currency: assetContract,
388
584
  recipient,
389
585
  description: opts.manifest.name
390
586
  })(fetchReq);
@@ -415,12 +611,13 @@ function createMppChargeHandler(opts) {
415
611
  if (idempotencyKey) {
416
612
  await seenTxStore.set(idempotencyKey, {
417
613
  txHash: reference ?? null,
418
- headers: receiptHeaders
614
+ headers: receiptHeaders,
615
+ createdAt: Date.now()
419
616
  });
420
617
  }
421
618
  if (reference && opts.onSettled) {
422
619
  Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
423
- console.error("[mpp-charge] onSettled callback error:", err);
620
+ logger("error", "[mpp-charge] onSettled callback error", { error: err });
424
621
  opts.onCallbackError?.(err, "onSettled");
425
622
  });
426
623
  }
@@ -528,8 +725,9 @@ function createMppSessionHandler(opts) {
528
725
  const networkId = MPP_NETWORK2[opts.network];
529
726
  const rateHuman = opts.rate;
530
727
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
728
+ const logger = resolveLogger(opts.logger);
531
729
  const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
532
- const innerStore = server$4.Store.memory();
730
+ const innerStore = server$3.Store.memory();
533
731
  let record = null;
534
732
  let voucherCount = 0;
535
733
  let sessionOpened = false;
@@ -579,7 +777,7 @@ function createMppSessionHandler(opts) {
579
777
  reason
580
778
  });
581
779
  } catch (err) {
582
- console.error("[mpp-session] onOrphaned handler failed:", err);
780
+ logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
583
781
  }
584
782
  }
585
783
  }
@@ -626,7 +824,7 @@ function createMppSessionHandler(opts) {
626
824
  sessionOpened = true;
627
825
  if (opts.onSessionOpen) {
628
826
  Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
629
- console.error("[mpp-session] onSessionOpen callback error:", err);
827
+ logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
630
828
  opts.onCallbackError?.(err, "onSessionOpen");
631
829
  });
632
830
  }
@@ -634,7 +832,7 @@ function createMppSessionHandler(opts) {
634
832
  if (opts.onVoucher) {
635
833
  const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
636
834
  Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
637
- console.error("[mpp-session] onVoucher callback error:", err);
835
+ logger("error", "[mpp-session] onVoucher callback error", { error: err });
638
836
  opts.onCallbackError?.(err, "onVoucher");
639
837
  });
640
838
  }
@@ -644,7 +842,7 @@ function createMppSessionHandler(opts) {
644
842
  methods: [
645
843
  withTypedChannelErrors(
646
844
  onVerifiedCredential(
647
- server$4.stellar.channel({
845
+ server$3.stellar.channel({
648
846
  channel: opts.channelFactory,
649
847
  commitmentKey: opts.commitmentPublicKey,
650
848
  network: networkId,
@@ -675,7 +873,7 @@ function createMppSessionHandler(opts) {
675
873
  }
676
874
  if (closeAmount > 0n && closeSig) {
677
875
  const closePayer = record?.payer ?? null;
678
- const closeTxHash = await server$4.close({
876
+ const closeTxHash = await server$3.close({
679
877
  channel: opts.channelFactory,
680
878
  amount: closeAmount,
681
879
  signature: Buffer.from(closeSig, "hex"),
@@ -687,7 +885,7 @@ function createMppSessionHandler(opts) {
687
885
  if (opts.onSettled) {
688
886
  const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
689
887
  Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
690
- console.error("[mpp-session] onSettled callback error:", err);
888
+ logger("error", "[mpp-session] onSettled callback error", { error: err });
691
889
  opts.onCallbackError?.(err, "onSettled");
692
890
  });
693
891
  }
@@ -698,7 +896,7 @@ function createMppSessionHandler(opts) {
698
896
  payeeKeypair.publicKey()
699
897
  );
700
898
  if (!settlementAddresses) {
701
- console.error("[mpp-session] skipped session_settled vault record: payer address unavailable");
899
+ logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
702
900
  } else {
703
901
  try {
704
902
  const { Networks } = await import('@stellar/stellar-sdk');
@@ -721,7 +919,7 @@ function createMppSessionHandler(opts) {
721
919
  preparedTx.sign(payeeKeypair);
722
920
  await server.sendTransaction(preparedTx);
723
921
  } catch (recordErr) {
724
- console.error("[mpp-session] failed to record session_settled on vault:", recordErr);
922
+ logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
725
923
  }
726
924
  }
727
925
  }
@@ -838,12 +1036,13 @@ function routedock(opts) {
838
1036
  payeeSecretKey: opts.payeeSecretKey,
839
1037
  network: opts.network,
840
1038
  amount: x402Price,
841
- assetContract: opts.assetContract,
1039
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
842
1040
  ...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
843
1041
  manifest: signedManifest,
844
1042
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
845
1043
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
846
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
1044
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
1045
+ ...opts.logger ? { logger: opts.logger } : {}
847
1046
  })
848
1047
  );
849
1048
  }
@@ -857,11 +1056,12 @@ function routedock(opts) {
857
1056
  payeeSecretKey: opts.payeeSecretKey,
858
1057
  network: opts.network,
859
1058
  amount: chargePrice,
860
- assetContract: opts.assetContract,
1059
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
861
1060
  manifest: signedManifest,
862
1061
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
863
1062
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
864
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
1063
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
1064
+ ...opts.logger ? { logger: opts.logger } : {}
865
1065
  })
866
1066
  );
867
1067
  }
@@ -879,7 +1079,7 @@ function routedock(opts) {
879
1079
  network: opts.network,
880
1080
  channelFactory: sessionPricing.channelFactory,
881
1081
  rate: sessionPricing.rate,
882
- assetContract: opts.assetContract,
1082
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
883
1083
  manifest: signedManifest,
884
1084
  commitmentPublicKey: opts.commitmentPublicKey,
885
1085
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
@@ -887,7 +1087,8 @@ function routedock(opts) {
887
1087
  ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
888
1088
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
889
1089
  ...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
890
- ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
1090
+ ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
1091
+ ...opts.logger ? { logger: opts.logger } : {}
891
1092
  })
892
1093
  );
893
1094
  }
@@ -1008,25 +1209,26 @@ async function reconcileAbandonedSessions(opts) {
1008
1209
  }
1009
1210
  }
1010
1211
  async function runStartupReconciliation(opts, logger) {
1011
- const log = logger || console.log;
1012
- log("[SessionReconciler] Running startup reconciliation...");
1212
+ const log = resolveLogger(logger);
1213
+ log("info", "[SessionReconciler] Running startup reconciliation...");
1013
1214
  try {
1014
1215
  const stats = await reconcileAbandonedSessions(opts);
1015
1216
  if (stats.orphanedCount === 0) {
1016
- log("[SessionReconciler] No orphaned sessions found");
1217
+ log("info", "[SessionReconciler] No orphaned sessions found");
1017
1218
  return;
1018
1219
  }
1019
1220
  log(
1221
+ "info",
1020
1222
  `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
1021
1223
  );
1022
1224
  if (stats.errors.length > 0) {
1023
- log("[SessionReconciler] Reconciliation errors:");
1225
+ log("warn", "[SessionReconciler] Reconciliation errors:");
1024
1226
  for (const err of stats.errors) {
1025
- log(` - ${err.channelId}: ${err.reason}`);
1227
+ log("warn", ` - ${err.channelId}: ${err.reason}`);
1026
1228
  }
1027
1229
  }
1028
1230
  } catch (err) {
1029
- log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
1231
+ log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
1030
1232
  }
1031
1233
  }
1032
1234
 
@@ -1160,7 +1362,7 @@ var SupabaseSeenTxStore = class {
1160
1362
  }
1161
1363
  supabase;
1162
1364
  async get(key) {
1163
- const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
1365
+ const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
1164
1366
  if (error) {
1165
1367
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
1166
1368
  }
@@ -1168,7 +1370,8 @@ var SupabaseSeenTxStore = class {
1168
1370
  const headers = data.headers;
1169
1371
  return {
1170
1372
  txHash: data.tx_hash ?? null,
1171
- ...headers ? { headers } : {}
1373
+ ...headers ? { headers } : {},
1374
+ createdAt: Date.parse(data.created_at)
1172
1375
  };
1173
1376
  }
1174
1377
  async set(key, record) {
@@ -1176,7 +1379,8 @@ var SupabaseSeenTxStore = class {
1176
1379
  {
1177
1380
  key,
1178
1381
  tx_hash: record.txHash,
1179
- headers: record.headers ?? null
1382
+ headers: record.headers ?? null,
1383
+ created_at: new Date(record.createdAt ?? Date.now()).toISOString()
1180
1384
  },
1181
1385
  { onConflict: "key" }
1182
1386
  );
@@ -1184,10 +1388,27 @@ var SupabaseSeenTxStore = class {
1184
1388
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
1185
1389
  }
1186
1390
  }
1391
+ /**
1392
+ * Atomically increment the settlement's replay counter through the
1393
+ * `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
1394
+ * row lock gives concurrent claims distinct counts. Returns `Infinity` when
1395
+ * the key no longer exists, so the caller treats it as spent.
1396
+ */
1397
+ async claimReplay(key) {
1398
+ const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
1399
+ p_key: key
1400
+ });
1401
+ if (error) {
1402
+ throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
1403
+ }
1404
+ if (data == null) return Number.POSITIVE_INFINITY;
1405
+ return data;
1406
+ }
1187
1407
  };
1188
1408
 
1189
1409
  exports.DEFAULT_MAX_SESSION_DURATION_MS = DEFAULT_MAX_SESSION_DURATION_MS;
1190
1410
  exports.InMemorySeenTxStore = InMemorySeenTxStore;
1411
+ exports.MAX_SETTLEMENT_REPLAYS = MAX_SETTLEMENT_REPLAYS;
1191
1412
  exports.RouteDockChannelStateError = RouteDockChannelStateError;
1192
1413
  exports.RouteDockClientVersionError = RouteDockClientVersionError;
1193
1414
  exports.RouteDockDisputeError = RouteDockDisputeError;
@@ -1203,15 +1424,22 @@ exports.RouteDockSessionError = RouteDockSessionError;
1203
1424
  exports.RouteDockSignatureError = RouteDockSignatureError;
1204
1425
  exports.RouteDockTrustlineError = RouteDockTrustlineError;
1205
1426
  exports.RouteDockVoucherMonotonicityError = RouteDockVoucherMonotonicityError;
1427
+ exports.SETTLEMENT_REPLAY_WINDOW_MS = SETTLEMENT_REPLAY_WINDOW_MS;
1206
1428
  exports.SupabaseSeenTxStore = SupabaseSeenTxStore;
1207
1429
  exports.SupabaseSessionStore = SupabaseSessionStore;
1430
+ exports.checkSettlementReplay = checkSettlementReplay;
1208
1431
  exports.createMppChargeHandler = createMppChargeHandler;
1209
1432
  exports.createMppSessionHandler = createMppSessionHandler;
1210
1433
  exports.createX402Handler = createX402Handler;
1211
1434
  exports.decimalToStroops = decimalToStroops;
1435
+ exports.getEligibleAssets = getEligibleAssets;
1436
+ exports.isAssetEligible = isAssetEligible;
1212
1437
  exports.isVoucherStoreValue = isVoucherStoreValue;
1438
+ exports.normalizeManifestAssets = normalizeManifestAssets;
1213
1439
  exports.paymentIdempotencyKey = paymentIdempotencyKey;
1214
1440
  exports.reconcileAbandonedSessions = reconcileAbandonedSessions;
1215
1441
  exports.registerProvider = registerProvider;
1442
+ exports.resolveAssetContract = resolveAssetContract;
1216
1443
  exports.routedock = routedock;
1217
1444
  exports.runStartupReconciliation = runStartupReconciliation;
1445
+ exports.selectAsset = selectAsset;