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