@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
@@ -7,15 +7,37 @@ 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
 
16
16
  // src/provider/x402Handler.ts
17
17
 
18
- // src/provider/payee.ts
18
+ // src/errors.ts
19
+ var RouteDockError = class extends Error {
20
+ code;
21
+ retryable;
22
+ retryAfterMs;
23
+ constructor(message, code, retryable, options = {}) {
24
+ super(message, { cause: options.cause });
25
+ this.name = "RouteDockError";
26
+ this.code = code;
27
+ this.retryable = retryable;
28
+ if (options.retryAfterMs !== void 0) {
29
+ this.retryAfterMs = options.retryAfterMs;
30
+ }
31
+ }
32
+ };
33
+ var RouteDockManifestError = class extends RouteDockError {
34
+ constructor(message, options = {}) {
35
+ super(message, "MANIFEST", false, options);
36
+ this.name = "RouteDockManifestError";
37
+ }
38
+ };
39
+
40
+ // src/internal/payee.ts
19
41
  function resolvePayee(manifest, mode) {
20
42
  const override = manifest.pricing[mode]?.payee;
21
43
  return override ?? manifest.payee;
@@ -26,9 +48,113 @@ function extractPayerAddress(key) {
26
48
  return stellarSdk.StrKey.isValidEd25519PublicKey(key) || stellarSdk.StrKey.isValidMed25519PublicKey(key) ? key : null;
27
49
  }
28
50
 
51
+ // src/internal/assetUtils.ts
52
+ function normalizeManifestAssets(manifest) {
53
+ if (manifest.assets && manifest.assets.length > 0) {
54
+ return manifest.assets;
55
+ }
56
+ if (manifest.asset && manifest.asset_contract) {
57
+ return [
58
+ {
59
+ asset: manifest.asset,
60
+ asset_contract: manifest.asset_contract
61
+ }
62
+ ];
63
+ }
64
+ throw new RouteDockManifestError(
65
+ "Manifest must define either assets[] or root asset/asset_contract fields"
66
+ );
67
+ }
68
+ function getEligibleAssets(manifest, mode, endpoint) {
69
+ const assets = normalizeManifestAssets(manifest);
70
+ return assets.filter((asset) => {
71
+ if (asset.modes && !asset.modes.includes(mode)) {
72
+ return false;
73
+ }
74
+ if (endpoint && asset.endpoints && asset.endpoints.length > 0) {
75
+ const trimmedEndpoint = endpoint.replace(/^\//, "");
76
+ if (asset.endpoints.includes(endpoint) || asset.endpoints.includes(trimmedEndpoint)) {
77
+ return true;
78
+ }
79
+ if (manifest.endpoints) {
80
+ let pathname = endpoint;
81
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
82
+ try {
83
+ pathname = new URL(endpoint).pathname;
84
+ } catch {
85
+ }
86
+ }
87
+ const trimmedPathname = pathname.replace(/^\//, "");
88
+ const matchesEndpoint = Object.entries(manifest.endpoints).some(([key, desc]) => {
89
+ const pathMatches = desc.path === pathname || desc.path === endpoint || desc.path.replace(/\/+$/, "") === pathname.replace(/\/+$/, "") || desc.path.replace(/^\//, "") === trimmedPathname;
90
+ const keyMatches = key === endpoint || key === trimmedEndpoint;
91
+ if (pathMatches || keyMatches) {
92
+ return asset.endpoints.includes(key) || asset.endpoints.includes(desc.path) || asset.endpoints.includes(desc.path.replace(/^\//, ""));
93
+ }
94
+ return false;
95
+ });
96
+ if (!matchesEndpoint) {
97
+ return false;
98
+ }
99
+ } else {
100
+ return false;
101
+ }
102
+ }
103
+ return true;
104
+ });
105
+ }
106
+ function resolveAssetContract(manifest, mode, endpoint, explicitContract) {
107
+ if (manifest.assets && manifest.assets.length > 0) {
108
+ return selectAsset(manifest, mode, endpoint).asset_contract;
109
+ }
110
+ return explicitContract ?? selectAsset(manifest, mode, endpoint).asset_contract;
111
+ }
112
+ function selectAsset(manifest, mode, endpoint) {
113
+ const eligible = getEligibleAssets(manifest, mode, endpoint);
114
+ if (eligible.length === 0) {
115
+ const endpointMsg = endpoint ? ` for endpoint '${endpoint}'` : "";
116
+ throw new RouteDockManifestError(
117
+ `No eligible assets found for mode '${mode}'${endpointMsg}`
118
+ );
119
+ }
120
+ return eligible[0];
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
+ }
138
+
29
139
  // src/provider/SeenTxStore.ts
140
+ var SETTLEMENT_REPLAY_WINDOW_MS = 6e4;
141
+ var MAX_SETTLEMENT_REPLAYS = 1;
142
+ async function checkSettlementReplay(store, key, now = Date.now()) {
143
+ const record = await store.get(key);
144
+ if (!record) return { kind: "miss" };
145
+ const { createdAt } = record;
146
+ if (createdAt !== void 0 && now - createdAt > SETTLEMENT_REPLAY_WINDOW_MS) {
147
+ return { kind: "spent" };
148
+ }
149
+ if (store.claimReplay) {
150
+ const replayed = await store.claimReplay(key);
151
+ if (replayed > MAX_SETTLEMENT_REPLAYS) return { kind: "spent" };
152
+ }
153
+ return { kind: "replay", record };
154
+ }
30
155
  var InMemorySeenTxStore = class {
31
156
  map = /* @__PURE__ */ new Map();
157
+ replayCounts = /* @__PURE__ */ new Map();
32
158
  order = [];
33
159
  constructor(maxEntriesOrOptions = 1e4) {
34
160
  const options = typeof maxEntriesOrOptions === "number" ? { maxEntries: maxEntriesOrOptions } : maxEntriesOrOptions;
@@ -36,7 +162,8 @@ var InMemorySeenTxStore = class {
36
162
  if (options.warn !== false) {
37
163
  const isServerless = typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers";
38
164
  if (isServerless) {
39
- console.warn(
165
+ resolveLogger(options.logger)(
166
+ "warn",
40
167
  "[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."
41
168
  );
42
169
  }
@@ -51,21 +178,43 @@ var InMemorySeenTxStore = class {
51
178
  this.order.push(key);
52
179
  if (this.order.length > this.maxEntries) {
53
180
  const evicted = this.order.shift();
54
- if (evicted !== void 0) this.map.delete(evicted);
181
+ if (evicted !== void 0) {
182
+ this.map.delete(evicted);
183
+ this.replayCounts.delete(evicted);
184
+ }
55
185
  }
56
186
  }
57
187
  this.map.set(key, record);
58
188
  }
189
+ claimReplay(key) {
190
+ if (!this.map.has(key)) return Number.POSITIVE_INFINITY;
191
+ const next = (this.replayCounts.get(key) ?? 0) + 1;
192
+ this.replayCounts.set(key, next);
193
+ return next;
194
+ }
59
195
  };
60
196
  async function sha256Hex(input) {
61
197
  const buf = new TextEncoder().encode(input);
62
198
  const digest = await crypto.subtle.digest("SHA-256", buf);
63
199
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
64
200
  }
65
- async function paymentIdempotencyKey(getHeader) {
201
+ async function paymentIdempotencyKey(getHeader, scope) {
66
202
  const material = getHeader("payment-signature") ?? getHeader("x-payment") ?? getHeader("authorization");
67
203
  if (!material) return null;
68
- return sha256Hex(material);
204
+ return sha256Hex(
205
+ [scope.method, scope.path, scope.amount, scope.payTo, material].join("\n")
206
+ );
207
+ }
208
+
209
+ // src/provider/settleResult.ts
210
+ function readSettleResult(result) {
211
+ const settle = result;
212
+ const failed = (reason) => reason ? { ok: false, reason } : { ok: false };
213
+ if (!settle || settle.success !== true) return failed(settle?.errorReason);
214
+ if (typeof settle.transaction !== "string" || settle.transaction.length === 0) {
215
+ return failed(settle.errorReason);
216
+ }
217
+ return { ok: true, txHash: settle.transaction };
69
218
  }
70
219
 
71
220
  // src/provider/x402Handler.ts
@@ -78,7 +227,8 @@ function createX402Handler(opts) {
78
227
  const caip2 = CAIP2[opts.network];
79
228
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
80
229
  const signer = stellar.createEd25519Signer(opts.payeeSecretKey, caip2);
81
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
230
+ const logger = resolveLogger(opts.logger);
231
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
82
232
  const useOzFacilitator = opts.network === "mainnet" && opts.facilitatorApiKey;
83
233
  const localFacilitator = new facilitator.ExactStellarScheme([signer], {
84
234
  areFeesSponsored: true
@@ -99,22 +249,29 @@ function createX402Handler(opts) {
99
249
  }
100
250
  const amountInBaseUnits = String(usdcToUnits(opts.amount));
101
251
  const payTo = resolvePayee(opts.manifest, "x402");
102
- const requirements = {
103
- scheme: "exact",
104
- network: caip2,
105
- asset: opts.assetContract,
106
- amount: amountInBaseUnits,
107
- payTo,
108
- maxTimeoutSeconds: 60,
109
- extra: {
110
- areFeesSponsored: true,
111
- ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
112
- }
113
- };
114
252
  return async (req, res, next) => {
115
253
  try {
116
- const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
117
- if (!paymentHeader) {
254
+ const endpoint = req.path || req.originalUrl;
255
+ const assetContract = resolveAssetContract(
256
+ opts.manifest,
257
+ "x402",
258
+ endpoint,
259
+ opts.assetContract
260
+ );
261
+ const requirements = {
262
+ scheme: "exact",
263
+ network: caip2,
264
+ asset: assetContract,
265
+ amount: amountInBaseUnits,
266
+ payTo,
267
+ maxTimeoutSeconds: 60,
268
+ extra: {
269
+ areFeesSponsored: true,
270
+ ...useOzFacilitator ? {} : { facilitatorAddresses: [payeeKeypair.publicKey()] }
271
+ }
272
+ };
273
+ const respondPaymentRequired = async (error, reason) => {
274
+ const body = { error, ...reason ? { reason } : {} };
118
275
  if (ozServer) {
119
276
  const resourceInfo = {
120
277
  url: `${req.protocol}://${req.get("host") ?? ""}${req.originalUrl}`,
@@ -124,7 +281,7 @@ function createX402Handler(opts) {
124
281
  [requirements],
125
282
  resourceInfo
126
283
  );
127
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json({ error: "Payment Required" });
284
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(paymentRequired)).json(body);
128
285
  } else {
129
286
  const x402Response = {
130
287
  x402Version: 2,
@@ -134,19 +291,35 @@ function createX402Handler(opts) {
134
291
  },
135
292
  accepts: [requirements]
136
293
  };
137
- res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json({ error: "Payment Required" });
294
+ res.status(402).setHeader("Content-Type", "application/json").setHeader("X-Payment-Requirements", http.encodePaymentRequiredHeader(x402Response)).json(body);
138
295
  }
296
+ };
297
+ const paymentHeader = req.headers["payment-signature"] ?? req.headers["x-payment"];
298
+ if (!paymentHeader) {
299
+ await respondPaymentRequired("Payment Required");
139
300
  return;
140
301
  }
141
- const idempotencyKey = await paymentIdempotencyKey((name) => {
142
- const v = req.headers[name.toLowerCase()];
143
- return Array.isArray(v) ? v[0] : v;
144
- });
302
+ const idempotencyKey = await paymentIdempotencyKey(
303
+ (name) => {
304
+ const v = req.headers[name.toLowerCase()];
305
+ return Array.isArray(v) ? v[0] : v;
306
+ },
307
+ {
308
+ method: req.method,
309
+ path: req.originalUrl.split("?")[0],
310
+ amount: requirements.amount,
311
+ payTo: requirements.payTo
312
+ }
313
+ );
145
314
  if (idempotencyKey) {
146
- const cached = await seenTxStore.get(idempotencyKey);
147
- if (cached) {
148
- if (cached.headers) {
149
- for (const [k, val] of Object.entries(cached.headers)) {
315
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
316
+ if (replayCheck.kind === "spent") {
317
+ res.status(402).json({ error: "Payment already used" });
318
+ return;
319
+ }
320
+ if (replayCheck.kind === "replay") {
321
+ if (replayCheck.record.headers) {
322
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
150
323
  res.setHeader(k, val);
151
324
  }
152
325
  }
@@ -165,15 +338,18 @@ function createX402Handler(opts) {
165
338
  }
166
339
  if (ozServer) {
167
340
  const settleResult = await ozServer.settlePayment(payload, requirements);
168
- txHash = settleResult.transaction ?? null;
169
- if (settleResult) {
170
- res.setHeader(
171
- "X-Payment-Response",
172
- http.encodePaymentResponseHeader(
173
- settleResult
174
- )
175
- );
341
+ const outcome = readSettleResult(settleResult);
342
+ if (!outcome.ok) {
343
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
344
+ return;
176
345
  }
346
+ txHash = outcome.txHash;
347
+ res.setHeader(
348
+ "X-Payment-Response",
349
+ http.encodePaymentResponseHeader(
350
+ settleResult
351
+ )
352
+ );
177
353
  } else {
178
354
  const verifyResult = await localFacilitator.verify(
179
355
  payload,
@@ -190,15 +366,18 @@ function createX402Handler(opts) {
190
366
  payload,
191
367
  requirements
192
368
  );
193
- txHash = settleResult.transaction ?? null;
194
- if (settleResult) {
195
- res.setHeader(
196
- "X-Payment-Response",
197
- http.encodePaymentResponseHeader(
198
- settleResult
199
- )
200
- );
369
+ const outcome = readSettleResult(settleResult);
370
+ if (!outcome.ok) {
371
+ await respondPaymentRequired("Payment settlement failed", outcome.reason);
372
+ return;
201
373
  }
374
+ txHash = outcome.txHash;
375
+ res.setHeader(
376
+ "X-Payment-Response",
377
+ http.encodePaymentResponseHeader(
378
+ settleResult
379
+ )
380
+ );
202
381
  }
203
382
  if (idempotencyKey) {
204
383
  const headers = {};
@@ -206,17 +385,21 @@ function createX402Handler(opts) {
206
385
  if (typeof paymentResponse === "string") {
207
386
  headers["X-Payment-Response"] = paymentResponse;
208
387
  }
209
- await seenTxStore.set(idempotencyKey, { txHash, headers });
388
+ await seenTxStore.set(idempotencyKey, { txHash, headers, createdAt: Date.now() });
210
389
  }
211
390
  if (txHash && opts.onSettled) {
212
391
  Promise.resolve().then(() => opts.onSettled(txHash, opts.amount, "x402", payerAddress)).catch((err) => {
213
- console.error("[x402] onSettled callback error:", err);
392
+ logger("error", "[x402] onSettled callback error", { error: err });
214
393
  opts.onCallbackError?.(err, "onSettled");
215
394
  });
216
395
  }
217
396
  next();
218
397
  } catch (err) {
219
- console.error("[x402] Settlement error:", err);
398
+ if (err instanceof RouteDockManifestError) {
399
+ next(err);
400
+ return;
401
+ }
402
+ logger("error", "[x402] Settlement error", { error: err });
220
403
  res.status(500).json({ error: "Payment settlement failed" });
221
404
  }
222
405
  };
@@ -229,20 +412,37 @@ function createMppChargeHandler(opts) {
229
412
  const networkId = MPP_NETWORK[opts.network];
230
413
  const amountHumanReadable = opts.amount;
231
414
  const recipient = resolvePayee(opts.manifest, "mpp-charge");
232
- const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore();
233
- const mppx = server$2.Mppx.create({
234
- secretKey: opts.payeeSecretKey,
235
- methods: [
236
- server$3.stellar.charge({
237
- recipient,
238
- currency: opts.assetContract,
239
- network: networkId,
240
- feePayer: { envelopeSigner: opts.payeeSecretKey }
241
- })
242
- ]
243
- });
415
+ const logger = resolveLogger(opts.logger);
416
+ const seenTxStore = opts.seenTxStore ?? new InMemorySeenTxStore({ logger });
417
+ const mppxInstances = /* @__PURE__ */ new Map();
418
+ function getMppx(contract) {
419
+ let instance = mppxInstances.get(contract);
420
+ if (!instance) {
421
+ instance = server$2.Mppx.create({
422
+ secretKey: opts.payeeSecretKey,
423
+ methods: [
424
+ server$4.stellar.charge({
425
+ recipient,
426
+ currency: contract,
427
+ network: networkId,
428
+ feePayer: { envelopeSigner: opts.payeeSecretKey }
429
+ })
430
+ ]
431
+ });
432
+ mppxInstances.set(contract, instance);
433
+ }
434
+ return instance;
435
+ }
244
436
  return async (req, res, next) => {
245
437
  try {
438
+ const endpoint = req.path || req.originalUrl;
439
+ const assetContract = resolveAssetContract(
440
+ opts.manifest,
441
+ "mpp-charge",
442
+ endpoint,
443
+ opts.assetContract
444
+ );
445
+ const mppx = getMppx(assetContract);
246
446
  let payerAddress = null;
247
447
  try {
248
448
  const authHeader = req.headers["authorization"];
@@ -258,15 +458,27 @@ function createMppChargeHandler(opts) {
258
458
  }
259
459
  } catch {
260
460
  }
261
- const idempotencyKey = await paymentIdempotencyKey((name) => {
262
- const v = req.headers[name.toLowerCase()];
263
- return Array.isArray(v) ? v[0] : v;
264
- });
461
+ const idempotencyKey = await paymentIdempotencyKey(
462
+ (name) => {
463
+ const v = req.headers[name.toLowerCase()];
464
+ return Array.isArray(v) ? v[0] : v;
465
+ },
466
+ {
467
+ method: req.method,
468
+ path: req.originalUrl.split("?")[0],
469
+ amount: amountHumanReadable,
470
+ payTo: recipient
471
+ }
472
+ );
265
473
  if (idempotencyKey) {
266
- const cached = await seenTxStore.get(idempotencyKey);
267
- if (cached) {
268
- if (cached.headers) {
269
- for (const [k, val] of Object.entries(cached.headers)) {
474
+ const replayCheck = await checkSettlementReplay(seenTxStore, idempotencyKey);
475
+ if (replayCheck.kind === "spent") {
476
+ res.status(402).json({ error: "Payment already used" });
477
+ return;
478
+ }
479
+ if (replayCheck.kind === "replay") {
480
+ if (replayCheck.record.headers) {
481
+ for (const [k, val] of Object.entries(replayCheck.record.headers)) {
270
482
  res.setHeader(k, val);
271
483
  }
272
484
  }
@@ -278,7 +490,7 @@ function createMppChargeHandler(opts) {
278
490
  const handler = mppx["stellar/charge"];
279
491
  const result = await handler({
280
492
  amount: amountHumanReadable,
281
- currency: opts.assetContract,
493
+ currency: assetContract,
282
494
  recipient,
283
495
  description: opts.manifest.name
284
496
  })(fetchReq);
@@ -309,12 +521,13 @@ function createMppChargeHandler(opts) {
309
521
  if (idempotencyKey) {
310
522
  await seenTxStore.set(idempotencyKey, {
311
523
  txHash: reference ?? null,
312
- headers: receiptHeaders
524
+ headers: receiptHeaders,
525
+ createdAt: Date.now()
313
526
  });
314
527
  }
315
528
  if (reference && opts.onSettled) {
316
529
  Promise.resolve().then(() => opts.onSettled(reference, opts.amount, "mpp-charge", payerAddress)).catch((err) => {
317
- console.error("[mpp-charge] onSettled callback error:", err);
530
+ logger("error", "[mpp-charge] onSettled callback error", { error: err });
318
531
  opts.onCallbackError?.(err, "onSettled");
319
532
  });
320
533
  }
@@ -415,8 +628,9 @@ function createMppSessionHandler(opts) {
415
628
  const networkId = MPP_NETWORK2[opts.network];
416
629
  const rateHuman = opts.rate;
417
630
  const payeeKeypair = stellarSdk.Keypair.fromSecret(opts.payeeSecretKey);
631
+ const logger = resolveLogger(opts.logger);
418
632
  const voucherRecordKey = `routedock:session:voucher:${opts.channelFactory}`;
419
- const innerStore = server$4.Store.memory();
633
+ const innerStore = server$3.Store.memory();
420
634
  let record = null;
421
635
  let voucherCount = 0;
422
636
  let sessionOpened = false;
@@ -466,7 +680,7 @@ function createMppSessionHandler(opts) {
466
680
  reason
467
681
  });
468
682
  } catch (err) {
469
- console.error("[mpp-session] onOrphaned handler failed:", err);
683
+ logger("error", "[mpp-session] onOrphaned handler failed", { error: err });
470
684
  }
471
685
  }
472
686
  }
@@ -513,7 +727,7 @@ function createMppSessionHandler(opts) {
513
727
  sessionOpened = true;
514
728
  if (opts.onSessionOpen) {
515
729
  Promise.resolve().then(() => opts.onSessionOpen(opts.channelFactory, record.payer)).catch((err) => {
516
- console.error("[mpp-session] onSessionOpen callback error:", err);
730
+ logger("error", "[mpp-session] onSessionOpen callback error", { error: err });
517
731
  opts.onCallbackError?.(err, "onSessionOpen");
518
732
  });
519
733
  }
@@ -521,7 +735,7 @@ function createMppSessionHandler(opts) {
521
735
  if (opts.onVoucher) {
522
736
  const humanAmount = (Number(record.amount) / 1e7).toFixed(7);
523
737
  Promise.resolve().then(() => opts.onVoucher(opts.channelFactory, voucherCount, humanAmount, record.signature)).catch((err) => {
524
- console.error("[mpp-session] onVoucher callback error:", err);
738
+ logger("error", "[mpp-session] onVoucher callback error", { error: err });
525
739
  opts.onCallbackError?.(err, "onVoucher");
526
740
  });
527
741
  }
@@ -531,7 +745,7 @@ function createMppSessionHandler(opts) {
531
745
  methods: [
532
746
  withTypedChannelErrors(
533
747
  onVerifiedCredential(
534
- server$4.stellar.channel({
748
+ server$3.stellar.channel({
535
749
  channel: opts.channelFactory,
536
750
  commitmentKey: opts.commitmentPublicKey,
537
751
  network: networkId,
@@ -562,7 +776,7 @@ function createMppSessionHandler(opts) {
562
776
  }
563
777
  if (closeAmount > 0n && closeSig) {
564
778
  const closePayer = record?.payer ?? null;
565
- const closeTxHash = await server$4.close({
779
+ const closeTxHash = await server$3.close({
566
780
  channel: opts.channelFactory,
567
781
  amount: closeAmount,
568
782
  signature: Buffer.from(closeSig, "hex"),
@@ -574,7 +788,7 @@ function createMppSessionHandler(opts) {
574
788
  if (opts.onSettled) {
575
789
  const totalPaid = (Number(closeAmount) / 1e7).toFixed(7);
576
790
  Promise.resolve().then(() => opts.onSettled(closeTxHash, totalPaid, "mpp-session", closePayer)).catch((err) => {
577
- console.error("[mpp-session] onSettled callback error:", err);
791
+ logger("error", "[mpp-session] onSettled callback error", { error: err });
578
792
  opts.onCallbackError?.(err, "onSettled");
579
793
  });
580
794
  }
@@ -585,7 +799,7 @@ function createMppSessionHandler(opts) {
585
799
  payeeKeypair.publicKey()
586
800
  );
587
801
  if (!settlementAddresses) {
588
- console.error("[mpp-session] skipped session_settled vault record: payer address unavailable");
802
+ logger("warn", "[mpp-session] skipped session_settled vault record: payer address unavailable");
589
803
  } else {
590
804
  try {
591
805
  const { Networks } = await import('@stellar/stellar-sdk');
@@ -608,7 +822,7 @@ function createMppSessionHandler(opts) {
608
822
  preparedTx.sign(payeeKeypair);
609
823
  await server.sendTransaction(preparedTx);
610
824
  } catch (recordErr) {
611
- console.error("[mpp-session] failed to record session_settled on vault:", recordErr);
825
+ logger("error", "[mpp-session] failed to record session_settled on vault", { error: recordErr });
612
826
  }
613
827
  }
614
828
  }
@@ -801,12 +1015,13 @@ function routedockFastify(opts) {
801
1015
  payeeSecretKey: opts.payeeSecretKey,
802
1016
  network: opts.network,
803
1017
  amount: x402Price,
804
- assetContract: opts.assetContract,
1018
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
805
1019
  ...opts.facilitatorApiKey ? { facilitatorApiKey: opts.facilitatorApiKey } : {},
806
1020
  manifest: signedManifest,
807
1021
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
808
1022
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
809
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
1023
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
1024
+ ...opts.logger ? { logger: opts.logger } : {}
810
1025
  })
811
1026
  );
812
1027
  }
@@ -820,11 +1035,12 @@ function routedockFastify(opts) {
820
1035
  payeeSecretKey: opts.payeeSecretKey,
821
1036
  network: opts.network,
822
1037
  amount: chargePrice,
823
- assetContract: opts.assetContract,
1038
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
824
1039
  manifest: signedManifest,
825
1040
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
826
1041
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
827
- ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {}
1042
+ ...opts.seenTxStore ? { seenTxStore: opts.seenTxStore } : {},
1043
+ ...opts.logger ? { logger: opts.logger } : {}
828
1044
  })
829
1045
  );
830
1046
  }
@@ -842,7 +1058,7 @@ function routedockFastify(opts) {
842
1058
  network: opts.network,
843
1059
  channelFactory: sessionPricing.channelFactory,
844
1060
  rate: sessionPricing.rate,
845
- assetContract: opts.assetContract,
1061
+ ...opts.assetContract ? { assetContract: opts.assetContract } : {},
846
1062
  manifest: signedManifest,
847
1063
  commitmentPublicKey: opts.commitmentPublicKey,
848
1064
  ...opts.onSettled ? { onSettled: opts.onSettled } : {},
@@ -850,7 +1066,8 @@ function routedockFastify(opts) {
850
1066
  ...opts.onVoucher ? { onVoucher: opts.onVoucher } : {},
851
1067
  ...opts.onCallbackError ? { onCallbackError: opts.onCallbackError } : {},
852
1068
  ...opts.onOrphaned ? { onOrphaned: opts.onOrphaned } : {},
853
- ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {}
1069
+ ...opts.idleTimeoutMs != null ? { idleTimeoutMs: opts.idleTimeoutMs } : {},
1070
+ ...opts.logger ? { logger: opts.logger } : {}
854
1071
  })
855
1072
  );
856
1073
  }
@@ -1,6 +1,6 @@
1
1
  import { FastifyPluginAsync } from 'fastify';
2
- import { P as PaymentMode, R as RouteDockManifest } from '../types-DmhmfLzP.cjs';
3
- import { O as OrphanedSessionInfo, S as SeenTxStore } from '../MppSessionHandler-B4TyD7Xg.cjs';
2
+ import { P as PaymentMode, h as RouteDockManifest, g as RouteDockLogger } from '../logger-D8_Q2ytp.cjs';
3
+ import { O as OrphanedSessionInfo, S as SeenTxStore } from '../MppSessionHandler-1EL8QBAn.cjs';
4
4
  import 'mppx';
5
5
  import 'express';
6
6
 
@@ -14,8 +14,8 @@ interface RouteDockFastifyOptions {
14
14
  channelFactory: string;
15
15
  };
16
16
  };
17
- asset: string;
18
- assetContract: string;
17
+ asset?: string;
18
+ assetContract?: string;
19
19
  payee: string;
20
20
  network: 'testnet' | 'mainnet';
21
21
  payeeSecretKey: string;
@@ -29,6 +29,11 @@ interface RouteDockFastifyOptions {
29
29
  onOrphaned?: (channelId: string, info: OrphanedSessionInfo) => Promise<void>;
30
30
  idleTimeoutMs?: number;
31
31
  seenTxStore?: SeenTxStore;
32
+ /**
33
+ * Structured log sink for every adapter diagnostic (settlement errors,
34
+ * callback failures, orphaned sessions). Defaults to a console-backed logger.
35
+ */
36
+ logger?: RouteDockLogger;
32
37
  }
33
38
  declare function routedockFastify(opts: RouteDockFastifyOptions): FastifyPluginAsync;
34
39