@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
@@ -4,8 +4,8 @@ import { ExactStellarScheme as ExactStellarScheme$1 } from '@x402/stellar/exact/
4
4
  import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server';
5
5
  import { createEd25519Signer } from '@x402/stellar';
6
6
  import { encodePaymentRequiredHeader, decodePaymentSignatureHeader, encodePaymentResponseHeader } from '@x402/core/http';
7
- import { stellar } from '@stellar/mpp/charge/server';
8
- import { Store, stellar as stellar$1, close } from '@stellar/mpp/channel/server';
7
+ import { stellar as stellar$1 } from '@stellar/mpp/charge/server';
8
+ import { Store, stellar, close } from '@stellar/mpp/channel/server';
9
9
  import { Mppx } from 'mppx/server';
10
10
  import { createHash } from 'crypto';
11
11
  import { usdcToStroops } from '@routedock/nulth-sdk';
@@ -109,7 +109,7 @@ function verifyManifestSignature(manifest, expectedPayee) {
109
109
  }
110
110
  }
111
111
 
112
- // src/provider/payee.ts
112
+ // src/internal/payee.ts
113
113
  function resolvePayee(manifest, mode) {
114
114
  const override = manifest.pricing[mode]?.payee;
115
115
  return override ?? manifest.payee;
@@ -119,6 +119,22 @@ function extractPayerAddress(key) {
119
119
  if (typeof key !== "string") return null;
120
120
  return StrKey.isValidEd25519PublicKey(key) || StrKey.isValidMed25519PublicKey(key) ? key : null;
121
121
  }
122
+
123
+ // src/internal/logger.ts
124
+ function createConsoleLogger() {
125
+ return (level, message, fields) => {
126
+ const values = fields ? Object.values(fields) : [];
127
+ if (values.length > 0) {
128
+ console[level](message, ...values);
129
+ } else {
130
+ console[level](message);
131
+ }
132
+ };
133
+ }
134
+ var consoleLogger = createConsoleLogger();
135
+ function resolveLogger(logger, fallback = consoleLogger) {
136
+ return logger ?? fallback;
137
+ }
122
138
  function channelAuthorizer(envelopeSigner) {
123
139
  return { envelopeSigner };
124
140
  }
@@ -204,8 +220,24 @@ function hexToBytes(hex) {
204
220
  }
205
221
 
206
222
  // src/provider/SeenTxStore.ts
223
+ var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
224
+ var MAX_SETTLEMENT_REPLAYS = 1;
225
+ async function checkSettlementReplay(store, key, now = Date.now()) {
226
+ const record = await store.get(key);
227
+ if (!record) return { kind: "miss" };
228
+ const { createdAt } = record;
229
+ if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
230
+ return { kind: "spent" };
231
+ }
232
+ if (store.claimReplay) {
233
+ const replayed = await store.claimReplay(key);
234
+ if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
235
+ }
236
+ return { kind: "replay", record };
237
+ }
207
238
  var InMemorySeenTxStore = class {
208
239
  map = /* @__PURE__ */ new Map();
240
+ replayCounts = /* @__PURE__ */ new Map();
209
241
  order = [];
210
242
  constructor(maxEntriesOrOptions = 1e4) {
211
243
  const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
@@ -213,7 +245,8 @@ var InMemorySeenTxStore = class {
213
245
  if (options.warn !== false) {
214
246
  const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
215
247
  if (isServerless) {
216
- console.warn(
248
+ resolveLogger(options.logger)(
249
+ "warn",
217
250
  "[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."
218
251
  );
219
252
  }
@@ -228,21 +261,115 @@ var InMemorySeenTxStore = class {
228
261
  this.order.push(key);
229
262
  if (this.order.length > this.maxEntries) {
230
263
  const evicted = this.order.shift();
231
- if (evicted !== void 0) this.map.delete(evicted);
264
+ if (evicted !== void 0) {
265
+ this.map.delete(evicted);
266
+ this.replayCounts.delete(evicted);
267
+ }
232
268
  }
233
269
  }
234
270
  this.map.set(key, record);
235
271
  }
272
+ claimReplay(key) {
273
+ if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
274
+ const next = (this.replayCounts.get(key) ?? 0) + 1;
275
+ this.replayCounts.set(key, next);
276
+ return next;
277
+ }
236
278
  };
237
279
  async function sha256Hex(input) {
238
280
  const buf = new TextEncoder().encode(input);
239
281
  const digest = await crypto.subtle.digest("SHA-256", buf);
240
282
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
241
283
  }
242
- async function paymentIdempotencyKey(getHeader) {
284
+ async function paymentIdempotencyKey(getHeader, scope) {
243
285
  const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
244
286
  if (!material) return null;
245
- return sha256Hex(material);
287
+ return sha256Hex(
288
+ [scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
289
+ );
290
+ }
291
+
292
+ // src/internal/assetUtils.ts
293
+ function normalizeManifestAssets(manifest) {
294
+ if (manifest.assets && manifest.assets.length > 0) {
295
+ return manifest.assets;
296
+ }
297
+ if (manifest.asset && manifest.asset_contract) {
298
+ return [
299
+ {
300
+ asset: manifest.asset,
301
+ asset_contract: manifest.asset_contract
302
+ }
303
+ ];
304
+ }
305
+ throw new RouteDockManifestError(
306
+ "Manifest must define either assets[] or root asset/asset_contract fields"
307
+ );
308
+ }
309
+ function getEligibleAssets(manifest, mode, endpoint) {
310
+ const assets = normalizeManifestAssets(manifest);
311
+ return assets.filter((asset) => {
312
+ if (asset.modes && !asset.modes.includes(mode)) {
313
+ return false;
314
+ }
315
+ if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
316
+ const trimmedEndpoint = endpoint.replace(/^\//, "");
317
+ if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
318
+ return true;
319
+ }
320
+ if (manifest.endpoints) {
321
+ let pathname = endpoint;
322
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
323
+ try {
324
+ pathname = new URL(endpoint).pathname;
325
+ } catch {
326
+ }
327
+ }
328
+ const trimmedPathname = pathname.replace(/^\//, "");
329
+ const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
330
+ const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
331
+ const keyMatches = key === endpoint || key === trimmedEndpoint;
332
+ if (pathMatches || keyMatches) {
333
+ return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
334
+ }
335
+ return false;
336
+ });
337
+ if (!matchesEndpoint) {
338
+ return false;
339
+ }
340
+ } else {
341
+ return false;
342
+ }
343
+ }
344
+ return true;
345
+ });
346
+ }
347
+ function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
348
+ if (manifest.assets && manifest.assets.length > 0) {
349
+ return selectAsset(manifest, mode, endpoint).asset_contract;
350
+ }
351
+ return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
352
+ }
353
+ function selectAsset(manifest, mode, endpoint) {
354
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
355
+ if (eligible.length === 0) {
356
+ const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
357
+ throw new RouteDockManifestError(
358
+ `No eligible assets found for mode '${mode}'${endpointMsg}`
359
+ );
360
+ }
361
+ return eligible[0];
362
+ }
363
+
364
+ // src/provider/settleResult.ts
365
+ function readSettleResult(result) {
366
+ const settle = result;
367
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
368
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
369
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
370
+ return failed(settle.errorReason);
371
+ }
372
+ return { ok: true, txHash: settle.transaction };
246
373
  }
247
374
 
248
375
  // src/provider/registerProvider.ts
@@ -295,7 +422,7 @@ var SupabaseSeenTxStore = class {
295
422
  }
296
423
  supabase;
297
424
  async get(key) {
298
- const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
425
+ const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
299
426
  if (error) {
300
427
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
301
428
  }
@@ -303,7 +430,8 @@ var SupabaseSeenTxStore = class {
303
430
  const headers = data.headers;
304
431
  return {
305
432
  txHash: data.tx_hash ?? null,
306
- ...headers ? { headers } : {}
433
+ ...headers ? { headers } : {},
434
+ createdAt: Date.parse(data.created_at)
307
435
  };
308
436
  }
309
437
  async set(key, record) {
@@ -311,7 +439,8 @@ var SupabaseSeenTxStore = class {
311
439
  {
312
440
  key,
313
441
  tx_hash: record.txHash,
314
- headers: record.headers ?? null
442
+ headers: record.headers ?? null,
443
+ created_at: new Date(record.createdAt ?? Date.now()).toISOString()
315
444
  },
316
445
  { onConflict: "key" }
317
446
  );
@@ -319,6 +448,22 @@ var SupabaseSeenTxStore = class {
319
448
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
320
449
  }
321
450
  }
451
+ /**
452
+ * Atomically increment the settlement's replay counter through the
453
+ * `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
454
+ * row lock gives concurrent claims distinct counts. Returns `Infinity` when
455
+ * the key no longer exists, so the caller treats it as spent.
456
+ */
457
+ async claimReplay(key) {
458
+ const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
459
+ p_key: key
460
+ });
461
+ if (error) {
462
+ throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
463
+ }
464
+ if (data == null) return Number.POSITIVE_INFINITY;
465
+ return data;
466
+ }
322
467
  };
323
468
  var MPP_NETWORK = {
324
469
  testnet: "stellar:testnet",
@@ -398,25 +543,26 @@ async function reconcileAbandonedSessions(opts) {
398
543
  }
399
544
  }
400
545
  async function runStartupReconciliation(opts, logger) {
401
- const log = logger || console.log;
402
- log("[SessionReconciler] Running startup reconciliation...");
546
+ const log = resolveLogger(logger);
547
+ log("info", "[SessionReconciler] Running startup reconciliation...");
403
548
  try {
404
549
  const stats = await reconcileAbandonedSessions(opts);
405
550
  if (stats.orphanedCount === 0) {
406
- log("[SessionReconciler] No orphaned sessions found");
551
+ log("info", "[SessionReconciler] No orphaned sessions found");
407
552
  return;
408
553
  }
409
554
  log(
555
+ "info",
410
556
  `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
411
557
  );
412
558
  if (stats.errors.length > 0) {
413
- log("[SessionReconciler] Reconciliation errors:");
559
+ log("warn", "[SessionReconciler] Reconciliation errors:");
414
560
  for (const err of stats.errors) {
415
- log(` - ${err.channelId}: ${err.reason}`);
561
+ log("warn", ` - ${err.channelId}: ${err.reason}`);
416
562
  }
417
563
  }
418
564
  } catch (err) {
419
- log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
565
+ log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
420
566
  }
421
567
  }
422
568
 
@@ -431,7 +577,8 @@ function createX402HonoHandler(opts) {
431
577
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
432
578
  const signer = createEd25519Signer(opts.payeeSecretKey, caip2);
433
579
  const x402Price = opts.pricing.x402;
434
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
580
+ const logger = resolveLogger(opts.logger);
581
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
435
582
  const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
436
583
  const localFacilitator = new ExactStellarScheme([signer], {
437
584
  areFeesSponsored: true
@@ -451,22 +598,28 @@ function createX402HonoHandler(opts) {
451
598
  ozServer.register(caip2, new ExactStellarScheme$1());
452
599
  }
453
600
  const amountInBaseUnits = String(usdcToUnits(x402Price));
454
- const requirements = {
455
- scheme: "exact",
456
- network: caip2,
457
- asset: opts.assetContract,
458
- amount: amountInBaseUnits,
459
- payTo: resolvePayee(opts.manifest, "x402"),
460
- maxTimeoutSeconds: 60,
461
- extra: {
462
- areFeesSponsored: true,
463
- ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
464
- }
465
- };
466
601
  return async (c, next) => {
467
602
  try {
468
- const paymentHeader = c.req.header("payment-signature") ?? c.req.header("x-payment");
469
- if (!paymentHeader) {
603
+ const endpoint = c.req.path;
604
+ const assetContract = resolveAssetContract(
605
+ opts.manifest,
606
+ "x402",
607
+ endpoint,
608
+ opts.assetContract
609
+ );
610
+ const requirements = {
611
+ scheme: "exact",
612
+ network: caip2,
613
+ asset: assetContract,
614
+ amount: amountInBaseUnits,
615
+ payTo: resolvePayee(opts.manifest, "x402"),
616
+ maxTimeoutSeconds: 60,
617
+ extra: {
618
+ areFeesSponsored: true,
619
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
620
+ }
621
+ };
622
+ const respondPaymentRequired = async (error, reason) => {
470
623
  if (ozServer) {
471
624
  const resourceInfo = {
472
625
  url: c.req.url,
@@ -477,7 +630,6 @@ function createX402HonoHandler(opts) {
477
630
  resourceInfo
478
631
  );
479
632
  c.header("X-Payment-Requirements", encodePaymentRequiredHeader(paymentRequired));
480
- return c.json({ error: "Payment Required" }, 402);
481
633
  } else {
482
634
  const x402Response = {
483
635
  x402Version: 2,
@@ -485,15 +637,30 @@ function createX402HonoHandler(opts) {
485
637
  accepts: [requirements]
486
638
  };
487
639
  c.header("X-Payment-Requirements", encodePaymentRequiredHeader(x402Response));
488
- return c.json({ error: "Payment Required" }, 402);
489
640
  }
641
+ return c.json({ error, ...reason ? { reason } : {} }, 402);
642
+ };
643
+ const paymentHeader = c.req.header("payment-signature") ?? c.req.header("x-payment");
644
+ if (!paymentHeader) {
645
+ return respondPaymentRequired("Payment Required");
490
646
  }
491
- const idempotencyKey = await paymentIdempotencyKey((name) => c.req.header(name));
647
+ const idempotencyKey = await paymentIdempotencyKey(
648
+ (name) => c.req.header(name),
649
+ {
650
+ method: c.req.method,
651
+ path: c.req.path,
652
+ amount: requirements.amount,
653
+ payTo: requirements.payTo
654
+ }
655
+ );
492
656
  if (idempotencyKey) {
493
- const cached = await seenTxStore.get(idempotencyKey);
494
- if (cached) {
495
- if (cached.headers) {
496
- for (const [k, val] of Object.entries(cached.headers)) {
657
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
658
+ if (replayCheck.kind === "spent") {
659
+ return c.json({ error: "Payment already used" }, 402);
660
+ }
661
+ if (replayCheck.kind === "replay") {
662
+ if (replayCheck.record.headers) {
663
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
497
664
  c.header(k, val);
498
665
  }
499
666
  }
@@ -513,13 +680,15 @@ function createX402HonoHandler(opts) {
513
680
  }
514
681
  if (ozServer) {
515
682
  const settleResult = await ozServer.settlePayment(payload, requirements);
516
- txHash = settleResult.transaction ?? null;
517
- if (settleResult) {
518
- paymentResponseHeader = encodePaymentResponseHeader(
519
- settleResult
520
- );
521
- c.header("X-Payment-Response", paymentResponseHeader);
683
+ const outcome = readSettleResult(settleResult);
684
+ if (!outcome.ok) {
685
+ return respondPaymentRequired("Payment settlement failed", outcome.reason);
522
686
  }
687
+ txHash = outcome.txHash;
688
+ paymentResponseHeader = encodePaymentResponseHeader(
689
+ settleResult
690
+ );
691
+ c.header("X-Payment-Response", paymentResponseHeader);
523
692
  } else {
524
693
  const verifyResult = await localFacilitator.verify(
525
694
  payload,
@@ -538,28 +707,33 @@ function createX402HonoHandler(opts) {
538
707
  payload,
539
708
  requirements
540
709
  );
541
- txHash = settleResult.transaction ?? null;
542
- if (settleResult) {
543
- paymentResponseHeader = encodePaymentResponseHeader(
544
- settleResult
545
- );
546
- c.header("X-Payment-Response", paymentResponseHeader);
710
+ const outcome = readSettleResult(settleResult);
711
+ if (!outcome.ok) {
712
+ return respondPaymentRequired("Payment settlement failed", outcome.reason);
547
713
  }
714
+ txHash = outcome.txHash;
715
+ paymentResponseHeader = encodePaymentResponseHeader(
716
+ settleResult
717
+ );
718
+ c.header("X-Payment-Response", paymentResponseHeader);
548
719
  }
549
720
  if (idempotencyKey) {
550
721
  const headers = {};
551
722
  if (paymentResponseHeader) headers["X-Payment-Response"] = paymentResponseHeader;
552
- await seenTxStore.set(idempotencyKey, { txHash, headers });
723
+ await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
553
724
  }
554
725
  if (txHash && opts.onSettled) {
555
726
  Promise.resolve().then(() => opts.onSettled(txHash, x402Price, "x402", payerAddress)).catch((err) => {
556
- console.error("[x402] onSettled callback error:", err);
727
+ logger("error", "[x402] onSettled callback error", { error: err });
557
728
  opts.onCallbackError?.(err, "onSettled");
558
729
  });
559
730
  }
560
731
  await next();
561
732
  } catch (err) {
562
- console.error("[x402] Settlement error:", err);
733
+ if (err instanceof RouteDockManifestError) {
734
+ throw err;
735
+ }
736
+ logger("error", "[x402] Settlement error", { error: err });
563
737
  return c.json({ error: "Payment settlement failed" }, 500);
564
738
  }
565
739
  };
@@ -568,20 +742,37 @@ function createMppChargeHonoHandler(opts) {
568
742
  const networkId = CAIP2[opts.network];
569
743
  const chargePrice = opts.pricing["mpp-charge"];
570
744
  const recipient = resolvePayee(opts.manifest, "mpp-charge");
571
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
572
- const mppx = Mppx.create({
573
- secretKey: opts.payeeSecretKey,
574
- methods: [
575
- stellar({
576
- recipient,
577
- currency: opts.assetContract,
578
- network: networkId,
579
- feePayer: { envelopeSigner: opts.payeeSecretKey }
580
- })
581
- ]
582
- });
745
+ const logger = resolveLogger(opts.logger);
746
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
747
+ const mppxInstances = /* @__PURE__ */ new Map();
748
+ function getMppx(contract) {
749
+ let instance = mppxInstances.get(contract);
750
+ if (!instance) {
751
+ instance = Mppx.create({
752
+ secretKey: opts.payeeSecretKey,
753
+ methods: [
754
+ stellar$1({
755
+ recipient,
756
+ currency: contract,
757
+ network: networkId,
758
+ feePayer: { envelopeSigner: opts.payeeSecretKey }
759
+ })
760
+ ]
761
+ });
762
+ mppxInstances.set(contract, instance);
763
+ }
764
+ return instance;
765
+ }
583
766
  return async (c, next) => {
584
767
  try {
768
+ const endpoint = c.req.path;
769
+ const assetContract = resolveAssetContract(
770
+ opts.manifest,
771
+ "mpp-charge",
772
+ endpoint,
773
+ opts.assetContract
774
+ );
775
+ const mppx = getMppx(assetContract);
585
776
  let payerAddress = null;
586
777
  try {
587
778
  const authHeader = c.req.header("authorization");
@@ -597,12 +788,23 @@ function createMppChargeHonoHandler(opts) {
597
788
  }
598
789
  } catch {
599
790
  }
600
- const idempotencyKey = await paymentIdempotencyKey((name) => c.req.header(name));
791
+ const idempotencyKey = await paymentIdempotencyKey(
792
+ (name) => c.req.header(name),
793
+ {
794
+ method: c.req.method,
795
+ path: c.req.path,
796
+ amount: chargePrice,
797
+ payTo: recipient
798
+ }
799
+ );
601
800
  if (idempotencyKey) {
602
- const cached = await seenTxStore.get(idempotencyKey);
603
- if (cached) {
604
- if (cached.headers) {
605
- for (const [k, val] of Object.entries(cached.headers)) {
801
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
802
+ if (replayCheck.kind === "spent") {
803
+ return c.json({ error: "Payment already used" }, 402);
804
+ }
805
+ if (replayCheck.kind === "replay") {
806
+ if (replayCheck.record.headers) {
807
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
606
808
  c.header(k, val);
607
809
  }
608
810
  }
@@ -613,7 +815,7 @@ function createMppChargeHonoHandler(opts) {
613
815
  const handler = mppx["stellar/charge"];
614
816
  const result = await handler({
615
817
  amount: chargePrice,
616
- currency: opts.assetContract,
818
+ currency: assetContract,
617
819
  recipient,
618
820
  description: opts.manifest.name
619
821
  })(c.req.raw);
@@ -643,12 +845,13 @@ function createMppChargeHonoHandler(opts) {
643
845
  if (idempotencyKey) {
644
846
  await seenTxStore.set(idempotencyKey, {
645
847
  txHash: reference ?? null,
646
- headers: receiptHeaders
848
+ headers: receiptHeaders,
849
+ createdAt: Date.now()
647
850
  });
648
851
  }
649
852
  if (reference && opts.onSettled) {
650
853
  Promise.resolve().then(() => opts.onSettled(reference, chargePrice, "mpp-charge", payerAddress)).catch((err) => {
651
- console.error("[mpp-charge] onSettled callback error:", err);
854
+ logger("error", "[mpp-charge] onSettled callback error", { error: err });
652
855
  opts.onCallbackError?.(err, "onSettled");
653
856
  });
654
857
  }
@@ -665,6 +868,7 @@ function mppSessionWsVerified(c) {
665
868
  function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
666
869
  const networkId = CAIP2[opts.network];
667
870
  const payeeKeypair = Keypair.fromSecret(opts.payeeSecretKey);
871
+ const logger = resolveLogger(opts.logger);
668
872
  const voucherRecordKey = `routedock:session:voucher:${sessionPricing.channelFactory}`;
669
873
  const innerStore = opts.sessionStore ?? Store.memory();
670
874
  let record = null;
@@ -717,7 +921,7 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
717
921
  reason
718
922
  });
719
923
  } catch (err) {
720
- console.error("[mpp-session] onOrphaned handler failed:", err);
924
+ logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
721
925
  }
722
926
  }
723
927
  }
@@ -764,7 +968,7 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
764
968
  sessionOpened = true;
765
969
  if (opts.onSessionOpen) {
766
970
  Promise.resolve().then(() => opts.onSessionOpen(sessionPricing.channelFactory, record.payer)).catch((err) => {
767
- console.error("[mpp-session] onSessionOpen callback error:", err);
971
+ logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
768
972
  opts.onCallbackError?.(err, "onSessionOpen");
769
973
  });
770
974
  }
@@ -772,7 +976,7 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
772
976
  if (opts.onVoucher) {
773
977
  const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
774
978
  Promise.resolve().then(() => opts.onVoucher(sessionPricing.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
775
- console.error("[mpp-session] onVoucher callback error:", err);
979
+ logger("error", "[mpp-session] onVoucher callback error", { error: err });
776
980
  opts.onCallbackError?.(err, "onVoucher");
777
981
  });
778
982
  }
@@ -782,7 +986,7 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
782
986
  methods: [
783
987
  withTypedChannelErrors(
784
988
  onVerifiedCredential(
785
- stellar$1({
989
+ stellar({
786
990
  channel: sessionPricing.channelFactory,
787
991
  commitmentKey: opts.commitmentPublicKey,
788
992
  network: networkId,
@@ -829,7 +1033,7 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
829
1033
  if (opts.onSettled) {
830
1034
  const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
831
1035
  Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, reportMode, closePayer)).catch((err) => {
832
- console.error(`[mpp-session] onSettled callback error:`, err);
1036
+ logger("error", "[mpp-session] onSettled callback error", { error: err });
833
1037
  opts.onCallbackError?.(err, "onSettled");
834
1038
  });
835
1039
  }