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