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