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