@routedock/routedock 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +305 -0
  2. package/dist/MppSessionHandler-1EL8QBAn.d.cts +215 -0
  3. package/dist/MppSessionHandler-Bq4S-JTE.d.ts +215 -0
  4. package/dist/{ProviderRegistry-BJ9oJLVz.d.cts → ProviderRegistry-CyZ5eryO.d.cts} +2 -2
  5. package/dist/{ProviderRegistry-C50nyhaF.d.ts → ProviderRegistry-w4_hkI_1.d.ts} +2 -2
  6. package/dist/{RouteDockClient-BbkqjhMG.d.ts → RouteDockClient-CXvWXix5.d.cts} +25 -9
  7. package/dist/{RouteDockClient-B1roCa8h.d.cts → RouteDockClient-CjM2VyOd.d.ts} +25 -9
  8. package/dist/{SessionReconciler-Dscw43F3.d.ts → SessionReconciler-Bw-PoBVl.d.ts} +14 -5
  9. package/dist/{SessionReconciler-bi0z2qtN.d.cts → SessionReconciler-Cz2nuOlJ.d.cts} +14 -5
  10. package/dist/{SessionStore-DvkZv8-E.d.ts → SessionStore-B4-Hi4H8.d.cts} +34 -2
  11. package/dist/{SessionStore-BpK8vv0L.d.cts → SessionStore-BaYiegP0.d.ts} +34 -2
  12. package/dist/client.cjs +432 -125
  13. package/dist/client.d.cts +9 -8
  14. package/dist/client.d.ts +9 -8
  15. package/dist/client.js +424 -127
  16. package/dist/index.cjs +655 -228
  17. package/dist/index.d.cts +8 -8
  18. package/dist/index.d.ts +8 -8
  19. package/dist/index.js +646 -229
  20. package/dist/{types-DmhmfLzP.d.cts → logger-D8_Q2ytp.d.cts} +58 -1
  21. package/dist/{types-DmhmfLzP.d.ts → logger-D8_Q2ytp.d.ts} +58 -1
  22. package/dist/provider/fastify.cjs +306 -89
  23. package/dist/provider/fastify.d.cts +9 -4
  24. package/dist/provider/fastify.d.ts +9 -4
  25. package/dist/provider/fastify.js +305 -88
  26. package/dist/provider/hono.cjs +286 -82
  27. package/dist/provider/hono.d.cts +9 -7
  28. package/dist/provider/hono.d.ts +9 -7
  29. package/dist/provider/hono.js +282 -78
  30. package/dist/provider.cjs +326 -98
  31. package/dist/provider.d.cts +14 -10
  32. package/dist/provider.d.ts +14 -10
  33. package/dist/provider.js +318 -98
  34. package/dist/react.cjs +410 -125
  35. package/dist/react.d.cts +2 -2
  36. package/dist/react.d.ts +2 -2
  37. package/dist/react.js +411 -126
  38. package/dist/{routedockMiddleware-BlDsHuPl.d.ts → routedockMiddleware-57gUaEMf.d.ts} +9 -4
  39. package/dist/{routedockMiddleware-D97A5ptQ.d.cts → routedockMiddleware-DlyLrxp7.d.cts} +9 -4
  40. package/dist/schema.cjs +40 -0
  41. package/dist/schema.js +40 -0
  42. package/dist/{sign-Dpj2Mp7k.d.ts → sign-AKH67sft.d.cts} +1 -1
  43. package/dist/{sign-CUf3rhTx.d.cts → sign-BEq-OsSq.d.ts} +1 -1
  44. package/dist/testing.cjs +6 -4
  45. package/dist/testing.d.cts +32 -13
  46. package/dist/testing.d.ts +32 -13
  47. package/dist/testing.js +6 -4
  48. package/package.json +2 -2
  49. package/dist/MppSessionHandler-B4TyD7Xg.d.cts +0 -121
  50. package/dist/MppSessionHandler-CLB5SzgN.d.ts +0 -121
@@ -6,9 +6,9 @@ var server$1 = require('@x402/stellar/exact/server');
6
6
  var server = require('@x402/core/server');
7
7
  var stellar = require('@x402/stellar');
8
8
  var http = require('@x402/core/http');
9
- var server$3 = require('@stellar/mpp/charge/server');
10
- var server$4 = require('@stellar/mpp/channel/server');
11
- var server$2 = require('mppx/server');
9
+ var server$4 = require('@stellar/mpp/charge/server');
10
+ var server$2 = require('@stellar/mpp/channel/server');
11
+ var server$3 = require('mppx/server');
12
12
  var crypto$1 = require('crypto');
13
13
  var nulthSdk = require('@routedock/nulth-sdk');
14
14
  var mppx = require('mppx');
@@ -111,7 +111,7 @@ function verifyManifestSignature(manifest, expectedPayee) {
111
111
  }
112
112
  }
113
113
 
114
- // src/provider/payee.ts
114
+ // src/internal/payee.ts
115
115
  function resolvePayee(manifest, mode) {
116
116
  const override = manifest.pricing[mode]?.payee;
117
117
  return override ?? manifest.payee;
@@ -121,6 +121,22 @@ function extractPayerAddress(key) {
121
121
  if (typeof key !== "string") return null;
122
122
  return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
123
123
  }
124
+
125
+ // src/internal/logger.ts
126
+ function createConsoleLogger() {
127
+ return (level, message, fields) => {
128
+ const values = fields ? Object.values(fields) : [];
129
+ if (values.length > 0) {
130
+ console[level](message, ...values);
131
+ } else {
132
+ console[level](message);
133
+ }
134
+ };
135
+ }
136
+ var consoleLogger = createConsoleLogger();
137
+ function resolveLogger(logger, fallback = consoleLogger) {
138
+ return logger ?? fallback;
139
+ }
124
140
  function channelAuthorizer(envelopeSigner) {
125
141
  return { envelopeSigner };
126
142
  }
@@ -206,8 +222,24 @@ function hexToBytes(hex) {
206
222
  }
207
223
 
208
224
  // src/provider/SeenTxStore.ts
225
+ var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
226
+ var MAX_SETTLEMENT_REPLAYS = 1;
227
+ async function checkSettlementReplay(store, key, now = Date.now()) {
228
+ const record = await store.get(key);
229
+ if (!record) return { kind: "miss" };
230
+ const { createdAt } = record;
231
+ if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
232
+ return { kind: "spent" };
233
+ }
234
+ if (store.claimReplay) {
235
+ const replayed = await store.claimReplay(key);
236
+ if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
237
+ }
238
+ return { kind: "replay", record };
239
+ }
209
240
  var InMemorySeenTxStore = class {
210
241
  map = /* @__PURE__ */ new Map();
242
+ replayCounts = /* @__PURE__ */ new Map();
211
243
  order = [];
212
244
  constructor(maxEntriesOrOptions = 1e4) {
213
245
  const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
@@ -215,7 +247,8 @@ var InMemorySeenTxStore = class {
215
247
  if (options.warn !== false) {
216
248
  const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
217
249
  if (isServerless) {
218
- console.warn(
250
+ resolveLogger(options.logger)(
251
+ "warn",
219
252
  "[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."
220
253
  );
221
254
  }
@@ -230,21 +263,115 @@ var InMemorySeenTxStore = class {
230
263
  this.order.push(key);
231
264
  if (this.order.length > this.maxEntries) {
232
265
  const evicted = this.order.shift();
233
- if (evicted !== void 0) this.map.delete(evicted);
266
+ if (evicted !== void 0) {
267
+ this.map.delete(evicted);
268
+ this.replayCounts.delete(evicted);
269
+ }
234
270
  }
235
271
  }
236
272
  this.map.set(key, record);
237
273
  }
274
+ claimReplay(key) {
275
+ if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
276
+ const next = (this.replayCounts.get(key) ?? 0) + 1;
277
+ this.replayCounts.set(key, next);
278
+ return next;
279
+ }
238
280
  };
239
281
  async function sha256Hex(input) {
240
282
  const buf = new TextEncoder().encode(input);
241
283
  const digest = await crypto.subtle.digest("SHA-256", buf);
242
284
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
243
285
  }
244
- async function paymentIdempotencyKey(getHeader) {
286
+ async function paymentIdempotencyKey(getHeader, scope) {
245
287
  const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
246
288
  if (!material) return null;
247
- return sha256Hex(material);
289
+ return sha256Hex(
290
+ [scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
291
+ );
292
+ }
293
+
294
+ // src/internal/assetUtils.ts
295
+ function normalizeManifestAssets(manifest) {
296
+ if (manifest.assets && manifest.assets.length > 0) {
297
+ return manifest.assets;
298
+ }
299
+ if (manifest.asset && manifest.asset_contract) {
300
+ return [
301
+ {
302
+ asset: manifest.asset,
303
+ asset_contract: manifest.asset_contract
304
+ }
305
+ ];
306
+ }
307
+ throw new RouteDockManifestError(
308
+ "Manifest must define either assets[] or root asset/asset_contract fields"
309
+ );
310
+ }
311
+ function getEligibleAssets(manifest, mode, endpoint) {
312
+ const assets = normalizeManifestAssets(manifest);
313
+ return assets.filter((asset) => {
314
+ if (asset.modes && !asset.modes.includes(mode)) {
315
+ return false;
316
+ }
317
+ if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
318
+ const trimmedEndpoint = endpoint.replace(/^\//, "");
319
+ if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
320
+ return true;
321
+ }
322
+ if (manifest.endpoints) {
323
+ let pathname = endpoint;
324
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
325
+ try {
326
+ pathname = new URL(endpoint).pathname;
327
+ } catch {
328
+ }
329
+ }
330
+ const trimmedPathname = pathname.replace(/^\//, "");
331
+ const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
332
+ const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
333
+ const keyMatches = key === endpoint || key === trimmedEndpoint;
334
+ if (pathMatches || keyMatches) {
335
+ return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
336
+ }
337
+ return false;
338
+ });
339
+ if (!matchesEndpoint) {
340
+ return false;
341
+ }
342
+ } else {
343
+ return false;
344
+ }
345
+ }
346
+ return true;
347
+ });
348
+ }
349
+ function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
350
+ if (manifest.assets && manifest.assets.length > 0) {
351
+ return selectAsset(manifest, mode, endpoint).asset_contract;
352
+ }
353
+ return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
354
+ }
355
+ function selectAsset(manifest, mode, endpoint) {
356
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
357
+ if (eligible.length === 0) {
358
+ const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
359
+ throw new RouteDockManifestError(
360
+ `No eligible assets found for mode '${mode}'${endpointMsg}`
361
+ );
362
+ }
363
+ return eligible[0];
364
+ }
365
+
366
+ // src/provider/settleResult.ts
367
+ function readSettleResult(result) {
368
+ const settle = result;
369
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
370
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
371
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
372
+ return failed(settle.errorReason);
373
+ }
374
+ return { ok: true, txHash: settle.transaction };
248
375
  }
249
376
 
250
377
  // src/provider/registerProvider.ts
@@ -297,7 +424,7 @@ var SupabaseSeenTxStore = class {
297
424
  }
298
425
  supabase;
299
426
  async get(key) {
300
- const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers").eq("key", key).maybeSingle();
427
+ const { data, error } = await this.supabase.from("settlements").select("tx_hash, headers, created_at").eq("key", key).maybeSingle();
301
428
  if (error) {
302
429
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.get failed: ${error.message}`);
303
430
  }
@@ -305,7 +432,8 @@ var SupabaseSeenTxStore = class {
305
432
  const headers = data.headers;
306
433
  return {
307
434
  txHash: data.tx_hash ?? null,
308
- ...headers ? { headers } : {}
435
+ ...headers ? { headers } : {},
436
+ createdAt: Date.parse(data.created_at)
309
437
  };
310
438
  }
311
439
  async set(key, record) {
@@ -313,7 +441,8 @@ var SupabaseSeenTxStore = class {
313
441
  {
314
442
  key,
315
443
  tx_hash: record.txHash,
316
- headers: record.headers ?? null
444
+ headers: record.headers ?? null,
445
+ created_at: new Date(record.createdAt ?? Date.now()).toISOString()
317
446
  },
318
447
  { onConflict: "key" }
319
448
  );
@@ -321,6 +450,22 @@ var SupabaseSeenTxStore = class {
321
450
  throw new RouteDockNetworkError(`SupabaseSeenTxStore.set failed: ${error.message}`);
322
451
  }
323
452
  }
453
+ /**
454
+ * Atomically increment the settlement's replay counter through the
455
+ * `claim_settlement_replay` Postgres function (migration 006). The UPDATE's
456
+ * row lock gives concurrent claims distinct counts. Returns `Infinity` when
457
+ * the key no longer exists, so the caller treats it as spent.
458
+ */
459
+ async claimReplay(key) {
460
+ const { data, error } = await this.supabase.rpc("claim_settlement_replay", {
461
+ p_key: key
462
+ });
463
+ if (error) {
464
+ throw new RouteDockNetworkError(`SupabaseSeenTxStore.claimReplay failed: ${error.message}`);
465
+ }
466
+ if (data == null) return Number.POSITIVE_INFINITY;
467
+ return data;
468
+ }
324
469
  };
325
470
  var MPP_NETWORK = {
326
471
  testnet: "stellar:testnet",
@@ -400,25 +545,26 @@ async function reconcileAbandonedSessions(opts) {
400
545
  }
401
546
  }
402
547
  async function runStartupReconciliation(opts, logger) {
403
- const log = logger || console.log;
404
- log("[SessionReconciler] Running startup reconciliation...");
548
+ const log = resolveLogger(logger);
549
+ log("info", "[SessionReconciler] Running startup reconciliation...");
405
550
  try {
406
551
  const stats = await reconcileAbandonedSessions(opts);
407
552
  if (stats.orphanedCount === 0) {
408
- log("[SessionReconciler] No orphaned sessions found");
553
+ log("info", "[SessionReconciler] No orphaned sessions found");
409
554
  return;
410
555
  }
411
556
  log(
557
+ "info",
412
558
  `[SessionReconciler] Found ${stats.orphanedCount} orphaned session(s). Recovered: ${stats.recoveredCount}, Skipped: ${stats.skippedCount}, Failed: ${stats.failedCount}`
413
559
  );
414
560
  if (stats.errors.length > 0) {
415
- log("[SessionReconciler] Reconciliation errors:");
561
+ log("warn", "[SessionReconciler] Reconciliation errors:");
416
562
  for (const err of stats.errors) {
417
- log(` - ${err.channelId}: ${err.reason}`);
563
+ log("warn", ` - ${err.channelId}: ${err.reason}`);
418
564
  }
419
565
  }
420
566
  } catch (err) {
421
- log(`[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
567
+ log("error", `[SessionReconciler] Startup reconciliation failed: ${formatMppError(err)}`);
422
568
  }
423
569
  }
424
570
 
@@ -433,7 +579,8 @@ function createX402HonoHandler(opts) {
433
579
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
434
580
  const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
435
581
  const x402Price = opts.pricing.x402;
436
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
582
+ const logger = resolveLogger(opts.logger);
583
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
437
584
  const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
438
585
  const localFacilitator = new facilitator.ExactStellarScheme([signer], {
439
586
  areFeesSponsored: true
@@ -453,22 +600,28 @@ function createX402HonoHandler(opts) {
453
600
  ozServer.register(caip2, new server$1.ExactStellarScheme());
454
601
  }
455
602
  const amountInBaseUnits = String(usdcToUnits(x402Price));
456
- const requirements = {
457
- scheme: "exact",
458
- network: caip2,
459
- asset: opts.assetContract,
460
- amount: amountInBaseUnits,
461
- payTo: resolvePayee(opts.manifest, "x402"),
462
- maxTimeoutSeconds: 60,
463
- extra: {
464
- areFeesSponsored: true,
465
- ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
466
- }
467
- };
468
603
  return async (c, next) => {
469
604
  try {
470
- const paymentHeader = c.req.header("payment-signature") ?? c.req.header("x-payment");
471
- if (!paymentHeader) {
605
+ const endpoint = c.req.path;
606
+ const assetContract = resolveAssetContract(
607
+ opts.manifest,
608
+ "x402",
609
+ endpoint,
610
+ opts.assetContract
611
+ );
612
+ const requirements = {
613
+ scheme: "exact",
614
+ network: caip2,
615
+ asset: assetContract,
616
+ amount: amountInBaseUnits,
617
+ payTo: resolvePayee(opts.manifest, "x402"),
618
+ maxTimeoutSeconds: 60,
619
+ extra: {
620
+ areFeesSponsored: true,
621
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
622
+ }
623
+ };
624
+ const respondPaymentRequired = async (error, reason) => {
472
625
  if (ozServer) {
473
626
  const resourceInfo = {
474
627
  url: c.req.url,
@@ -479,7 +632,6 @@ function createX402HonoHandler(opts) {
479
632
  resourceInfo
480
633
  );
481
634
  c.header("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired));
482
- return c.json({ error: "Payment Required" }, 402);
483
635
  } else {
484
636
  const x402Response = {
485
637
  x402Version: 2,
@@ -487,15 +639,30 @@ function createX402HonoHandler(opts) {
487
639
  accepts: [requirements]
488
640
  };
489
641
  c.header("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response));
490
- return c.json({ error: "Payment Required" }, 402);
491
642
  }
643
+ return c.json({ error, ...reason ? { reason } : {} }, 402);
644
+ };
645
+ const paymentHeader = c.req.header("payment-signature") ?? c.req.header("x-payment");
646
+ if (!paymentHeader) {
647
+ return respondPaymentRequired("Payment Required");
492
648
  }
493
- const idempotencyKey = await paymentIdempotencyKey((name) => c.req.header(name));
649
+ const idempotencyKey = await paymentIdempotencyKey(
650
+ (name) => c.req.header(name),
651
+ {
652
+ method: c.req.method,
653
+ path: c.req.path,
654
+ amount: requirements.amount,
655
+ payTo: requirements.payTo
656
+ }
657
+ );
494
658
  if (idempotencyKey) {
495
- const cached = await seenTxStore.get(idempotencyKey);
496
- if (cached) {
497
- if (cached.headers) {
498
- for (const [k, val] of Object.entries(cached.headers)) {
659
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
660
+ if (replayCheck.kind === "spent") {
661
+ return c.json({ error: "Payment already used" }, 402);
662
+ }
663
+ if (replayCheck.kind === "replay") {
664
+ if (replayCheck.record.headers) {
665
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
499
666
  c.header(k, val);
500
667
  }
501
668
  }
@@ -515,13 +682,15 @@ function createX402HonoHandler(opts) {
515
682
  }
516
683
  if (ozServer) {
517
684
  const settleResult = await ozServer.settlePayment(payload, requirements);
518
- txHash = settleResult.transaction ?? null;
519
- if (settleResult) {
520
- paymentResponseHeader = http.encodePaymentResponseHeader(
521
- settleResult
522
- );
523
- c.header("X-Payment-Response", paymentResponseHeader);
685
+ const outcome = readSettleResult(settleResult);
686
+ if (!outcome.ok) {
687
+ return respondPaymentRequired("Payment settlement failed", outcome.reason);
524
688
  }
689
+ txHash = outcome.txHash;
690
+ paymentResponseHeader = http.encodePaymentResponseHeader(
691
+ settleResult
692
+ );
693
+ c.header("X-Payment-Response", paymentResponseHeader);
525
694
  } else {
526
695
  const verifyResult = await localFacilitator.verify(
527
696
  payload,
@@ -540,28 +709,33 @@ function createX402HonoHandler(opts) {
540
709
  payload,
541
710
  requirements
542
711
  );
543
- txHash = settleResult.transaction ?? null;
544
- if (settleResult) {
545
- paymentResponseHeader = http.encodePaymentResponseHeader(
546
- settleResult
547
- );
548
- c.header("X-Payment-Response", paymentResponseHeader);
712
+ const outcome = readSettleResult(settleResult);
713
+ if (!outcome.ok) {
714
+ return respondPaymentRequired("Payment settlement failed", outcome.reason);
549
715
  }
716
+ txHash = outcome.txHash;
717
+ paymentResponseHeader = http.encodePaymentResponseHeader(
718
+ settleResult
719
+ );
720
+ c.header("X-Payment-Response", paymentResponseHeader);
550
721
  }
551
722
  if (idempotencyKey) {
552
723
  const headers = {};
553
724
  if (paymentResponseHeader) headers["X-Payment-Response"] = paymentResponseHeader;
554
- await seenTxStore.set(idempotencyKey, { txHash, headers });
725
+ await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
555
726
  }
556
727
  if (txHash && opts.onSettled) {
557
728
  Promise.resolve().then(() => opts.onSettled(txHash, x402Price, "x402", payerAddress)).catch((err) => {
558
- console.error("[x402] onSettled callback error:", err);
729
+ logger("error", "[x402] onSettled callback error", { error: err });
559
730
  opts.onCallbackError?.(err, "onSettled");
560
731
  });
561
732
  }
562
733
  await next();
563
734
  } catch (err) {
564
- console.error("[x402] Settlement error:", err);
735
+ if (err instanceof RouteDockManifestError) {
736
+ throw err;
737
+ }
738
+ logger("error", "[x402] Settlement error", { error: err });
565
739
  return c.json({ error: "Payment settlement failed" }, 500);
566
740
  }
567
741
  };
@@ -570,20 +744,37 @@ function createMppChargeHonoHandler(opts) {
570
744
  const networkId = CAIP2[opts.network];
571
745
  const chargePrice = opts.pricing["mpp-charge"];
572
746
  const recipient = resolvePayee(opts.manifest, "mpp-charge");
573
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
574
- const mppx = server$2.Mppx.create({
575
- secretKey: opts.payeeSecretKey,
576
- methods: [
577
- server$3.stellar({
578
- recipient,
579
- currency: opts.assetContract,
580
- network: networkId,
581
- feePayer: { envelopeSigner: opts.payeeSecretKey }
582
- })
583
- ]
584
- });
747
+ const logger = resolveLogger(opts.logger);
748
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
749
+ const mppxInstances = /* @__PURE__ */ new Map();
750
+ function getMppx(contract) {
751
+ let instance = mppxInstances.get(contract);
752
+ if (!instance) {
753
+ instance = server$3.Mppx.create({
754
+ secretKey: opts.payeeSecretKey,
755
+ methods: [
756
+ server$4.stellar({
757
+ recipient,
758
+ currency: contract,
759
+ network: networkId,
760
+ feePayer: { envelopeSigner: opts.payeeSecretKey }
761
+ })
762
+ ]
763
+ });
764
+ mppxInstances.set(contract, instance);
765
+ }
766
+ return instance;
767
+ }
585
768
  return async (c, next) => {
586
769
  try {
770
+ const endpoint = c.req.path;
771
+ const assetContract = resolveAssetContract(
772
+ opts.manifest,
773
+ "mpp-charge",
774
+ endpoint,
775
+ opts.assetContract
776
+ );
777
+ const mppx = getMppx(assetContract);
587
778
  let payerAddress = null;
588
779
  try {
589
780
  const authHeader = c.req.header("authorization");
@@ -599,12 +790,23 @@ function createMppChargeHonoHandler(opts) {
599
790
  }
600
791
  } catch {
601
792
  }
602
- const idempotencyKey = await paymentIdempotencyKey((name) => c.req.header(name));
793
+ const idempotencyKey = await paymentIdempotencyKey(
794
+ (name) => c.req.header(name),
795
+ {
796
+ method: c.req.method,
797
+ path: c.req.path,
798
+ amount: chargePrice,
799
+ payTo: recipient
800
+ }
801
+ );
603
802
  if (idempotencyKey) {
604
- const cached = await seenTxStore.get(idempotencyKey);
605
- if (cached) {
606
- if (cached.headers) {
607
- for (const [k, val] of Object.entries(cached.headers)) {
803
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
804
+ if (replayCheck.kind === "spent") {
805
+ return c.json({ error: "Payment already used" }, 402);
806
+ }
807
+ if (replayCheck.kind === "replay") {
808
+ if (replayCheck.record.headers) {
809
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
608
810
  c.header(k, val);
609
811
  }
610
812
  }
@@ -615,7 +817,7 @@ function createMppChargeHonoHandler(opts) {
615
817
  const handler = mppx["stellar/charge"];
616
818
  const result = await handler({
617
819
  amount: chargePrice,
618
- currency: opts.assetContract,
820
+ currency: assetContract,
619
821
  recipient,
620
822
  description: opts.manifest.name
621
823
  })(c.req.raw);
@@ -645,12 +847,13 @@ function createMppChargeHonoHandler(opts) {
645
847
  if (idempotencyKey) {
646
848
  await seenTxStore.set(idempotencyKey, {
647
849
  txHash: reference ?? null,
648
- headers: receiptHeaders
850
+ headers: receiptHeaders,
851
+ createdAt: Date.now()
649
852
  });
650
853
  }
651
854
  if (reference && opts.onSettled) {
652
855
  Promise.resolve().then(() => opts.onSettled(reference, chargePrice, "mpp-charge", payerAddress)).catch((err) => {
653
- console.error("[mpp-charge] onSettled callback error:", err);
856
+ logger("error", "[mpp-charge] onSettled callback error", { error: err });
654
857
  opts.onCallbackError?.(err, "onSettled");
655
858
  });
656
859
  }
@@ -667,8 +870,9 @@ function mppSessionWsVerified(c) {
667
870
  function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
668
871
  const networkId = CAIP2[opts.network];
669
872
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
873
+ const logger = resolveLogger(opts.logger);
670
874
  const voucherRecordKey = `routedock:session:voucher:${sessionPricing.channelFactory}`;
671
- const innerStore = opts.sessionStore ?? server$4.Store.memory();
875
+ const innerStore = opts.sessionStore ?? server$2.Store.memory();
672
876
  let record = null;
673
877
  let voucherCount = 0;
674
878
  let sessionOpened = false;
@@ -719,7 +923,7 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
719
923
  reason
720
924
  });
721
925
  } catch (err) {
722
- console.error("[mpp-session] onOrphaned handler failed:", err);
926
+ logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
723
927
  }
724
928
  }
725
929
  }
@@ -766,7 +970,7 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
766
970
  sessionOpened = true;
767
971
  if (opts.onSessionOpen) {
768
972
  Promise.resolve().then(() => opts.onSessionOpen(sessionPricing.channelFactory, record.payer)).catch((err) => {
769
- console.error("[mpp-session] onSessionOpen callback error:", err);
973
+ logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
770
974
  opts.onCallbackError?.(err, "onSessionOpen");
771
975
  });
772
976
  }
@@ -774,17 +978,17 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
774
978
  if (opts.onVoucher) {
775
979
  const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
776
980
  Promise.resolve().then(() => opts.onVoucher(sessionPricing.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
777
- console.error("[mpp-session] onVoucher callback error:", err);
981
+ logger("error", "[mpp-session] onVoucher callback error", { error: err });
778
982
  opts.onCallbackError?.(err, "onVoucher");
779
983
  });
780
984
  }
781
985
  }
782
- const mppx = server$2.Mppx.create({
986
+ const mppx = server$3.Mppx.create({
783
987
  secretKey: opts.payeeSecretKey,
784
988
  methods: [
785
989
  withTypedChannelErrors(
786
990
  onVerifiedCredential(
787
- server$4.stellar({
991
+ server$2.stellar({
788
992
  channel: sessionPricing.channelFactory,
789
993
  commitmentKey: opts.commitmentPublicKey,
790
994
  network: networkId,
@@ -818,7 +1022,7 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
818
1022
  closeSig = record?.signature || (bodyAmount === recordAmount ? body?.signature ?? "" : "");
819
1023
  }
820
1024
  if (closeAmount > 0n && closeSig) {
821
- const closeTxHash = await server$4.close({
1025
+ const closeTxHash = await server$2.close({
822
1026
  channel: sessionPricing.channelFactory,
823
1027
  amount: closeAmount,
824
1028
  signature: hexToBytes(closeSig),
@@ -831,7 +1035,7 @@ function createMppSessionHandlerState(opts, sessionPricing, reportMode) {
831
1035
  if (opts.onSettled) {
832
1036
  const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
833
1037
  Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, reportMode, closePayer)).catch((err) => {
834
- console.error(`[mpp-session] onSettled callback error:`, err);
1038
+ logger("error", "[mpp-session] onSettled callback error", { error: err });
835
1039
  opts.onCallbackError?.(err, "onSettled");
836
1040
  });
837
1041
  }
@@ -1,10 +1,10 @@
1
1
  import { MiddlewareHandler } from 'hono';
2
2
  import { Store } from '@stellar/mpp/channel/server';
3
- import { P as PaymentMode, R as RouteDockManifest } from '../types-DmhmfLzP.cjs';
4
- import { O as OrphanedSessionInfo, S as SeenTxStore } from '../MppSessionHandler-B4TyD7Xg.cjs';
5
- export { I as InMemorySeenTxStore, a as SettlementRecord } from '../MppSessionHandler-B4TyD7Xg.cjs';
6
- export { R as ReconciliationStats, a as RegisterProviderOptions, S as SessionReconcilerOptions, b as SupabaseSeenTxStore, r as reconcileAbandonedSessions, c as registerProvider, e as runStartupReconciliation } from '../SessionReconciler-bi0z2qtN.cjs';
7
- export { s as signManifest } from '../sign-CUf3rhTx.cjs';
3
+ import { P as PaymentMode, h as RouteDockManifest, g as RouteDockLogger } from '../logger-D8_Q2ytp.cjs';
4
+ import { O as OrphanedSessionInfo, S as SeenTxStore } from '../MppSessionHandler-1EL8QBAn.cjs';
5
+ export { I as InMemorySeenTxStore, a as SettlementRecord } from '../MppSessionHandler-1EL8QBAn.cjs';
6
+ export { R as ReconciliationStats, a as RegisterProviderOptions, S as SessionReconcilerOptions, b as SupabaseSeenTxStore, r as reconcileAbandonedSessions, c as registerProvider, e as runStartupReconciliation } from '../SessionReconciler-Cz2nuOlJ.cjs';
7
+ export { s as signManifest } from '../sign-AKH67sft.cjs';
8
8
  import 'mppx';
9
9
  import 'express';
10
10
  import '@supabase/supabase-js';
@@ -26,8 +26,8 @@ interface RouteDockHonoOptions {
26
26
  channelFactory: string;
27
27
  };
28
28
  };
29
- asset: string;
30
- assetContract: string;
29
+ asset?: string;
30
+ assetContract?: string;
31
31
  payee: string;
32
32
  network: Network;
33
33
  payeeSecretKey: string;
@@ -56,6 +56,8 @@ interface RouteDockHonoOptions {
56
56
  * Durable Object storage) so voucher tracking survives isolate eviction.
57
57
  */
58
58
  sessionStore?: Store.Store;
59
+ /** Log sink for internal error paths. Defaults to a console-backed logger. */
60
+ logger?: RouteDockLogger;
59
61
  }
60
62
  /**
61
63
  * Context key set by the mpp-session-ws middleware after a request's payment
@@ -1,10 +1,10 @@
1
1
  import { MiddlewareHandler } from 'hono';
2
2
  import { Store } from '@stellar/mpp/channel/server';
3
- import { P as PaymentMode, R as RouteDockManifest } from '../types-DmhmfLzP.js';
4
- import { O as OrphanedSessionInfo, S as SeenTxStore } from '../MppSessionHandler-CLB5SzgN.js';
5
- export { I as InMemorySeenTxStore, a as SettlementRecord } from '../MppSessionHandler-CLB5SzgN.js';
6
- export { R as ReconciliationStats, a as RegisterProviderOptions, S as SessionReconcilerOptions, b as SupabaseSeenTxStore, r as reconcileAbandonedSessions, c as registerProvider, e as runStartupReconciliation } from '../SessionReconciler-Dscw43F3.js';
7
- export { s as signManifest } from '../sign-Dpj2Mp7k.js';
3
+ import { P as PaymentMode, h as RouteDockManifest, g as RouteDockLogger } from '../logger-D8_Q2ytp.js';
4
+ import { O as OrphanedSessionInfo, S as SeenTxStore } from '../MppSessionHandler-Bq4S-JTE.js';
5
+ export { I as InMemorySeenTxStore, a as SettlementRecord } from '../MppSessionHandler-Bq4S-JTE.js';
6
+ export { R as ReconciliationStats, a as RegisterProviderOptions, S as SessionReconcilerOptions, b as SupabaseSeenTxStore, r as reconcileAbandonedSessions, c as registerProvider, e as runStartupReconciliation } from '../SessionReconciler-Bw-PoBVl.js';
7
+ export { s as signManifest } from '../sign-BEq-OsSq.js';
8
8
  import 'mppx';
9
9
  import 'express';
10
10
  import '@supabase/supabase-js';
@@ -26,8 +26,8 @@ interface RouteDockHonoOptions {
26
26
  channelFactory: string;
27
27
  };
28
28
  };
29
- asset: string;
30
- assetContract: string;
29
+ asset?: string;
30
+ assetContract?: string;
31
31
  payee: string;
32
32
  network: Network;
33
33
  payeeSecretKey: string;
@@ -56,6 +56,8 @@ interface RouteDockHonoOptions {
56
56
  * Durable Object storage) so voucher tracking survives isolate eviction.
57
57
  */
58
58
  sessionStore?: Store.Store;
59
+ /** Log sink for internal error paths. Defaults to a console-backed logger. */
60
+ logger?: RouteDockLogger;
59
61
  }
60
62
  /**
61
63
  * Context key set by the mpp-session-ws middleware after a request's payment