@agentcash/router 1.5.2 → 1.6.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/.claude/CLAUDE.md +116 -2
- package/README.md +34 -5
- package/dist/index.cjs +2173 -1079
- package/dist/index.d.cts +391 -276
- package/dist/index.d.ts +391 -276
- package/dist/index.js +2169 -1077
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -83,6 +83,18 @@ function buildEvmExactOptions(accepts, price) {
|
|
|
83
83
|
payTo
|
|
84
84
|
}));
|
|
85
85
|
}
|
|
86
|
+
function buildEvmUptoOptions(accepts, price) {
|
|
87
|
+
return accepts.filter(
|
|
88
|
+
(accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
|
|
89
|
+
).map((accept) => ({
|
|
90
|
+
scheme: "upto",
|
|
91
|
+
network: accept.network,
|
|
92
|
+
payTo: accept.payTo,
|
|
93
|
+
price,
|
|
94
|
+
...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
|
|
95
|
+
...accept.extra ? { extra: accept.extra } : {}
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
86
98
|
var init_evm = __esm({
|
|
87
99
|
"src/protocols/x402/evm.ts"() {
|
|
88
100
|
"use strict";
|
|
@@ -267,40 +279,40 @@ async function createX402Server(config) {
|
|
|
267
279
|
facilitatorsByNetwork
|
|
268
280
|
};
|
|
269
281
|
}
|
|
270
|
-
function
|
|
282
|
+
function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
|
|
283
|
+
const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
|
|
284
|
+
return groups.map((group) => {
|
|
285
|
+
const inner = new HTTPFacilitatorClient(group.config);
|
|
286
|
+
const kinds = buildSupportedKinds(group);
|
|
287
|
+
return hardcodedSupportedClient(inner, kinds);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function hardcodedSupportedClient(inner, kinds) {
|
|
271
291
|
return {
|
|
272
292
|
verify: inner.verify.bind(inner),
|
|
273
293
|
settle: inner.settle.bind(inner),
|
|
274
|
-
getSupported: async () => ({
|
|
275
|
-
kinds,
|
|
276
|
-
extensions: [],
|
|
277
|
-
signers: {}
|
|
278
|
-
})
|
|
294
|
+
getSupported: async () => ({ kinds, extensions: [], signers: {} })
|
|
279
295
|
};
|
|
280
296
|
}
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
extra: {
|
|
292
|
-
features: {
|
|
293
|
-
xSettlementAccountSupported: true
|
|
294
|
-
}
|
|
297
|
+
function buildSupportedKinds(group) {
|
|
298
|
+
return group.networks.flatMap((network) => {
|
|
299
|
+
const exactKind = {
|
|
300
|
+
x402Version: 2,
|
|
301
|
+
scheme: "exact",
|
|
302
|
+
network,
|
|
303
|
+
...group.family === "solana" ? {
|
|
304
|
+
extra: {
|
|
305
|
+
features: {
|
|
306
|
+
xSettlementAccountSupported: true
|
|
295
307
|
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return [exactKind];
|
|
302
|
-
}
|
|
303
|
-
return
|
|
308
|
+
}
|
|
309
|
+
} : {}
|
|
310
|
+
};
|
|
311
|
+
const uptoKind = { x402Version: 2, scheme: "upto", network };
|
|
312
|
+
if (group.family === "evm") {
|
|
313
|
+
return [exactKind, uptoKind];
|
|
314
|
+
}
|
|
315
|
+
return [exactKind, uptoKind];
|
|
304
316
|
});
|
|
305
317
|
}
|
|
306
318
|
var init_server = __esm({
|
|
@@ -313,65 +325,9 @@ var init_server = __esm({
|
|
|
313
325
|
}
|
|
314
326
|
});
|
|
315
327
|
|
|
316
|
-
// src/upstash-rest.ts
|
|
317
|
-
var upstash_rest_exports = {};
|
|
318
|
-
__export(upstash_rest_exports, {
|
|
319
|
-
createUpstashRest: () => createUpstashRest
|
|
320
|
-
});
|
|
321
|
-
function createUpstashRest(url, token) {
|
|
322
|
-
const base = url.replace(/\/+$/, "");
|
|
323
|
-
const headers = { Authorization: `Bearer ${token}` };
|
|
324
|
-
async function get(key) {
|
|
325
|
-
const res = await fetch(`${base}/get/${key}`, { headers });
|
|
326
|
-
if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
|
|
327
|
-
const { result } = await res.json();
|
|
328
|
-
return result ?? null;
|
|
329
|
-
}
|
|
330
|
-
async function set(key, value) {
|
|
331
|
-
const res = await fetch(`${base}`, {
|
|
332
|
-
method: "POST",
|
|
333
|
-
headers: { ...headers, "Content-Type": "application/json" },
|
|
334
|
-
body: JSON.stringify(["SET", key, JSON.stringify(value)])
|
|
335
|
-
});
|
|
336
|
-
if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
|
|
337
|
-
return await res.json();
|
|
338
|
-
}
|
|
339
|
-
async function del(key) {
|
|
340
|
-
const res = await fetch(`${base}`, {
|
|
341
|
-
method: "POST",
|
|
342
|
-
headers: { ...headers, "Content-Type": "application/json" },
|
|
343
|
-
body: JSON.stringify(["DEL", key])
|
|
344
|
-
});
|
|
345
|
-
if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
|
|
346
|
-
return await res.json();
|
|
347
|
-
}
|
|
348
|
-
return {
|
|
349
|
-
get,
|
|
350
|
-
set,
|
|
351
|
-
del,
|
|
352
|
-
async update(key, fn) {
|
|
353
|
-
const current = await get(key);
|
|
354
|
-
const change = fn(current);
|
|
355
|
-
if (change.op === "set") await set(key, change.value);
|
|
356
|
-
if (change.op === "delete") await del(key);
|
|
357
|
-
return change.result;
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
var init_upstash_rest = __esm({
|
|
362
|
-
"src/upstash-rest.ts"() {
|
|
363
|
-
"use strict";
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
|
|
367
328
|
// src/registry.ts
|
|
368
329
|
var RouteRegistry = class {
|
|
369
330
|
routes = /* @__PURE__ */ new Map();
|
|
370
|
-
// Internal map key includes the HTTP method so that POST and DELETE on the
|
|
371
|
-
// same path coexist. Within the same path+method, last-write-wins is still
|
|
372
|
-
// intentional — Next.js module loading order is non-deterministic during
|
|
373
|
-
// build and discovery stubs may register the same route in either order.
|
|
374
|
-
// Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
|
|
375
331
|
mapKey(entry) {
|
|
376
332
|
return `${entry.key}:${entry.method}`;
|
|
377
333
|
}
|
|
@@ -384,8 +340,6 @@ var RouteRegistry = class {
|
|
|
384
340
|
}
|
|
385
341
|
this.routes.set(k, entry);
|
|
386
342
|
}
|
|
387
|
-
// Accepts either a compound key ("site/domain:DELETE") or a path-only key
|
|
388
|
-
// ("site/domain") — path-only returns the first registered method for that path.
|
|
389
343
|
get(key) {
|
|
390
344
|
const direct = this.routes.get(key);
|
|
391
345
|
if (direct) return direct;
|
|
@@ -417,24 +371,17 @@ var RouteRegistry = class {
|
|
|
417
371
|
|
|
418
372
|
// src/headers.ts
|
|
419
373
|
var HEADERS = {
|
|
420
|
-
// ---- Standard HTTP ----
|
|
421
374
|
AUTHORIZATION: "Authorization",
|
|
422
375
|
WWW_AUTHENTICATE: "WWW-Authenticate",
|
|
423
|
-
// ---- Auth ----
|
|
424
376
|
API_KEY: "X-API-Key",
|
|
425
|
-
// ---- Request meta (used by plugin/observability) ----
|
|
426
377
|
WALLET_ADDRESS: "X-Wallet-Address",
|
|
427
378
|
CLIENT_ID: "X-Client-ID",
|
|
428
379
|
SESSION_ID: "X-Session-ID",
|
|
429
|
-
// ---- SIWX ----
|
|
430
380
|
SIWX: "SIGN-IN-WITH-X",
|
|
431
|
-
// ---- x402 (payment) ----
|
|
432
381
|
X402_PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
|
|
433
|
-
/** Legacy x402 payment header — accepted alongside PAYMENT-SIGNATURE. */
|
|
434
382
|
X402_PAYMENT_LEGACY: "X-PAYMENT",
|
|
435
383
|
X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
|
|
436
384
|
X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
|
|
437
|
-
// ---- MPP (payment) ----
|
|
438
385
|
MPP_PAYMENT_RECEIPT: "Payment-Receipt"
|
|
439
386
|
};
|
|
440
387
|
var AUTH_SCHEME = {
|
|
@@ -517,11 +464,31 @@ function consolePlugin() {
|
|
|
517
464
|
};
|
|
518
465
|
}
|
|
519
466
|
|
|
467
|
+
// src/alert.ts
|
|
468
|
+
function createReporter(plugin, pluginCtx, route) {
|
|
469
|
+
return (level, message, meta) => {
|
|
470
|
+
firePluginHook(plugin, "onAlert", pluginCtx, {
|
|
471
|
+
level,
|
|
472
|
+
message,
|
|
473
|
+
route,
|
|
474
|
+
...meta ? { meta } : {}
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
520
479
|
// src/pipeline/context/preflight.ts
|
|
521
480
|
function preflight(routeEntry, handler, deps, request) {
|
|
522
481
|
const meta = buildMeta(request, routeEntry);
|
|
523
482
|
const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
|
|
524
|
-
return {
|
|
483
|
+
return {
|
|
484
|
+
routeEntry,
|
|
485
|
+
handler,
|
|
486
|
+
deps,
|
|
487
|
+
request,
|
|
488
|
+
meta,
|
|
489
|
+
pluginCtx,
|
|
490
|
+
report: createReporter(deps.plugin, pluginCtx, routeEntry.key)
|
|
491
|
+
};
|
|
525
492
|
}
|
|
526
493
|
function buildMeta(request, routeEntry) {
|
|
527
494
|
return {
|
|
@@ -566,19 +533,38 @@ function validateBody(parsed, schema) {
|
|
|
566
533
|
};
|
|
567
534
|
}
|
|
568
535
|
|
|
536
|
+
// src/pipeline/context/fire-plugin-response.ts
|
|
537
|
+
function firePluginResponse(ctx, response, requestBody, responseBody) {
|
|
538
|
+
firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
|
|
539
|
+
statusCode: response.status,
|
|
540
|
+
statusText: response.statusText,
|
|
541
|
+
duration: Date.now() - ctx.meta.startTime,
|
|
542
|
+
contentType: response.headers.get("content-type"),
|
|
543
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
544
|
+
requestBody,
|
|
545
|
+
responseBody
|
|
546
|
+
});
|
|
547
|
+
if (response.status >= 400 && response.status !== 402) {
|
|
548
|
+
firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
|
|
549
|
+
status: response.status,
|
|
550
|
+
message: response.statusText || `HTTP ${response.status}`,
|
|
551
|
+
settled: false
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
569
556
|
// src/pipeline/context/parse-body.ts
|
|
570
|
-
async function parseBody(
|
|
571
|
-
if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
|
|
557
|
+
async function parseBody(ctx, request = ctx.request) {
|
|
558
|
+
if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
|
|
572
559
|
const raw = await bufferBody(request);
|
|
573
|
-
const result = validateBody(raw, routeEntry.bodySchema);
|
|
560
|
+
const result = validateBody(raw, ctx.routeEntry.bodySchema);
|
|
574
561
|
if (result.success) return { ok: true, data: result.data };
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
};
|
|
562
|
+
const response = NextResponse.json(
|
|
563
|
+
{ success: false, error: result.error, issues: result.issues },
|
|
564
|
+
{ status: 400 }
|
|
565
|
+
);
|
|
566
|
+
firePluginResponse(ctx, response);
|
|
567
|
+
return { ok: false, response };
|
|
582
568
|
}
|
|
583
569
|
|
|
584
570
|
// src/pipeline/context/parse-query.ts
|
|
@@ -604,28 +590,6 @@ function handlerFailureError(response) {
|
|
|
604
590
|
|
|
605
591
|
// src/pipeline/context/fail.ts
|
|
606
592
|
import { NextResponse as NextResponse2 } from "next/server";
|
|
607
|
-
|
|
608
|
-
// src/pipeline/context/fire-plugin-response.ts
|
|
609
|
-
function firePluginResponse(ctx, response, requestBody, responseBody) {
|
|
610
|
-
firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
|
|
611
|
-
statusCode: response.status,
|
|
612
|
-
statusText: response.statusText,
|
|
613
|
-
duration: Date.now() - ctx.meta.startTime,
|
|
614
|
-
contentType: response.headers.get("content-type"),
|
|
615
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
616
|
-
requestBody,
|
|
617
|
-
responseBody
|
|
618
|
-
});
|
|
619
|
-
if (response.status >= 400 && response.status !== 402) {
|
|
620
|
-
firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
|
|
621
|
-
status: response.status,
|
|
622
|
-
message: response.statusText || `HTTP ${response.status}`,
|
|
623
|
-
settled: false
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// src/pipeline/context/fail.ts
|
|
629
593
|
function fail(ctx, status, message, requestBody) {
|
|
630
594
|
const response = NextResponse2.json({ success: false, error: message }, { status });
|
|
631
595
|
firePluginResponse(ctx, response, requestBody);
|
|
@@ -643,7 +607,7 @@ async function runValidate(ctx, body) {
|
|
|
643
607
|
}
|
|
644
608
|
}
|
|
645
609
|
|
|
646
|
-
// src/
|
|
610
|
+
// src/pipeline/flows/static/static-invoke.ts
|
|
647
611
|
import { NextResponse as NextResponse3 } from "next/server";
|
|
648
612
|
|
|
649
613
|
// src/types.ts
|
|
@@ -655,23 +619,15 @@ var HttpError = class extends Error {
|
|
|
655
619
|
}
|
|
656
620
|
};
|
|
657
621
|
|
|
658
|
-
// src/
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const result = await handler(ctx);
|
|
662
|
-
if (result instanceof Response) return result;
|
|
663
|
-
return NextResponse3.json(result);
|
|
664
|
-
} catch (error) {
|
|
665
|
-
options.onError?.(error);
|
|
666
|
-
const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
|
|
667
|
-
const message = error instanceof Error ? error.message : "Internal error";
|
|
668
|
-
return NextResponse3.json({ success: false, error: message }, { status });
|
|
669
|
-
}
|
|
622
|
+
// src/pipeline/flows/static/static-invoke.ts
|
|
623
|
+
function invokePaidStatic(ctx, wallet, account, body, payment) {
|
|
624
|
+
return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
|
|
670
625
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
626
|
+
function invokeUnauthed(ctx, wallet, account, body) {
|
|
627
|
+
return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, null));
|
|
628
|
+
}
|
|
629
|
+
function buildHandlerCtx(ctx, wallet, account, body, payment) {
|
|
630
|
+
return {
|
|
675
631
|
body,
|
|
676
632
|
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
677
633
|
request: ctx.request,
|
|
@@ -690,21 +646,45 @@ async function invoke(ctx, wallet, account, body, payment) {
|
|
|
690
646
|
},
|
|
691
647
|
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
692
648
|
};
|
|
649
|
+
}
|
|
650
|
+
async function runHandler(ctx, handlerCtx) {
|
|
651
|
+
let returned;
|
|
652
|
+
try {
|
|
653
|
+
returned = ctx.handler(handlerCtx);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
return errorResult(error);
|
|
656
|
+
}
|
|
657
|
+
if (isAsyncIterable(returned) && !isThenable(returned)) {
|
|
658
|
+
return errorResult(
|
|
659
|
+
new HttpError(
|
|
660
|
+
`route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
|
|
661
|
+
500
|
|
662
|
+
)
|
|
663
|
+
);
|
|
664
|
+
}
|
|
693
665
|
let rawResult;
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
666
|
+
try {
|
|
667
|
+
rawResult = await returned;
|
|
668
|
+
} catch (error) {
|
|
669
|
+
return errorResult(error);
|
|
670
|
+
}
|
|
671
|
+
const response = rawResult instanceof Response ? rawResult : NextResponse3.json(rawResult);
|
|
672
|
+
return { response, rawResult };
|
|
673
|
+
}
|
|
674
|
+
function errorResult(error) {
|
|
675
|
+
const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
|
|
676
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
677
|
+
return {
|
|
678
|
+
response: NextResponse3.json({ success: false, error: message }, { status }),
|
|
679
|
+
rawResult: void 0,
|
|
680
|
+
handlerError: error
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function isAsyncIterable(value) {
|
|
684
|
+
return value != null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
685
|
+
}
|
|
686
|
+
function isThenable(value) {
|
|
687
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
708
688
|
}
|
|
709
689
|
|
|
710
690
|
// src/pipeline/context/fire-provider-quota.ts
|
|
@@ -738,24 +718,24 @@ function computeQuotaLevel(remaining, warn, critical) {
|
|
|
738
718
|
return "healthy";
|
|
739
719
|
}
|
|
740
720
|
|
|
741
|
-
// src/pipeline/context/finalize.ts
|
|
721
|
+
// src/pipeline/context/finalize/response.ts
|
|
742
722
|
function finalize(ctx, response, rawResult, requestBody) {
|
|
743
723
|
fireProviderQuota(ctx, response, rawResult);
|
|
744
724
|
firePluginResponse(ctx, response, requestBody, rawResult);
|
|
745
725
|
return response;
|
|
746
726
|
}
|
|
747
727
|
|
|
748
|
-
// src/pipeline/context/
|
|
749
|
-
async function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
728
|
+
// src/pipeline/context/grant-entitlement.ts
|
|
729
|
+
async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
730
|
+
if (!ctx.routeEntry.siwxEnabled) return;
|
|
731
|
+
try {
|
|
732
|
+
await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
|
|
733
|
+
} catch (error) {
|
|
734
|
+
ctx.report(
|
|
735
|
+
"warn",
|
|
736
|
+
`Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`
|
|
737
|
+
);
|
|
754
738
|
}
|
|
755
|
-
const validateErr = await runValidate(ctx, body.data);
|
|
756
|
-
if (validateErr) return validateErr;
|
|
757
|
-
const result = await invoke(ctx, wallet, account, body.data, null);
|
|
758
|
-
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
759
739
|
}
|
|
760
740
|
|
|
761
741
|
// src/pipeline/context/settlement-context.ts
|
|
@@ -772,23 +752,6 @@ function settlementContext(ctx, scope) {
|
|
|
772
752
|
};
|
|
773
753
|
}
|
|
774
754
|
|
|
775
|
-
// src/pipeline/context/run-before-settle.ts
|
|
776
|
-
async function runBeforeSettle(ctx, scope) {
|
|
777
|
-
const hook = ctx.routeEntry.settlement?.beforeSettle;
|
|
778
|
-
if (!hook) return null;
|
|
779
|
-
try {
|
|
780
|
-
await hook(settlementContext(ctx, scope));
|
|
781
|
-
return null;
|
|
782
|
-
} catch (error) {
|
|
783
|
-
return fail(
|
|
784
|
-
ctx,
|
|
785
|
-
errorStatus(error, 500),
|
|
786
|
-
errorMessage(error, "Pre-settlement validation failed"),
|
|
787
|
-
scope.body
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
755
|
// src/pipeline/context/run-settlement-error.ts
|
|
793
756
|
async function runSettlementError(ctx, scope, error, phase) {
|
|
794
757
|
const hook = ctx.routeEntry.settlement?.onSettlementError;
|
|
@@ -797,12 +760,7 @@ async function runSettlementError(ctx, scope, error, phase) {
|
|
|
797
760
|
await hook({ ...settlementContext(ctx, scope), error, phase });
|
|
798
761
|
} catch (hookError) {
|
|
799
762
|
const message = errorMessage(hookError, "Settlement error hook failed");
|
|
800
|
-
|
|
801
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
802
|
-
level: "error",
|
|
803
|
-
message: `Settlement error hook failed: ${message}`,
|
|
804
|
-
route: ctx.routeEntry.key
|
|
805
|
-
});
|
|
763
|
+
ctx.report("error", `Settlement error hook failed: ${message}`);
|
|
806
764
|
}
|
|
807
765
|
}
|
|
808
766
|
|
|
@@ -814,81 +772,145 @@ async function runAfterSettle(ctx, scope) {
|
|
|
814
772
|
await hook(settlementContext(ctx, scope));
|
|
815
773
|
} catch (error) {
|
|
816
774
|
const message = errorMessage(error, "Post-settlement hook failed");
|
|
817
|
-
|
|
818
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
819
|
-
level: "error",
|
|
820
|
-
message: `Post-settlement hook failed: ${message}`,
|
|
821
|
-
route: ctx.routeEntry.key
|
|
822
|
-
});
|
|
775
|
+
ctx.report("error", `Post-settlement hook failed: ${message}`);
|
|
823
776
|
await runSettlementError(ctx, scope, error, "afterSettle");
|
|
824
777
|
}
|
|
825
778
|
}
|
|
826
779
|
|
|
827
|
-
// src/pipeline/context/
|
|
828
|
-
async function
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
route: ctx.routeEntry.key
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// src/pipeline/context/grant-entitlement.ts
|
|
845
|
-
async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
846
|
-
if (!ctx.routeEntry.siwxEnabled) return;
|
|
847
|
-
try {
|
|
848
|
-
await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
|
|
849
|
-
} catch (error) {
|
|
850
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
851
|
-
level: "warn",
|
|
852
|
-
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
853
|
-
route: ctx.routeEntry.key
|
|
854
|
-
});
|
|
855
|
-
}
|
|
780
|
+
// src/pipeline/context/finalize/epilogue.ts
|
|
781
|
+
async function runPostSettleEpilogue(args) {
|
|
782
|
+
const { ctx, strategy, wallet, settle, afterSettleScope, rawResult, body } = args;
|
|
783
|
+
await grantEntitlementIfSiwx(ctx, wallet);
|
|
784
|
+
firePluginHook(ctx.deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
|
|
785
|
+
protocol: strategy.protocol,
|
|
786
|
+
payer: wallet,
|
|
787
|
+
transaction: settle.settledPayment.transaction ?? "",
|
|
788
|
+
network: settle.settledPayment.network
|
|
789
|
+
});
|
|
790
|
+
await runAfterSettle(ctx, afterSettleScope);
|
|
791
|
+
return finalize(ctx, settle.response, rawResult, body);
|
|
856
792
|
}
|
|
857
793
|
|
|
858
|
-
// src/pipeline/context/
|
|
859
|
-
async function
|
|
860
|
-
const { ctx, strategy, verifyOutcome, scope, rawResult, body, onSettleError } = args;
|
|
861
|
-
const { request, routeEntry, deps } = ctx;
|
|
794
|
+
// src/pipeline/context/finalize/request.ts
|
|
795
|
+
async function settleAndFinalizeRequest(args) {
|
|
796
|
+
const { ctx, strategy, verifyOutcome, scope, rawResult, body, billedAmount, onSettleError } = args;
|
|
797
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
862
798
|
const settle = await strategy.settle({
|
|
863
799
|
request,
|
|
864
800
|
response: scope.response,
|
|
865
801
|
payment: verifyOutcome.payment,
|
|
866
802
|
token: verifyOutcome.token,
|
|
867
803
|
routeEntry,
|
|
868
|
-
deps
|
|
804
|
+
deps,
|
|
805
|
+
billedAmount,
|
|
806
|
+
report
|
|
869
807
|
});
|
|
870
808
|
if (!settle.ok) {
|
|
871
809
|
if (onSettleError) await onSettleError(settle.error, settle.failMessage);
|
|
872
810
|
return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
|
|
873
811
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
812
|
+
return runPostSettleEpilogue({
|
|
813
|
+
ctx,
|
|
814
|
+
strategy,
|
|
815
|
+
wallet: verifyOutcome.wallet,
|
|
816
|
+
settle,
|
|
817
|
+
afterSettleScope: {
|
|
818
|
+
...scope,
|
|
819
|
+
payment: settle.settledPayment,
|
|
820
|
+
response: settle.response
|
|
821
|
+
},
|
|
822
|
+
rawResult,
|
|
823
|
+
body
|
|
880
824
|
});
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/pipeline/context/finalize/stream.ts
|
|
828
|
+
async function settleAndFinalizeStream(args) {
|
|
829
|
+
const { ctx, strategy, verifyOutcome, source, account, body, bindChannelCharge } = args;
|
|
830
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
831
|
+
if (!strategy.settleStream) {
|
|
832
|
+
return fail(ctx, 500, `${strategy.protocol} does not support streaming handlers`, body);
|
|
833
|
+
}
|
|
834
|
+
const settle = await strategy.settleStream({
|
|
835
|
+
request,
|
|
836
|
+
source,
|
|
837
|
+
payment: verifyOutcome.payment,
|
|
838
|
+
token: verifyOutcome.token,
|
|
839
|
+
routeEntry,
|
|
840
|
+
deps,
|
|
841
|
+
bindChannelCharge,
|
|
842
|
+
report
|
|
885
843
|
});
|
|
886
|
-
|
|
844
|
+
if (!settle.ok) {
|
|
845
|
+
return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
|
|
846
|
+
}
|
|
847
|
+
return runPostSettleEpilogue({
|
|
848
|
+
ctx,
|
|
849
|
+
strategy,
|
|
850
|
+
wallet: verifyOutcome.wallet,
|
|
851
|
+
settle,
|
|
852
|
+
afterSettleScope: {
|
|
853
|
+
wallet: verifyOutcome.wallet,
|
|
854
|
+
account,
|
|
855
|
+
body,
|
|
856
|
+
payment: settle.settledPayment,
|
|
857
|
+
response: settle.response,
|
|
858
|
+
rawResult: void 0
|
|
859
|
+
},
|
|
860
|
+
rawResult: void 0,
|
|
861
|
+
body
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/pipeline/context/run-handler-only.ts
|
|
866
|
+
async function runHandlerOnly(ctx, wallet, account) {
|
|
867
|
+
const body = await parseBody(ctx);
|
|
868
|
+
if (!body.ok) return body.response;
|
|
869
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
870
|
+
if (validateErr) return validateErr;
|
|
871
|
+
const result = await invokeUnauthed(ctx, wallet, account, body.data);
|
|
872
|
+
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/pipeline/context/run-before-settle.ts
|
|
876
|
+
async function runBeforeSettle(ctx, scope) {
|
|
877
|
+
const hook = ctx.routeEntry.settlement?.beforeSettle;
|
|
878
|
+
if (!hook) return null;
|
|
879
|
+
try {
|
|
880
|
+
await hook(settlementContext(ctx, scope));
|
|
881
|
+
return null;
|
|
882
|
+
} catch (error) {
|
|
883
|
+
return fail(
|
|
884
|
+
ctx,
|
|
885
|
+
errorStatus(error, 500),
|
|
886
|
+
errorMessage(error, "Pre-settlement validation failed"),
|
|
887
|
+
scope.body
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/pipeline/context/run-settled-handler-error.ts
|
|
893
|
+
async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
|
|
894
|
+
const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
|
|
895
|
+
if (!hook) return;
|
|
896
|
+
try {
|
|
897
|
+
await hook({ ...settlementContext(ctx, scope), error });
|
|
898
|
+
} catch (hookError) {
|
|
899
|
+
const message = errorMessage(hookError, "Settled handler error hook failed");
|
|
900
|
+
ctx.report("error", `Settled handler error hook failed: ${message}`);
|
|
901
|
+
}
|
|
887
902
|
}
|
|
888
903
|
|
|
889
904
|
// src/auth/normalize-wallet.ts
|
|
890
905
|
function normalizeWalletAddress(address) {
|
|
891
|
-
|
|
906
|
+
const isEvm = /^0x/i.test(address);
|
|
907
|
+
return isEvm ? normalizeEvmWalletAddress(address) : normalizeSolanaWalletAddress(address);
|
|
908
|
+
}
|
|
909
|
+
function normalizeEvmWalletAddress(address) {
|
|
910
|
+
return address.toLowerCase();
|
|
911
|
+
}
|
|
912
|
+
function normalizeSolanaWalletAddress(address) {
|
|
913
|
+
return address;
|
|
892
914
|
}
|
|
893
915
|
|
|
894
916
|
// src/auth/siwx.ts
|
|
@@ -968,20 +990,18 @@ function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
|
|
|
968
990
|
return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
|
|
969
991
|
}
|
|
970
992
|
|
|
971
|
-
// src/pipeline/context/
|
|
972
|
-
function
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
if (errors.length === 0) return null;
|
|
984
|
-
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
993
|
+
// src/pipeline/context/resolve-early-body.ts
|
|
994
|
+
async function resolveEarlyBody(args) {
|
|
995
|
+
const { ctx, pricing, incomingStrategy } = args;
|
|
996
|
+
if (!shouldParseBodyEarly(incomingStrategy, ctx.routeEntry, pricing)) {
|
|
997
|
+
return { ok: true, earlyBody: void 0 };
|
|
998
|
+
}
|
|
999
|
+
const earlyClone = ctx.request.clone();
|
|
1000
|
+
const earlyResult = await parseBody(ctx, earlyClone);
|
|
1001
|
+
if (!earlyResult.ok) return { ok: false, response: earlyResult.response };
|
|
1002
|
+
const validateErr = await runValidate(ctx, earlyResult.data);
|
|
1003
|
+
if (validateErr) return { ok: false, response: validateErr };
|
|
1004
|
+
return { ok: true, earlyBody: earlyResult.data };
|
|
985
1005
|
}
|
|
986
1006
|
|
|
987
1007
|
// src/auth/api-key.ts
|
|
@@ -1002,6 +1022,39 @@ function extractBearerToken(header) {
|
|
|
1002
1022
|
return null;
|
|
1003
1023
|
}
|
|
1004
1024
|
|
|
1025
|
+
// src/pipeline/context/run-api-key-gate.ts
|
|
1026
|
+
async function runApiKeyGate(ctx) {
|
|
1027
|
+
const { request, routeEntry, deps } = ctx;
|
|
1028
|
+
if (!routeEntry.apiKeyResolver) return { ok: true, account: void 0 };
|
|
1029
|
+
const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
|
|
1030
|
+
if (!apiKeyResult.valid) {
|
|
1031
|
+
return { ok: false, response: fail(ctx, 401, "Invalid or missing API key") };
|
|
1032
|
+
}
|
|
1033
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
1034
|
+
authMode: "apiKey",
|
|
1035
|
+
wallet: null,
|
|
1036
|
+
route: routeEntry.key,
|
|
1037
|
+
account: apiKeyResult.account
|
|
1038
|
+
});
|
|
1039
|
+
return { ok: true, account: apiKeyResult.account };
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/pipeline/context/protocol-init-error.ts
|
|
1043
|
+
function protocolInitError(routeEntry, deps) {
|
|
1044
|
+
if (!routeEntry.pricing) return null;
|
|
1045
|
+
const errors = [];
|
|
1046
|
+
for (const protocol of routeEntry.protocols) {
|
|
1047
|
+
if (protocol === "x402" && deps.x402InitError) {
|
|
1048
|
+
errors.push(`x402: ${deps.x402InitError}`);
|
|
1049
|
+
}
|
|
1050
|
+
if (protocol === "mpp" && deps.mppInitError) {
|
|
1051
|
+
errors.push(`mpp: ${deps.mppInitError}`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (errors.length === 0) return null;
|
|
1055
|
+
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1005
1058
|
// src/pipeline/flows/api-key-only.ts
|
|
1006
1059
|
async function runApiKeyOnlyFlow(ctx) {
|
|
1007
1060
|
if (!ctx.routeEntry.apiKeyResolver) {
|
|
@@ -1171,13 +1224,22 @@ function selectPricing(raw, deps = {}) {
|
|
|
1171
1224
|
// src/protocols/mpp/credential.ts
|
|
1172
1225
|
import { Credential } from "mppx";
|
|
1173
1226
|
import { getAddress, isAddress } from "viem";
|
|
1227
|
+
var SESSION_ACTIONS = /* @__PURE__ */ new Set(["open", "topUp", "voucher", "close"]);
|
|
1174
1228
|
function readMppCredential(request) {
|
|
1175
1229
|
const credential = Credential.fromRequest(request);
|
|
1176
1230
|
if (!credential) return null;
|
|
1177
1231
|
const wallet = walletFromDid(credential.source ?? "");
|
|
1178
|
-
const
|
|
1232
|
+
const payload = credential.payload;
|
|
1233
|
+
const rawType = payload?.type;
|
|
1179
1234
|
const payloadType = rawType === "transaction" ? "transaction" : rawType === "hash" ? "hash" : "unknown";
|
|
1180
|
-
|
|
1235
|
+
const rawAction = payload?.action;
|
|
1236
|
+
const sessionAction = typeof rawAction === "string" && SESSION_ACTIONS.has(rawAction) ? rawAction : void 0;
|
|
1237
|
+
return {
|
|
1238
|
+
credential,
|
|
1239
|
+
wallet,
|
|
1240
|
+
payloadType,
|
|
1241
|
+
...sessionAction ? { sessionAction } : {}
|
|
1242
|
+
};
|
|
1181
1243
|
}
|
|
1182
1244
|
function walletFromDid(rawSource) {
|
|
1183
1245
|
const parts = rawSource.split(":");
|
|
@@ -1185,6 +1247,168 @@ function walletFromDid(rawSource) {
|
|
|
1185
1247
|
return normalizeWalletAddress(isAddress(last) ? getAddress(last) : rawSource);
|
|
1186
1248
|
}
|
|
1187
1249
|
|
|
1250
|
+
// src/protocols/mpp/session-mode.ts
|
|
1251
|
+
async function verifySessionMode(args, info) {
|
|
1252
|
+
const { request, deps, price, routeEntry } = args;
|
|
1253
|
+
if (!deps.mppx?.sessionRequest || !deps.mppx?.sessionStream || !deps.mppSessionConfig) {
|
|
1254
|
+
return {
|
|
1255
|
+
ok: false,
|
|
1256
|
+
kind: "config",
|
|
1257
|
+
message: "MPP sessions not configured on this server (set RouterConfig.mpp.session)"
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
const tickCost = routeEntry.tickCost;
|
|
1261
|
+
const unitType = routeEntry.unitType;
|
|
1262
|
+
const streaming = routeEntry.streaming === true;
|
|
1263
|
+
const middleware = streaming ? deps.mppx.sessionStream : deps.mppx.sessionRequest;
|
|
1264
|
+
const middlewareRequest = isChannelOnlyAction(info, request) ? new Request(request.url, { method: request.method, headers: request.headers }) : request;
|
|
1265
|
+
let result;
|
|
1266
|
+
try {
|
|
1267
|
+
result = await middleware({
|
|
1268
|
+
amount: tickCost,
|
|
1269
|
+
unitType,
|
|
1270
|
+
suggestedDeposit: price,
|
|
1271
|
+
...streaming ? { meta: { streaming: "true" } } : {}
|
|
1272
|
+
})(middlewareRequest);
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1275
|
+
return {
|
|
1276
|
+
ok: false,
|
|
1277
|
+
kind: "config",
|
|
1278
|
+
message: `MPP session verify failed: ${message}`
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
if (result.status === 402) {
|
|
1282
|
+
const failure = await readMppxProblemDetails(result.challenge);
|
|
1283
|
+
return { ok: false, kind: "invalid", failure };
|
|
1284
|
+
}
|
|
1285
|
+
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1286
|
+
const payment = {
|
|
1287
|
+
protocol: "mpp",
|
|
1288
|
+
status: "verified",
|
|
1289
|
+
payer: info.wallet,
|
|
1290
|
+
amount: price,
|
|
1291
|
+
network: "tempo:4217",
|
|
1292
|
+
...mppRecipient ? { recipient: mppRecipient } : {}
|
|
1293
|
+
};
|
|
1294
|
+
const token = {
|
|
1295
|
+
mode: "session",
|
|
1296
|
+
streaming,
|
|
1297
|
+
sessionResult: result,
|
|
1298
|
+
info,
|
|
1299
|
+
tickCost
|
|
1300
|
+
};
|
|
1301
|
+
return {
|
|
1302
|
+
ok: true,
|
|
1303
|
+
wallet: info.wallet,
|
|
1304
|
+
payment,
|
|
1305
|
+
token,
|
|
1306
|
+
alreadySettled: false
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
async function settleSessionMode(args) {
|
|
1310
|
+
const { request, response, payment, token, billedAmount } = args;
|
|
1311
|
+
const sessionToken = token;
|
|
1312
|
+
if (isChannelOnlyAction(sessionToken.info, request)) {
|
|
1313
|
+
const wrapped2 = sessionToken.sessionResult.withReceipt(
|
|
1314
|
+
new Response(null, { status: 200 })
|
|
1315
|
+
);
|
|
1316
|
+
return {
|
|
1317
|
+
ok: true,
|
|
1318
|
+
response: wrapped2,
|
|
1319
|
+
settledPayment: { ...payment, status: "settled", amount: billedAmount }
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
if (sessionToken.streaming) {
|
|
1323
|
+
return {
|
|
1324
|
+
ok: false,
|
|
1325
|
+
error: new Error("streaming session content settled via request path"),
|
|
1326
|
+
failMessage: "streaming session content settled via request path",
|
|
1327
|
+
failStatus: 500
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
const wrapped = sessionToken.sessionResult.withReceipt(
|
|
1331
|
+
response
|
|
1332
|
+
);
|
|
1333
|
+
wrapped.headers.set("Cache-Control", "private");
|
|
1334
|
+
const receiptHeader = wrapped.headers.get(HEADERS.MPP_PAYMENT_RECEIPT) ?? void 0;
|
|
1335
|
+
const settledPayment = {
|
|
1336
|
+
...payment,
|
|
1337
|
+
status: "settled",
|
|
1338
|
+
amount: billedAmount,
|
|
1339
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1340
|
+
};
|
|
1341
|
+
return { ok: true, response: wrapped, settledPayment };
|
|
1342
|
+
}
|
|
1343
|
+
async function buildSessionChallenge(args) {
|
|
1344
|
+
const { request, deps, suggestedDeposit, routeEntry, report } = args;
|
|
1345
|
+
if (!deps.mppSessionConfig) return {};
|
|
1346
|
+
const streaming = routeEntry.streaming === true;
|
|
1347
|
+
const middleware = streaming ? deps.mppx?.sessionStream : deps.mppx?.sessionRequest;
|
|
1348
|
+
if (!middleware) return {};
|
|
1349
|
+
const tickCost = routeEntry.tickCost;
|
|
1350
|
+
const unitType = routeEntry.unitType;
|
|
1351
|
+
try {
|
|
1352
|
+
const result = await middleware({
|
|
1353
|
+
amount: tickCost,
|
|
1354
|
+
unitType,
|
|
1355
|
+
suggestedDeposit,
|
|
1356
|
+
...streaming ? { meta: { streaming: "true" } } : {}
|
|
1357
|
+
})(request);
|
|
1358
|
+
if (result.status === 402) {
|
|
1359
|
+
const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
1360
|
+
if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
|
|
1361
|
+
}
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
report(
|
|
1364
|
+
"warn",
|
|
1365
|
+
`MPP session challenge build failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1366
|
+
);
|
|
1367
|
+
throw err;
|
|
1368
|
+
}
|
|
1369
|
+
return {};
|
|
1370
|
+
}
|
|
1371
|
+
function isChannelOnlyAction(info, request) {
|
|
1372
|
+
const action = info.sessionAction;
|
|
1373
|
+
if (!action) return false;
|
|
1374
|
+
if (action === "close" || action === "topUp") return true;
|
|
1375
|
+
if ((action === "open" || action === "voucher") && !hasRequestBody(request)) return true;
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
async function readMppxProblemDetails(challenge) {
|
|
1379
|
+
let body;
|
|
1380
|
+
try {
|
|
1381
|
+
body = await challenge.clone().text();
|
|
1382
|
+
} catch {
|
|
1383
|
+
return { reason: "mpp_session_invalid" };
|
|
1384
|
+
}
|
|
1385
|
+
if (!body) return { reason: "mpp_session_invalid" };
|
|
1386
|
+
let parsed;
|
|
1387
|
+
try {
|
|
1388
|
+
parsed = JSON.parse(body);
|
|
1389
|
+
} catch {
|
|
1390
|
+
return { reason: "mpp_session_invalid", message: body.slice(0, 500) };
|
|
1391
|
+
}
|
|
1392
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1393
|
+
return { reason: "mpp_session_invalid" };
|
|
1394
|
+
}
|
|
1395
|
+
const details = parsed;
|
|
1396
|
+
const typeUri = typeof details.type === "string" ? details.type : void 0;
|
|
1397
|
+
const slug = typeUri ? typeUri.split("/").pop() : void 0;
|
|
1398
|
+
const reason = slug ? slug.replace(/-/g, "_") : "mpp_session_invalid";
|
|
1399
|
+
const message = typeof details.detail === "string" && details.detail.length > 0 ? details.detail : typeof details.title === "string" ? details.title : void 0;
|
|
1400
|
+
return message ? { reason, message } : { reason };
|
|
1401
|
+
}
|
|
1402
|
+
function hasRequestBody(request) {
|
|
1403
|
+
const cl = request.headers.get("content-length");
|
|
1404
|
+
if (cl !== null) {
|
|
1405
|
+
const n = Number.parseInt(cl.trim(), 10);
|
|
1406
|
+
return Number.isFinite(n) && n > 0;
|
|
1407
|
+
}
|
|
1408
|
+
if (request.headers.get("transfer-encoding") !== null) return true;
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1188
1412
|
// src/protocols/mpp/transaction-mode.ts
|
|
1189
1413
|
import { Transaction as TempoTransaction } from "viem/tempo";
|
|
1190
1414
|
import { call as viemCall } from "viem/actions";
|
|
@@ -1212,7 +1436,7 @@ async function readChallengeReason(challenge) {
|
|
|
1212
1436
|
|
|
1213
1437
|
// src/protocols/mpp/transaction-mode.ts
|
|
1214
1438
|
async function verifyTxMode(args, info) {
|
|
1215
|
-
const { deps, price,
|
|
1439
|
+
const { deps, price, report } = args;
|
|
1216
1440
|
if (!deps.tempoClient) {
|
|
1217
1441
|
return {
|
|
1218
1442
|
ok: false,
|
|
@@ -1230,7 +1454,7 @@ async function verifyTxMode(args, info) {
|
|
|
1230
1454
|
});
|
|
1231
1455
|
} catch (err) {
|
|
1232
1456
|
const message = err instanceof Error ? err.message : String(err);
|
|
1233
|
-
|
|
1457
|
+
report("warn", `MPP simulation failed: ${message}`);
|
|
1234
1458
|
return { ok: false, kind: "invalid" };
|
|
1235
1459
|
}
|
|
1236
1460
|
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
@@ -1251,7 +1475,7 @@ async function verifyTxMode(args, info) {
|
|
|
1251
1475
|
};
|
|
1252
1476
|
}
|
|
1253
1477
|
async function settleTxMode(args) {
|
|
1254
|
-
const { request, response, payment, deps,
|
|
1478
|
+
const { request, response, payment, deps, report } = args;
|
|
1255
1479
|
if (!deps.mppx) {
|
|
1256
1480
|
return {
|
|
1257
1481
|
ok: false,
|
|
@@ -1265,7 +1489,7 @@ async function settleTxMode(args) {
|
|
|
1265
1489
|
result = await deps.mppx.charge({ amount: payment.amount })(request);
|
|
1266
1490
|
} catch (err) {
|
|
1267
1491
|
const message = err instanceof Error ? err.message : String(err);
|
|
1268
|
-
|
|
1492
|
+
report("error", `MPP broadcast failed after handler: ${message}`);
|
|
1269
1493
|
return {
|
|
1270
1494
|
ok: false,
|
|
1271
1495
|
error: err,
|
|
@@ -1282,7 +1506,7 @@ async function settleTxMode(args) {
|
|
|
1282
1506
|
mppResult: result,
|
|
1283
1507
|
challenge: result.challenge
|
|
1284
1508
|
});
|
|
1285
|
-
|
|
1509
|
+
report("error", `MPP payment failed after handler: ${detail}`);
|
|
1286
1510
|
return {
|
|
1287
1511
|
ok: false,
|
|
1288
1512
|
error: settlementError,
|
|
@@ -1305,10 +1529,10 @@ async function settleTxMode(args) {
|
|
|
1305
1529
|
|
|
1306
1530
|
// src/protocols/mpp/hash-mode.ts
|
|
1307
1531
|
async function verifyHashMode(args, info) {
|
|
1308
|
-
const { deps, price,
|
|
1532
|
+
const { deps, price, request, report } = args;
|
|
1309
1533
|
if (!deps.mppx) {
|
|
1310
1534
|
const reason = deps.mppInitError ? `MPP initialization failed: ${deps.mppInitError}` : "MPP not initialized \u2014 ensure mppx is installed and mpp config (secretKey, currency, recipient) is correct";
|
|
1311
|
-
|
|
1535
|
+
report("error", reason);
|
|
1312
1536
|
return { ok: false, kind: "config", message: reason };
|
|
1313
1537
|
}
|
|
1314
1538
|
let chargeResult;
|
|
@@ -1316,13 +1540,13 @@ async function verifyHashMode(args, info) {
|
|
|
1316
1540
|
chargeResult = await deps.mppx.charge({ amount: price })(request);
|
|
1317
1541
|
} catch (err) {
|
|
1318
1542
|
const message = err instanceof Error ? err.message : String(err);
|
|
1319
|
-
|
|
1543
|
+
report("error", `MPP charge failed: ${message}`);
|
|
1320
1544
|
return { ok: false, kind: "config", message: `MPP payment processing failed: ${message}` };
|
|
1321
1545
|
}
|
|
1322
1546
|
if (chargeResult.status === 402) {
|
|
1323
1547
|
const reason = await readChallengeReason(chargeResult.challenge);
|
|
1324
1548
|
const detail = reason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
|
|
1325
|
-
|
|
1549
|
+
report("warn", `MPP credential rejected: ${detail}`);
|
|
1326
1550
|
return { ok: false, kind: "invalid" };
|
|
1327
1551
|
}
|
|
1328
1552
|
const receiptHeader = chargeResult.withReceipt(new Response()).headers.get(
|
|
@@ -1367,9 +1591,20 @@ var mppStrategy = {
|
|
|
1367
1591
|
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
1368
1592
|
return Boolean(auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT));
|
|
1369
1593
|
},
|
|
1594
|
+
preflight(request, _routeEntry) {
|
|
1595
|
+
const info = readMppCredential(request);
|
|
1596
|
+
if (!info?.sessionAction) return null;
|
|
1597
|
+
if (!isChannelOnlyAction(info, request)) return null;
|
|
1598
|
+
return { skipBody: true, skipHandler: true };
|
|
1599
|
+
},
|
|
1370
1600
|
async verify(args) {
|
|
1371
1601
|
const info = readMppCredential(args.request);
|
|
1372
1602
|
if (!info) return { ok: false, kind: "invalid" };
|
|
1603
|
+
if (args.routeEntry.dynamicPrice) {
|
|
1604
|
+
if (!info.sessionAction) return { ok: false, kind: "invalid" };
|
|
1605
|
+
return verifySessionMode(args, info);
|
|
1606
|
+
}
|
|
1607
|
+
if (info.sessionAction) return { ok: false, kind: "invalid" };
|
|
1373
1608
|
if (info.payloadType === "transaction" && args.deps.tempoClient) {
|
|
1374
1609
|
return verifyTxMode(args, info);
|
|
1375
1610
|
}
|
|
@@ -1377,28 +1612,88 @@ var mppStrategy = {
|
|
|
1377
1612
|
},
|
|
1378
1613
|
async settle(args) {
|
|
1379
1614
|
const token = args.token;
|
|
1615
|
+
if (token.mode === "session") return settleSessionMode(args);
|
|
1380
1616
|
if (token.mode === "transaction") return settleTxMode(args);
|
|
1381
1617
|
return settleHashMode(args);
|
|
1382
1618
|
},
|
|
1619
|
+
async settleStream(args) {
|
|
1620
|
+
const token = args.token;
|
|
1621
|
+
if (token.mode !== "session" || !token.streaming) {
|
|
1622
|
+
return {
|
|
1623
|
+
ok: false,
|
|
1624
|
+
error: new Error("streaming requires a streaming-mode MPP session credential"),
|
|
1625
|
+
failMessage: "streaming requires a streaming-mode MPP session credential",
|
|
1626
|
+
failStatus: 400
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
const sessionToken = token;
|
|
1630
|
+
const sseResult = sessionToken.sessionResult;
|
|
1631
|
+
const { bindChannelCharge, source: handlerStream } = args;
|
|
1632
|
+
async function* forwardHandlerStreamWithChannelDebit(channel) {
|
|
1633
|
+
bindChannelCharge(channel.charge);
|
|
1634
|
+
try {
|
|
1635
|
+
for await (const chunk of handlerStream) {
|
|
1636
|
+
yield typeof chunk === "string" ? chunk : JSON.stringify(chunk);
|
|
1637
|
+
}
|
|
1638
|
+
} finally {
|
|
1639
|
+
bindChannelCharge(null);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const sse = sseResult.withReceipt(forwardHandlerStreamWithChannelDebit);
|
|
1643
|
+
sse.headers.set("Cache-Control", "private");
|
|
1644
|
+
const settledPayment = {
|
|
1645
|
+
...args.payment,
|
|
1646
|
+
status: "settled",
|
|
1647
|
+
amount: args.payment.amount
|
|
1648
|
+
};
|
|
1649
|
+
return { ok: true, response: sse, settledPayment };
|
|
1650
|
+
},
|
|
1383
1651
|
async buildChallenge(args) {
|
|
1384
1652
|
if (!args.deps.mppx) return {};
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
);
|
|
1395
|
-
throw err;
|
|
1653
|
+
const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
|
|
1654
|
+
if (args.routeEntry.dynamicPrice && sessionsConfigured) {
|
|
1655
|
+
const tickCost = args.routeEntry.tickCost;
|
|
1656
|
+
const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
|
|
1657
|
+
const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
|
|
1658
|
+
return buildSessionChallenge({
|
|
1659
|
+
...args,
|
|
1660
|
+
suggestedDeposit
|
|
1661
|
+
});
|
|
1396
1662
|
}
|
|
1397
|
-
return
|
|
1663
|
+
return buildChargeChallenge(args);
|
|
1398
1664
|
}
|
|
1399
1665
|
};
|
|
1666
|
+
function multiplyDecimal(decimal, factor) {
|
|
1667
|
+
if (!Number.isFinite(factor) || factor <= 0) return decimal;
|
|
1668
|
+
const [whole, fraction = ""] = decimal.split(".");
|
|
1669
|
+
const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
|
|
1670
|
+
const decimals = fraction.length;
|
|
1671
|
+
if (decimals === 0) return scaled;
|
|
1672
|
+
const padded = scaled.padStart(decimals + 1, "0");
|
|
1673
|
+
const intPart = padded.slice(0, padded.length - decimals);
|
|
1674
|
+
const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
|
|
1675
|
+
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
1676
|
+
}
|
|
1677
|
+
async function buildChargeChallenge(args) {
|
|
1678
|
+
if (!args.deps.mppx) return {};
|
|
1679
|
+
try {
|
|
1680
|
+
const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
|
|
1681
|
+
if (result.status === 402) {
|
|
1682
|
+
const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
1683
|
+
if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
|
|
1684
|
+
}
|
|
1685
|
+
} catch (err) {
|
|
1686
|
+
args.report(
|
|
1687
|
+
"warn",
|
|
1688
|
+
`MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1689
|
+
);
|
|
1690
|
+
throw err;
|
|
1691
|
+
}
|
|
1692
|
+
return {};
|
|
1693
|
+
}
|
|
1400
1694
|
|
|
1401
1695
|
// src/protocols/x402/strategy.ts
|
|
1696
|
+
import { PERMIT2_ADDRESS } from "@x402/evm";
|
|
1402
1697
|
init_accepts();
|
|
1403
1698
|
|
|
1404
1699
|
// src/protocols/x402/challenge.ts
|
|
@@ -1408,20 +1703,27 @@ init_solana();
|
|
|
1408
1703
|
// src/protocols/x402/requirements.ts
|
|
1409
1704
|
init_evm();
|
|
1410
1705
|
init_solana();
|
|
1411
|
-
async function buildExpectedRequirements(server, request, price, accepts) {
|
|
1412
|
-
const
|
|
1706
|
+
async function buildExpectedRequirements(server, request, price, accepts, report) {
|
|
1707
|
+
const sdkRequirements = await buildSdkHandledRequirements(
|
|
1708
|
+
server,
|
|
1709
|
+
request,
|
|
1710
|
+
price,
|
|
1711
|
+
accepts,
|
|
1712
|
+
report
|
|
1713
|
+
);
|
|
1413
1714
|
const customRequirements = buildCustomRequirements(price, accepts);
|
|
1414
|
-
return [...
|
|
1715
|
+
return [...sdkRequirements, ...customRequirements];
|
|
1415
1716
|
}
|
|
1416
|
-
async function
|
|
1417
|
-
const
|
|
1717
|
+
async function buildSdkHandledRequirements(server, request, price, accepts, report) {
|
|
1718
|
+
const groups = [
|
|
1418
1719
|
buildEvmExactOptions(accepts, price),
|
|
1720
|
+
buildEvmUptoOptions(accepts, price),
|
|
1419
1721
|
buildSolanaExactOptions(accepts, price)
|
|
1420
1722
|
].filter((options) => options.length > 0);
|
|
1421
|
-
if (
|
|
1723
|
+
if (groups.length === 0) return [];
|
|
1422
1724
|
const requirements = [];
|
|
1423
1725
|
const failures = [];
|
|
1424
|
-
for (const options of
|
|
1726
|
+
for (const options of groups) {
|
|
1425
1727
|
try {
|
|
1426
1728
|
requirements.push(
|
|
1427
1729
|
...await server.buildPaymentRequirementsFromOptions(options, { request })
|
|
@@ -1429,21 +1731,31 @@ async function buildExactRequirements(server, request, price, accepts) {
|
|
|
1429
1731
|
} catch (error) {
|
|
1430
1732
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1431
1733
|
failures.push(err);
|
|
1432
|
-
if (
|
|
1734
|
+
if (groups.length === 1) {
|
|
1433
1735
|
throw err;
|
|
1434
1736
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1737
|
+
report?.(
|
|
1738
|
+
"warn",
|
|
1739
|
+
`Failed to build x402 ${options[0]?.scheme} requirements for ${options[0]?.network}: ${err.message}`
|
|
1437
1740
|
);
|
|
1438
1741
|
}
|
|
1439
1742
|
}
|
|
1440
1743
|
if (requirements.length > 0) {
|
|
1441
1744
|
return requirements;
|
|
1442
1745
|
}
|
|
1443
|
-
throw failures[0] ?? new Error("Failed to build x402
|
|
1746
|
+
throw failures[0] ?? new Error("Failed to build x402 SDK-handled requirements");
|
|
1444
1747
|
}
|
|
1445
1748
|
function buildCustomRequirements(price, accepts) {
|
|
1446
|
-
return accepts.filter((accept) => accept
|
|
1749
|
+
return accepts.filter((accept) => !isSdkHandled(accept)).map((accept) => buildCustomRequirement(price, accept));
|
|
1750
|
+
}
|
|
1751
|
+
function isSdkHandled(accept) {
|
|
1752
|
+
if (isEvmNetwork(accept.network)) {
|
|
1753
|
+
return accept.scheme === "exact" || accept.scheme === "upto";
|
|
1754
|
+
}
|
|
1755
|
+
if (isSolanaRequirement({ network: accept.network })) {
|
|
1756
|
+
return accept.scheme === "exact";
|
|
1757
|
+
}
|
|
1758
|
+
return false;
|
|
1447
1759
|
}
|
|
1448
1760
|
function buildCustomRequirement(price, accept) {
|
|
1449
1761
|
if (!accept.asset) {
|
|
@@ -1477,7 +1789,7 @@ function decimalToAtomicUnits(amount, decimals) {
|
|
|
1477
1789
|
|
|
1478
1790
|
// src/protocols/x402/challenge.ts
|
|
1479
1791
|
async function buildX402Challenge(opts) {
|
|
1480
|
-
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
|
|
1792
|
+
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions, report } = opts;
|
|
1481
1793
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
1482
1794
|
const resource = buildChallengeResource(request, routeEntry);
|
|
1483
1795
|
const requirements = await buildChallengeRequirements(
|
|
@@ -1486,7 +1798,8 @@ async function buildX402Challenge(opts) {
|
|
|
1486
1798
|
price,
|
|
1487
1799
|
accepts,
|
|
1488
1800
|
resource,
|
|
1489
|
-
facilitatorsByNetwork
|
|
1801
|
+
facilitatorsByNetwork,
|
|
1802
|
+
report
|
|
1490
1803
|
);
|
|
1491
1804
|
const paymentRequired = await server.createPaymentRequiredResponse(
|
|
1492
1805
|
requirements,
|
|
@@ -1497,13 +1810,13 @@ async function buildX402Challenge(opts) {
|
|
|
1497
1810
|
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1498
1811
|
return { encoded, requirements };
|
|
1499
1812
|
}
|
|
1500
|
-
async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
|
|
1501
|
-
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1813
|
+
async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork, report) {
|
|
1814
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
|
|
1502
1815
|
if (!needsFacilitatorEnrichment(accepts)) return requirements;
|
|
1503
|
-
return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
|
|
1816
|
+
return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report);
|
|
1504
1817
|
}
|
|
1505
1818
|
function needsFacilitatorEnrichment(accepts) {
|
|
1506
|
-
return
|
|
1819
|
+
return hasSolanaAccepts(accepts);
|
|
1507
1820
|
}
|
|
1508
1821
|
async function enrichGroup(group, resource) {
|
|
1509
1822
|
const accepted = await enrichRequirementsWithFacilitatorAccepts(
|
|
@@ -1518,7 +1831,7 @@ async function enrichGroup(group, resource) {
|
|
|
1518
1831
|
}
|
|
1519
1832
|
return accepted;
|
|
1520
1833
|
}
|
|
1521
|
-
async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
|
|
1834
|
+
async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report) {
|
|
1522
1835
|
const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
|
|
1523
1836
|
if (groups.length === 0) return requirements;
|
|
1524
1837
|
const results = await Promise.all(
|
|
@@ -1528,8 +1841,9 @@ async function enrichChallengeRequirements(requirements, resource, facilitatorsB
|
|
|
1528
1841
|
} catch (err) {
|
|
1529
1842
|
const label = group.facilitator.url ?? group.facilitator.network;
|
|
1530
1843
|
const reason = err instanceof Error ? err.message : String(err);
|
|
1531
|
-
|
|
1532
|
-
|
|
1844
|
+
report?.(
|
|
1845
|
+
"warn",
|
|
1846
|
+
`${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
|
|
1533
1847
|
);
|
|
1534
1848
|
return { success: false, group };
|
|
1535
1849
|
}
|
|
@@ -1582,7 +1896,7 @@ function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
|
|
|
1582
1896
|
return facilitator;
|
|
1583
1897
|
}
|
|
1584
1898
|
function requiresFacilitatorEnrichment(requirement) {
|
|
1585
|
-
return
|
|
1899
|
+
return isSolanaRequirement(requirement);
|
|
1586
1900
|
}
|
|
1587
1901
|
function buildChallengeResource(request, routeEntry) {
|
|
1588
1902
|
return {
|
|
@@ -1594,33 +1908,68 @@ function buildChallengeResource(request, routeEntry) {
|
|
|
1594
1908
|
}
|
|
1595
1909
|
|
|
1596
1910
|
// src/protocols/x402/settle.ts
|
|
1597
|
-
async function settleX402Payment(server, payload, requirements) {
|
|
1911
|
+
async function settleX402Payment(server, payload, requirements, amountOverride) {
|
|
1598
1912
|
const { encodePaymentResponseHeader } = await import("@x402/core/http");
|
|
1913
|
+
if (amountOverride?.amount !== void 0) {
|
|
1914
|
+
const upstreamTaggedAmount = tagBareDecimalAsDollars(amountOverride.amount);
|
|
1915
|
+
const result2 = await server.settlePayment(payload, requirements, void 0, void 0, {
|
|
1916
|
+
amount: upstreamTaggedAmount
|
|
1917
|
+
});
|
|
1918
|
+
return {
|
|
1919
|
+
encoded: encodePaymentResponseHeader(result2),
|
|
1920
|
+
result: result2
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1599
1923
|
const result = await server.settlePayment(payload, requirements);
|
|
1600
|
-
|
|
1601
|
-
|
|
1924
|
+
return {
|
|
1925
|
+
encoded: encodePaymentResponseHeader(result),
|
|
1926
|
+
result
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
function tagBareDecimalAsDollars(amount) {
|
|
1930
|
+
if (/^\d+\.\d+$/.test(amount)) return `$${amount}`;
|
|
1931
|
+
return amount;
|
|
1602
1932
|
}
|
|
1603
1933
|
|
|
1604
1934
|
// src/protocols/x402/verify.ts
|
|
1935
|
+
import { VerifyError } from "@x402/core/types";
|
|
1605
1936
|
async function verifyX402Payment(opts) {
|
|
1606
|
-
const { server, request, price, accepts } = opts;
|
|
1937
|
+
const { server, request, price, accepts, report } = opts;
|
|
1607
1938
|
const payload = await readPaymentPayload(request);
|
|
1608
1939
|
if (!payload) return null;
|
|
1609
|
-
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1940
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
|
|
1610
1941
|
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
1942
|
+
const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
|
|
1611
1943
|
if (!matching) {
|
|
1612
|
-
return invalidPaymentVerification(
|
|
1944
|
+
return invalidPaymentVerification({
|
|
1945
|
+
reason: "requirements_mismatch",
|
|
1946
|
+
message: "Signed payment requirements did not match any server-built requirement",
|
|
1947
|
+
...accepted ? { accepted } : {}
|
|
1948
|
+
});
|
|
1613
1949
|
}
|
|
1614
1950
|
let verify;
|
|
1615
1951
|
try {
|
|
1616
1952
|
verify = await server.verifyPayment(payload, matching);
|
|
1617
1953
|
} catch (err) {
|
|
1618
|
-
|
|
1619
|
-
|
|
1954
|
+
if (err instanceof VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
|
|
1955
|
+
return invalidPaymentVerification({
|
|
1956
|
+
reason: err.invalidReason ?? "verify_error",
|
|
1957
|
+
...err.invalidMessage ? { message: err.invalidMessage } : {},
|
|
1958
|
+
...err.payer ? { payer: err.payer } : {},
|
|
1959
|
+
...accepted ? { accepted } : {}
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1620
1962
|
throw err;
|
|
1621
1963
|
}
|
|
1622
|
-
if (!verify.isValid)
|
|
1623
|
-
|
|
1964
|
+
if (!verify.isValid) {
|
|
1965
|
+
return invalidPaymentVerification({
|
|
1966
|
+
reason: verify.invalidReason ?? "unknown",
|
|
1967
|
+
...verify.invalidMessage ? { message: verify.invalidMessage } : {},
|
|
1968
|
+
...verify.payer ? { payer: verify.payer } : {},
|
|
1969
|
+
...accepted ? { accepted } : {}
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
if (!verify.payer) {
|
|
1624
1973
|
throw new Error("x402 verification succeeded without a payer address");
|
|
1625
1974
|
}
|
|
1626
1975
|
return {
|
|
@@ -1652,11 +2001,36 @@ async function readPaymentPayload(request) {
|
|
|
1652
2001
|
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
1653
2002
|
return decodePaymentSignatureHeader(paymentHeader);
|
|
1654
2003
|
}
|
|
1655
|
-
function invalidPaymentVerification() {
|
|
1656
|
-
return {
|
|
2004
|
+
function invalidPaymentVerification(failure) {
|
|
2005
|
+
return {
|
|
2006
|
+
valid: false,
|
|
2007
|
+
payload: null,
|
|
2008
|
+
requirements: null,
|
|
2009
|
+
payer: null,
|
|
2010
|
+
...failure ? { failure } : {}
|
|
2011
|
+
};
|
|
1657
2012
|
}
|
|
1658
2013
|
|
|
1659
2014
|
// src/protocols/x402/strategy.ts
|
|
2015
|
+
function formatVerifyFailureMessage(failure) {
|
|
2016
|
+
if (failure.reason === "permit2_allowance_required") {
|
|
2017
|
+
const wallet = failure.payer ?? "<the payer wallet>";
|
|
2018
|
+
const asset = failure.accepted?.asset ?? "<the asset>";
|
|
2019
|
+
const amount = failure.accepted?.amount ?? "<the required amount>";
|
|
2020
|
+
const network = failure.accepted?.network ?? "<the payment network>";
|
|
2021
|
+
return [
|
|
2022
|
+
`Payment rejected: In order for Upto to charge, the wallet ${wallet} MUST approve Permit2 to spend ${asset} on ${network}.`,
|
|
2023
|
+
`Required call (one-time, on-chain): ${asset}.approve(${PERMIT2_ADDRESS}, MAX_UINT256) from ${wallet}.`,
|
|
2024
|
+
`Permit2 contract address: ${PERMIT2_ADDRESS}.`,
|
|
2025
|
+
`Minimum allowance for this request: ${amount} (smallest units of ${asset}); use MAX_UINT256 to avoid re-approving on every future call.`,
|
|
2026
|
+
`Alternative without an on-chain transaction: the merchant can adopt the EIP-2612 gas-sponsoring extension (https://docs.x402.org/extensions/eip2612-gas-sponsoring).`
|
|
2027
|
+
].join(" ");
|
|
2028
|
+
}
|
|
2029
|
+
if (failure.message) {
|
|
2030
|
+
return `Payment rejected (${failure.reason}): ${failure.message}`;
|
|
2031
|
+
}
|
|
2032
|
+
return `Payment rejected: ${failure.reason}`;
|
|
2033
|
+
}
|
|
1660
2034
|
var x402Strategy = {
|
|
1661
2035
|
protocol: "x402",
|
|
1662
2036
|
detects(request) {
|
|
@@ -1664,114 +2038,123 @@ var x402Strategy = {
|
|
|
1664
2038
|
request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)
|
|
1665
2039
|
);
|
|
1666
2040
|
},
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
payment,
|
|
1703
|
-
token: {
|
|
1704
|
-
payload: verifyResult.payload,
|
|
1705
|
-
requirements: verifyResult.requirements
|
|
1706
|
-
}
|
|
1707
|
-
};
|
|
1708
|
-
},
|
|
1709
|
-
async settle(args) {
|
|
1710
|
-
const { response, payment, token, deps } = args;
|
|
1711
|
-
const x402Token = token;
|
|
1712
|
-
try {
|
|
1713
|
-
const settle = await settleX402Payment(
|
|
1714
|
-
deps.x402Server,
|
|
1715
|
-
x402Token.payload,
|
|
1716
|
-
x402Token.requirements
|
|
1717
|
-
);
|
|
1718
|
-
if (!settle.result?.success) {
|
|
1719
|
-
const reason = settle.result?.errorReason || "x402 settlement returned success=false";
|
|
1720
|
-
const error = new Error(reason);
|
|
1721
|
-
error.errorReason = reason;
|
|
1722
|
-
throw error;
|
|
1723
|
-
}
|
|
1724
|
-
response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
|
|
1725
|
-
response.headers.set("Cache-Control", "private");
|
|
1726
|
-
const transaction = String(settle.result?.transaction ?? "");
|
|
1727
|
-
const settledPayment = {
|
|
1728
|
-
...payment,
|
|
1729
|
-
status: "settled",
|
|
1730
|
-
...transaction ? { transaction } : {}
|
|
2041
|
+
verify: (args) => verifyX402(args),
|
|
2042
|
+
settle: (args) => settleX402(args),
|
|
2043
|
+
buildChallenge: (args) => buildX402ChallengeContribution(args)
|
|
2044
|
+
};
|
|
2045
|
+
async function verifyX402(args) {
|
|
2046
|
+
const { request, body, price, routeEntry, deps, report } = args;
|
|
2047
|
+
if (!deps.x402Server) {
|
|
2048
|
+
const reason = deps.x402InitError ? `x402 facilitator initialization failed: ${deps.x402InitError}` : "x402 server not initialized \u2014 ensure @x402/core, @x402/evm, and @coinbase/x402 are installed";
|
|
2049
|
+
report("error", reason);
|
|
2050
|
+
return { ok: false, kind: "config", message: reason };
|
|
2051
|
+
}
|
|
2052
|
+
const accepts = await resolveX402Accepts(
|
|
2053
|
+
request,
|
|
2054
|
+
routeEntry,
|
|
2055
|
+
deps.x402Accepts,
|
|
2056
|
+
deps.payeeAddress,
|
|
2057
|
+
body
|
|
2058
|
+
);
|
|
2059
|
+
const verifyResult = await verifyX402Payment({
|
|
2060
|
+
server: deps.x402Server,
|
|
2061
|
+
request,
|
|
2062
|
+
price,
|
|
2063
|
+
accepts,
|
|
2064
|
+
report
|
|
2065
|
+
});
|
|
2066
|
+
if (!verifyResult?.valid) {
|
|
2067
|
+
const failure = verifyResult?.failure;
|
|
2068
|
+
if (failure) {
|
|
2069
|
+
return {
|
|
2070
|
+
ok: false,
|
|
2071
|
+
kind: "invalid",
|
|
2072
|
+
failure: {
|
|
2073
|
+
reason: failure.reason,
|
|
2074
|
+
message: formatVerifyFailureMessage(failure)
|
|
2075
|
+
}
|
|
1731
2076
|
};
|
|
1732
|
-
return { ok: true, response, settledPayment };
|
|
1733
|
-
} catch (err) {
|
|
1734
|
-
const errObj = err;
|
|
1735
|
-
console.error("Settlement failed", {
|
|
1736
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1737
|
-
route: args.routeEntry.key,
|
|
1738
|
-
network: payment.network,
|
|
1739
|
-
errorReason: errObj.errorReason,
|
|
1740
|
-
facilitatorStatus: errObj.response?.status,
|
|
1741
|
-
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
1742
|
-
});
|
|
1743
|
-
return { ok: false, error: err, failMessage: "Settlement failed" };
|
|
1744
2077
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
}
|
|
1765
|
-
|
|
2078
|
+
return { ok: false, kind: "invalid" };
|
|
2079
|
+
}
|
|
2080
|
+
const wallet = normalizeWalletAddress(verifyResult.payer);
|
|
2081
|
+
const { network, payTo } = verifyResult.requirements;
|
|
2082
|
+
const payment = {
|
|
2083
|
+
protocol: "x402",
|
|
2084
|
+
status: "verified",
|
|
2085
|
+
payer: wallet,
|
|
2086
|
+
amount: price,
|
|
2087
|
+
network,
|
|
2088
|
+
...payTo ? { recipient: payTo } : {}
|
|
2089
|
+
};
|
|
2090
|
+
return {
|
|
2091
|
+
ok: true,
|
|
2092
|
+
wallet,
|
|
2093
|
+
payment,
|
|
2094
|
+
token: {
|
|
2095
|
+
payload: verifyResult.payload,
|
|
2096
|
+
requirements: verifyResult.requirements
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
async function settleX402(args) {
|
|
2101
|
+
const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
|
|
2102
|
+
const { payload, requirements } = token;
|
|
2103
|
+
const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
|
|
2104
|
+
try {
|
|
2105
|
+
const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
|
|
2106
|
+
if (!settle.result?.success) {
|
|
2107
|
+
throw Object.assign(
|
|
2108
|
+
new Error(settle.result?.errorReason ?? "x402 settlement returned success=false"),
|
|
2109
|
+
{ errorReason: settle.result?.errorReason }
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
|
|
2113
|
+
response.headers.set("Cache-Control", "private");
|
|
2114
|
+
const transaction = String(settle.result.transaction ?? "");
|
|
2115
|
+
const settledPayment = {
|
|
2116
|
+
...payment,
|
|
2117
|
+
status: "settled",
|
|
2118
|
+
amount: billedAmount,
|
|
2119
|
+
...transaction ? { transaction } : {}
|
|
2120
|
+
};
|
|
2121
|
+
return { ok: true, response, settledPayment };
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
reportSettleFailure(report, err, payment.network);
|
|
2124
|
+
return { ok: false, error: err, failMessage: "Settlement failed" };
|
|
1766
2125
|
}
|
|
1767
|
-
};
|
|
1768
|
-
function getRequirementNetwork(requirements, fallback) {
|
|
1769
|
-
const network = requirements?.network;
|
|
1770
|
-
return typeof network === "string" ? network : fallback;
|
|
1771
2126
|
}
|
|
1772
|
-
function
|
|
1773
|
-
const
|
|
1774
|
-
|
|
2127
|
+
async function buildX402ChallengeContribution(args) {
|
|
2128
|
+
const { request, routeEntry, body, price, extensions, deps, report } = args;
|
|
2129
|
+
if (!deps.x402Server) return {};
|
|
2130
|
+
const accepts = await resolveX402Accepts(
|
|
2131
|
+
request,
|
|
2132
|
+
routeEntry,
|
|
2133
|
+
deps.x402Accepts,
|
|
2134
|
+
deps.payeeAddress,
|
|
2135
|
+
body
|
|
2136
|
+
);
|
|
2137
|
+
const { encoded } = await buildX402Challenge({
|
|
2138
|
+
server: deps.x402Server,
|
|
2139
|
+
routeEntry,
|
|
2140
|
+
request,
|
|
2141
|
+
price,
|
|
2142
|
+
accepts,
|
|
2143
|
+
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
2144
|
+
extensions,
|
|
2145
|
+
report
|
|
2146
|
+
});
|
|
2147
|
+
return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
|
|
2148
|
+
}
|
|
2149
|
+
function reportSettleFailure(report, err, network) {
|
|
2150
|
+
const facilitator = err ?? {};
|
|
2151
|
+
report("error", "Settlement failed", {
|
|
2152
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2153
|
+
network,
|
|
2154
|
+
errorReason: facilitator.errorReason,
|
|
2155
|
+
facilitatorStatus: facilitator.response?.status,
|
|
2156
|
+
facilitatorBody: facilitator.response?.data ?? facilitator.response?.body
|
|
2157
|
+
});
|
|
1775
2158
|
}
|
|
1776
2159
|
|
|
1777
2160
|
// src/protocols/detect.ts
|
|
@@ -1805,23 +2188,75 @@ function getAllowedStrategies(allowed) {
|
|
|
1805
2188
|
return allowed.map((name) => STRATEGIES[name]);
|
|
1806
2189
|
}
|
|
1807
2190
|
|
|
1808
|
-
// src/pipeline/
|
|
2191
|
+
// src/pipeline/flows/build402.ts
|
|
1809
2192
|
import { NextResponse as NextResponse4 } from "next/server";
|
|
1810
|
-
|
|
1811
|
-
|
|
2193
|
+
|
|
2194
|
+
// src/pipeline/challenge-extensions.ts
|
|
2195
|
+
async function buildChallengeExtensions(ctx) {
|
|
2196
|
+
const { routeEntry } = ctx;
|
|
2197
|
+
let extensions;
|
|
1812
2198
|
try {
|
|
1813
|
-
|
|
2199
|
+
const { z } = await import("zod");
|
|
2200
|
+
const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
|
|
2201
|
+
const toJSON = (schema) => z.toJSONSchema(schema, {
|
|
2202
|
+
target: "draft-2020-12",
|
|
2203
|
+
unrepresentable: "any"
|
|
2204
|
+
});
|
|
2205
|
+
const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
|
|
2206
|
+
const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
|
|
2207
|
+
if (inputSchema) {
|
|
2208
|
+
const config = {
|
|
2209
|
+
method: routeEntry.method,
|
|
2210
|
+
bodyType: routeEntry.bodySchema ? "json" : void 0,
|
|
2211
|
+
inputSchema
|
|
2212
|
+
};
|
|
2213
|
+
if (routeEntry.inputExample !== void 0) {
|
|
2214
|
+
config.input = routeEntry.inputExample;
|
|
2215
|
+
}
|
|
2216
|
+
if (outputSchema && routeEntry.outputExample !== void 0) {
|
|
2217
|
+
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
2218
|
+
}
|
|
2219
|
+
extensions = declareDiscoveryExtension(config);
|
|
2220
|
+
}
|
|
1814
2221
|
} catch (err) {
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
);
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2222
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2223
|
+
level: "warn",
|
|
2224
|
+
message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2225
|
+
route: routeEntry.key
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
if (routeEntry.siwxEnabled) {
|
|
2229
|
+
try {
|
|
2230
|
+
const siwxExtension = await buildSIWXExtension();
|
|
2231
|
+
if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
|
|
2232
|
+
extensions = {
|
|
2233
|
+
...extensions ?? {},
|
|
2234
|
+
...siwxExtension
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
} catch {
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
return extensions;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// src/pipeline/flows/build402.ts
|
|
2244
|
+
async function build402(ctx, pricing, body, failure) {
|
|
2245
|
+
let challengePrice;
|
|
2246
|
+
try {
|
|
2247
|
+
challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
const message = errorMessage(err, "Price calculation failed");
|
|
2250
|
+
const errorResponse = NextResponse4.json(
|
|
2251
|
+
{ success: false, error: message },
|
|
2252
|
+
{ status: errorStatus(err, 500) }
|
|
2253
|
+
);
|
|
2254
|
+
firePluginResponse(ctx, errorResponse);
|
|
2255
|
+
return errorResponse;
|
|
2256
|
+
}
|
|
2257
|
+
const extensions = await buildChallengeExtensions(ctx);
|
|
2258
|
+
const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
|
|
2259
|
+
const response = new NextResponse4(responseBody, {
|
|
1825
2260
|
status: 402,
|
|
1826
2261
|
headers: {
|
|
1827
2262
|
"Content-Type": "application/json",
|
|
@@ -1836,7 +2271,8 @@ async function build402(ctx, pricing, body) {
|
|
|
1836
2271
|
body,
|
|
1837
2272
|
price: challengePrice,
|
|
1838
2273
|
extensions,
|
|
1839
|
-
deps: ctx.deps
|
|
2274
|
+
deps: ctx.deps,
|
|
2275
|
+
report: ctx.report
|
|
1840
2276
|
});
|
|
1841
2277
|
if (contribution.headers) {
|
|
1842
2278
|
for (const [name, value] of Object.entries(contribution.headers)) {
|
|
@@ -1845,11 +2281,7 @@ async function build402(ctx, pricing, body) {
|
|
|
1845
2281
|
}
|
|
1846
2282
|
} catch (err) {
|
|
1847
2283
|
const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
|
|
1848
|
-
|
|
1849
|
-
level: "critical",
|
|
1850
|
-
message,
|
|
1851
|
-
route: ctx.routeEntry.key
|
|
1852
|
-
});
|
|
2284
|
+
ctx.report("critical", message);
|
|
1853
2285
|
if (strategy.protocol === "x402") {
|
|
1854
2286
|
const errorResponse = NextResponse4.json(
|
|
1855
2287
|
{ success: false, error: message },
|
|
@@ -1863,97 +2295,313 @@ async function build402(ctx, pricing, body) {
|
|
|
1863
2295
|
firePluginResponse(ctx, response);
|
|
1864
2296
|
return response;
|
|
1865
2297
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2298
|
+
|
|
2299
|
+
// src/pipeline/flows/dynamic/dynamic-body-and-price.ts
|
|
2300
|
+
async function resolveDynamicBodyAndPrice(args) {
|
|
2301
|
+
const { ctx, pricing, skipBody } = args;
|
|
2302
|
+
if (skipBody) {
|
|
2303
|
+
return {
|
|
2304
|
+
ok: true,
|
|
2305
|
+
parsedBody: void 0,
|
|
2306
|
+
price: surrogatePriceForSkippedBody(ctx.routeEntry)
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
const body = await parseBody(ctx);
|
|
2310
|
+
if (!body.ok) return { ok: false, response: body.response };
|
|
2311
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
2312
|
+
if (validateErr) {
|
|
2313
|
+
return { ok: false, response: validateErr };
|
|
2314
|
+
}
|
|
2315
|
+
if (!pricing) {
|
|
2316
|
+
return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
|
|
2317
|
+
}
|
|
1869
2318
|
try {
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
const toJSON = (schema) => z.toJSONSchema(schema, {
|
|
1873
|
-
target: "draft-2020-12",
|
|
1874
|
-
unrepresentable: "any"
|
|
1875
|
-
});
|
|
1876
|
-
const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
|
|
1877
|
-
const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
|
|
1878
|
-
if (inputSchema) {
|
|
1879
|
-
const config = {
|
|
1880
|
-
method: routeEntry.method,
|
|
1881
|
-
bodyType: routeEntry.bodySchema ? "json" : void 0,
|
|
1882
|
-
inputSchema
|
|
1883
|
-
};
|
|
1884
|
-
if (routeEntry.inputExample !== void 0) {
|
|
1885
|
-
config.input = routeEntry.inputExample;
|
|
1886
|
-
}
|
|
1887
|
-
if (outputSchema && routeEntry.outputExample !== void 0) {
|
|
1888
|
-
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
1889
|
-
}
|
|
1890
|
-
extensions = declareDiscoveryExtension(config);
|
|
1891
|
-
}
|
|
2319
|
+
const price = await pricing.quote(body.data);
|
|
2320
|
+
return { ok: true, parsedBody: body.data, price };
|
|
1892
2321
|
} catch (err) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2322
|
+
return {
|
|
2323
|
+
ok: false,
|
|
2324
|
+
response: fail(
|
|
2325
|
+
ctx,
|
|
2326
|
+
errorStatus(err, 500),
|
|
2327
|
+
errorMessage(err, "Price calculation failed"),
|
|
2328
|
+
body.data
|
|
2329
|
+
)
|
|
2330
|
+
};
|
|
1898
2331
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2332
|
+
}
|
|
2333
|
+
function surrogatePriceForSkippedBody(routeEntry) {
|
|
2334
|
+
return routeEntry.maxPrice ?? routeEntry.minPrice ?? "0";
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/pipeline/flows/dynamic/dynamic-channel-mgmt.ts
|
|
2338
|
+
import { NextResponse as NextResponse5 } from "next/server";
|
|
2339
|
+
async function runDynamicChannelMgmtFlow(args) {
|
|
2340
|
+
const { ctx, strategy, account, pricing, skipBody } = args;
|
|
2341
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
2342
|
+
const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
|
|
2343
|
+
if (!bodyAndPrice.ok) return bodyAndPrice.response;
|
|
2344
|
+
const { parsedBody, price } = bodyAndPrice;
|
|
2345
|
+
const verifyOutcome = await strategy.verify({
|
|
2346
|
+
request,
|
|
2347
|
+
body: parsedBody,
|
|
2348
|
+
price,
|
|
2349
|
+
routeEntry,
|
|
2350
|
+
deps,
|
|
2351
|
+
report
|
|
2352
|
+
});
|
|
2353
|
+
if (verifyOutcome.ok === false) {
|
|
2354
|
+
if (verifyOutcome.kind === "config") {
|
|
2355
|
+
return fail(ctx, 500, verifyOutcome.message, parsedBody);
|
|
1909
2356
|
}
|
|
2357
|
+
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
1910
2358
|
}
|
|
1911
|
-
|
|
2359
|
+
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2360
|
+
firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
|
|
2361
|
+
protocol: strategy.protocol,
|
|
2362
|
+
payer: verifyOutcome.wallet,
|
|
2363
|
+
amount: price,
|
|
2364
|
+
network: verifyOutcome.payment.network
|
|
2365
|
+
});
|
|
2366
|
+
const synthetic = new NextResponse5(null, { status: 200 });
|
|
2367
|
+
const settleScope = {
|
|
2368
|
+
wallet: verifyOutcome.wallet,
|
|
2369
|
+
account,
|
|
2370
|
+
body: parsedBody,
|
|
2371
|
+
payment: verifyOutcome.payment,
|
|
2372
|
+
response: synthetic,
|
|
2373
|
+
rawResult: void 0
|
|
2374
|
+
};
|
|
2375
|
+
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2376
|
+
if (beforeErr) return beforeErr;
|
|
2377
|
+
return settleAndFinalizeRequest({
|
|
2378
|
+
ctx,
|
|
2379
|
+
strategy,
|
|
2380
|
+
verifyOutcome,
|
|
2381
|
+
scope: settleScope,
|
|
2382
|
+
rawResult: void 0,
|
|
2383
|
+
body: parsedBody,
|
|
2384
|
+
billedAmount: "0",
|
|
2385
|
+
onSettleError: async (error, failMessage) => {
|
|
2386
|
+
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2387
|
+
report("critical", `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`);
|
|
2388
|
+
}
|
|
2389
|
+
});
|
|
1912
2390
|
}
|
|
1913
2391
|
|
|
1914
|
-
// src/pipeline/flows/
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
wallet: null,
|
|
1925
|
-
route: routeEntry.key,
|
|
1926
|
-
account
|
|
2392
|
+
// src/pipeline/flows/dynamic/dynamic-invoke.ts
|
|
2393
|
+
import { NextResponse as NextResponse6 } from "next/server";
|
|
2394
|
+
|
|
2395
|
+
// src/pricing/atomic.ts
|
|
2396
|
+
var USDC_DECIMALS = 6;
|
|
2397
|
+
function decimalToAtomic(amount) {
|
|
2398
|
+
const m = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
|
|
2399
|
+
if (!m) {
|
|
2400
|
+
throw Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
|
|
2401
|
+
status: 400
|
|
1927
2402
|
});
|
|
1928
2403
|
}
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2404
|
+
const whole = m[1];
|
|
2405
|
+
const fraction = (m[2] ?? "").slice(0, USDC_DECIMALS).padEnd(USDC_DECIMALS, "0");
|
|
2406
|
+
return BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0");
|
|
2407
|
+
}
|
|
2408
|
+
function atomicToDecimal(atomic) {
|
|
2409
|
+
const whole = atomic / 10n ** BigInt(USDC_DECIMALS);
|
|
2410
|
+
const fraction = atomic % 10n ** BigInt(USDC_DECIMALS);
|
|
2411
|
+
if (fraction === 0n) return whole.toString();
|
|
2412
|
+
const fractionStr = fraction.toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
|
|
2413
|
+
return `${whole}.${fractionStr}`;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
// src/pricing/charge-context.ts
|
|
2417
|
+
function createChargeContext(args) {
|
|
2418
|
+
const { tickCost, maxPrice, route } = args;
|
|
2419
|
+
const tickAtomic = decimalToAtomic(tickCost);
|
|
2420
|
+
if (tickAtomic <= 0n) {
|
|
2421
|
+
throw new Error(`route '${route}': tickCost '${tickCost}' must be a positive decimal string`);
|
|
2422
|
+
}
|
|
2423
|
+
const capAtomic = maxPrice !== void 0 ? decimalToAtomic(maxPrice) : null;
|
|
2424
|
+
let ticks = 0;
|
|
2425
|
+
let atomic = 0n;
|
|
2426
|
+
let channelCharge = null;
|
|
2427
|
+
const charge = async () => {
|
|
2428
|
+
const nextAtomic = atomic + tickAtomic;
|
|
2429
|
+
if (capAtomic !== null && nextAtomic > capAtomic) {
|
|
2430
|
+
throw Object.assign(
|
|
2431
|
+
new Error(
|
|
2432
|
+
`route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
|
|
2433
|
+
),
|
|
2434
|
+
{ status: 400, code: "CHARGE_OVER_CAP" }
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
ticks += 1;
|
|
2438
|
+
atomic = nextAtomic;
|
|
2439
|
+
if (channelCharge) await channelCharge();
|
|
1936
2440
|
};
|
|
2441
|
+
return {
|
|
2442
|
+
charge,
|
|
2443
|
+
bindChannelCharge: (fn) => {
|
|
2444
|
+
channelCharge = fn;
|
|
2445
|
+
},
|
|
2446
|
+
tickCount: () => ticks,
|
|
2447
|
+
atomicTotal: () => atomic
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// src/pipeline/flows/dynamic/dynamic-invoke.ts
|
|
2452
|
+
async function invokeDynamic(ctx, wallet, account, body, payment) {
|
|
2453
|
+
const streaming = ctx.routeEntry.streaming === true;
|
|
2454
|
+
const chargeContext = streaming ? createChargeContext({
|
|
2455
|
+
tickCost: ctx.routeEntry.tickCost,
|
|
2456
|
+
maxPrice: ctx.routeEntry.maxPrice,
|
|
2457
|
+
route: ctx.routeEntry.key
|
|
2458
|
+
}) : null;
|
|
2459
|
+
const baseHandlerCtx = {
|
|
2460
|
+
body,
|
|
2461
|
+
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
2462
|
+
request: ctx.request,
|
|
2463
|
+
requestId: ctx.meta.requestId,
|
|
2464
|
+
route: ctx.routeEntry.key,
|
|
2465
|
+
wallet,
|
|
2466
|
+
payment,
|
|
2467
|
+
account,
|
|
2468
|
+
alert(level, message, alertMeta) {
|
|
2469
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2470
|
+
level,
|
|
2471
|
+
message,
|
|
2472
|
+
route: ctx.routeEntry.key,
|
|
2473
|
+
meta: alertMeta
|
|
2474
|
+
});
|
|
2475
|
+
},
|
|
2476
|
+
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
2477
|
+
};
|
|
2478
|
+
const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
|
|
2479
|
+
let returned;
|
|
2480
|
+
try {
|
|
2481
|
+
returned = ctx.handler(handlerCtx);
|
|
2482
|
+
} catch (error) {
|
|
2483
|
+
return errorResult2(error, chargeContext);
|
|
2484
|
+
}
|
|
2485
|
+
if (isAsyncIterable2(returned) && !isThenable2(returned)) {
|
|
2486
|
+
if (!chargeContext) {
|
|
2487
|
+
return errorResult2(
|
|
2488
|
+
new HttpError(
|
|
2489
|
+
"route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
|
|
2490
|
+
500
|
|
2491
|
+
),
|
|
2492
|
+
null
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
return {
|
|
2496
|
+
kind: "stream",
|
|
2497
|
+
source: returned,
|
|
2498
|
+
chargeContext
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
let rawResult;
|
|
2502
|
+
try {
|
|
2503
|
+
rawResult = await returned;
|
|
2504
|
+
} catch (error) {
|
|
2505
|
+
return errorResult2(error, chargeContext);
|
|
2506
|
+
}
|
|
2507
|
+
const response = rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
|
|
2508
|
+
return { kind: "request", response, rawResult };
|
|
2509
|
+
}
|
|
2510
|
+
function errorResult2(error, chargeContext) {
|
|
2511
|
+
const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
|
|
2512
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
2513
|
+
void chargeContext;
|
|
2514
|
+
return {
|
|
2515
|
+
kind: "request",
|
|
2516
|
+
response: NextResponse6.json({ success: false, error: message }, { status }),
|
|
2517
|
+
rawResult: void 0,
|
|
2518
|
+
handlerError: error
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
function isAsyncIterable2(value) {
|
|
2522
|
+
return value != null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
2523
|
+
}
|
|
2524
|
+
function isThenable2(value) {
|
|
2525
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// src/pipeline/flows/dynamic/dynamic-preflight.ts
|
|
2529
|
+
function resolveDynamicPreflight(strategy, request, routeEntry) {
|
|
2530
|
+
const outcome = strategy.preflight?.(request, routeEntry) ?? null;
|
|
2531
|
+
return {
|
|
2532
|
+
skipBody: outcome?.skipBody ?? false,
|
|
2533
|
+
skipHandler: outcome?.skipHandler ?? false
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// src/pipeline/flows/dynamic/dynamic-request.ts
|
|
2538
|
+
async function runDynamicRequestFlow(args) {
|
|
2539
|
+
const { ctx, strategy, verifyOutcome, account, body, result } = args;
|
|
2540
|
+
const { deps, routeEntry } = ctx;
|
|
2541
|
+
const settleScope = {
|
|
2542
|
+
wallet: verifyOutcome.wallet,
|
|
2543
|
+
account,
|
|
2544
|
+
body,
|
|
2545
|
+
payment: verifyOutcome.payment,
|
|
2546
|
+
response: result.response,
|
|
2547
|
+
rawResult: result.rawResult,
|
|
2548
|
+
handlerError: result.handlerError
|
|
2549
|
+
};
|
|
2550
|
+
if (result.response.status >= 400) {
|
|
2551
|
+
return finalize(ctx, result.response, result.rawResult, body);
|
|
2552
|
+
}
|
|
2553
|
+
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2554
|
+
if (beforeErr) return beforeErr;
|
|
2555
|
+
const billedAmount = routeEntry.tickCost;
|
|
2556
|
+
return settleAndFinalizeRequest({
|
|
2557
|
+
ctx,
|
|
2558
|
+
strategy,
|
|
2559
|
+
verifyOutcome,
|
|
2560
|
+
scope: settleScope,
|
|
2561
|
+
rawResult: result.rawResult,
|
|
2562
|
+
body,
|
|
2563
|
+
billedAmount,
|
|
2564
|
+
onSettleError: async (error, failMessage) => {
|
|
2565
|
+
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2566
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2567
|
+
level: "critical",
|
|
2568
|
+
message: `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
|
|
2569
|
+
route: routeEntry.key
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// src/pipeline/flows/dynamic/dynamic-stream.ts
|
|
2576
|
+
async function runDynamicStreamFlow(args) {
|
|
2577
|
+
const { ctx, strategy, verifyOutcome, account, body, result } = args;
|
|
2578
|
+
return settleAndFinalizeStream({
|
|
2579
|
+
ctx,
|
|
2580
|
+
strategy,
|
|
2581
|
+
verifyOutcome,
|
|
2582
|
+
source: result.source,
|
|
2583
|
+
account,
|
|
2584
|
+
body,
|
|
2585
|
+
bindChannelCharge: result.chargeContext.bindChannelCharge
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/pipeline/flows/dynamic/dynamic-paid.ts
|
|
2590
|
+
async function runDynamicPaidFlow(ctx) {
|
|
2591
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
2592
|
+
const apiKeyGate = await runApiKeyGate(ctx);
|
|
2593
|
+
if (!apiKeyGate.ok) return apiKeyGate.response;
|
|
2594
|
+
const { account } = apiKeyGate;
|
|
1937
2595
|
const pricing = selectPricing(routeEntry.pricing, {
|
|
1938
|
-
alert:
|
|
2596
|
+
alert: report,
|
|
1939
2597
|
maxPrice: routeEntry.maxPrice,
|
|
1940
2598
|
minPrice: routeEntry.minPrice,
|
|
1941
2599
|
route: routeEntry.key
|
|
1942
2600
|
});
|
|
1943
2601
|
const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
|
|
1944
|
-
|
|
1945
|
-
if (
|
|
1946
|
-
|
|
1947
|
-
const earlyResult = await parseBody(earlyClone, routeEntry);
|
|
1948
|
-
if (earlyResult.ok) {
|
|
1949
|
-
earlyBody = earlyResult.data;
|
|
1950
|
-
const validateErr2 = await runValidate(ctx, earlyBody);
|
|
1951
|
-
if (validateErr2) return validateErr2;
|
|
1952
|
-
} else {
|
|
1953
|
-
firePluginResponse(ctx, earlyResult.response);
|
|
1954
|
-
return earlyResult.response;
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
2602
|
+
const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
|
|
2603
|
+
if (!earlyResolution.ok) return earlyResolution.response;
|
|
2604
|
+
const { earlyBody } = earlyResolution;
|
|
1957
2605
|
const siwxFastPath = await trySiwxFastPath(ctx, account);
|
|
1958
2606
|
if (siwxFastPath) return siwxFastPath;
|
|
1959
2607
|
if (!incomingStrategy) {
|
|
@@ -1961,39 +2609,32 @@ async function runPaidFlow(ctx) {
|
|
|
1961
2609
|
if (initError) return fail(ctx, 500, initError);
|
|
1962
2610
|
return build402(ctx, pricing, earlyBody);
|
|
1963
2611
|
}
|
|
1964
|
-
const
|
|
1965
|
-
if (
|
|
1966
|
-
|
|
1967
|
-
return body.response;
|
|
1968
|
-
}
|
|
1969
|
-
const validateErr = await runValidate(ctx, body.data);
|
|
1970
|
-
if (validateErr) return validateErr;
|
|
1971
|
-
if (!pricing) {
|
|
1972
|
-
return fail(ctx, 500, "Pricing not configured", body.data);
|
|
1973
|
-
}
|
|
1974
|
-
let price;
|
|
1975
|
-
try {
|
|
1976
|
-
price = await pricing.quote(body.data);
|
|
1977
|
-
} catch (err) {
|
|
1978
|
-
return fail(
|
|
2612
|
+
const { skipBody, skipHandler } = resolveDynamicPreflight(incomingStrategy, request, routeEntry);
|
|
2613
|
+
if (skipHandler) {
|
|
2614
|
+
return runDynamicChannelMgmtFlow({
|
|
1979
2615
|
ctx,
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2616
|
+
strategy: incomingStrategy,
|
|
2617
|
+
account,
|
|
2618
|
+
pricing,
|
|
2619
|
+
skipBody
|
|
2620
|
+
});
|
|
1984
2621
|
}
|
|
2622
|
+
const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
|
|
2623
|
+
if (!bodyAndPrice.ok) return bodyAndPrice.response;
|
|
2624
|
+
const { parsedBody, price } = bodyAndPrice;
|
|
1985
2625
|
const verifyOutcome = await incomingStrategy.verify({
|
|
1986
2626
|
request,
|
|
1987
|
-
body:
|
|
2627
|
+
body: parsedBody,
|
|
1988
2628
|
price,
|
|
1989
2629
|
routeEntry,
|
|
1990
|
-
deps
|
|
2630
|
+
deps,
|
|
2631
|
+
report
|
|
1991
2632
|
});
|
|
1992
2633
|
if (verifyOutcome.ok === false) {
|
|
1993
2634
|
if (verifyOutcome.kind === "config") {
|
|
1994
|
-
return fail(ctx, 500, verifyOutcome.message,
|
|
2635
|
+
return fail(ctx, 500, verifyOutcome.message, parsedBody);
|
|
1995
2636
|
}
|
|
1996
|
-
return build402(ctx, pricing,
|
|
2637
|
+
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
1997
2638
|
}
|
|
1998
2639
|
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
1999
2640
|
firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
|
|
@@ -2002,11 +2643,71 @@ async function runPaidFlow(ctx) {
|
|
|
2002
2643
|
amount: price,
|
|
2003
2644
|
network: verifyOutcome.payment.network
|
|
2004
2645
|
});
|
|
2005
|
-
const result = await
|
|
2646
|
+
const result = await invokeDynamic(
|
|
2647
|
+
ctx,
|
|
2648
|
+
verifyOutcome.wallet,
|
|
2649
|
+
account,
|
|
2650
|
+
parsedBody,
|
|
2651
|
+
verifyOutcome.payment
|
|
2652
|
+
);
|
|
2653
|
+
switch (result.kind) {
|
|
2654
|
+
case "stream":
|
|
2655
|
+
return runDynamicStreamFlow({
|
|
2656
|
+
ctx,
|
|
2657
|
+
strategy: incomingStrategy,
|
|
2658
|
+
verifyOutcome,
|
|
2659
|
+
account,
|
|
2660
|
+
body: parsedBody,
|
|
2661
|
+
result
|
|
2662
|
+
});
|
|
2663
|
+
case "request":
|
|
2664
|
+
return runDynamicRequestFlow({
|
|
2665
|
+
ctx,
|
|
2666
|
+
strategy: incomingStrategy,
|
|
2667
|
+
verifyOutcome,
|
|
2668
|
+
account,
|
|
2669
|
+
body: parsedBody,
|
|
2670
|
+
result
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// src/pipeline/flows/static/static-body-and-price.ts
|
|
2676
|
+
async function resolveStaticBodyAndPrice(args) {
|
|
2677
|
+
const { ctx, pricing } = args;
|
|
2678
|
+
const body = await parseBody(ctx);
|
|
2679
|
+
if (!body.ok) return { ok: false, response: body.response };
|
|
2680
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
2681
|
+
if (validateErr) {
|
|
2682
|
+
return { ok: false, response: validateErr };
|
|
2683
|
+
}
|
|
2684
|
+
if (!pricing) {
|
|
2685
|
+
return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
|
|
2686
|
+
}
|
|
2687
|
+
try {
|
|
2688
|
+
const price = await pricing.quote(body.data);
|
|
2689
|
+
return { ok: true, parsedBody: body.data, price };
|
|
2690
|
+
} catch (err) {
|
|
2691
|
+
return {
|
|
2692
|
+
ok: false,
|
|
2693
|
+
response: fail(
|
|
2694
|
+
ctx,
|
|
2695
|
+
errorStatus(err, 500),
|
|
2696
|
+
errorMessage(err, "Price calculation failed"),
|
|
2697
|
+
body.data
|
|
2698
|
+
)
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// src/pipeline/flows/static/static-request.ts
|
|
2704
|
+
async function runStaticRequestFlow(args) {
|
|
2705
|
+
const { ctx, strategy, verifyOutcome, account, body, price, result } = args;
|
|
2706
|
+
const { deps, routeEntry } = ctx;
|
|
2006
2707
|
const settleScope = {
|
|
2007
2708
|
wallet: verifyOutcome.wallet,
|
|
2008
2709
|
account,
|
|
2009
|
-
body
|
|
2710
|
+
body,
|
|
2010
2711
|
payment: verifyOutcome.payment,
|
|
2011
2712
|
response: result.response,
|
|
2012
2713
|
rawResult: result.rawResult,
|
|
@@ -2016,44 +2717,198 @@ async function runPaidFlow(ctx) {
|
|
|
2016
2717
|
if (result.response.status >= 400) {
|
|
2017
2718
|
const settledScope = settleScope;
|
|
2018
2719
|
await runSettledHandlerError(ctx, settledScope);
|
|
2019
|
-
return finalize(ctx, result.response, result.rawResult, body
|
|
2720
|
+
return finalize(ctx, result.response, result.rawResult, body);
|
|
2020
2721
|
}
|
|
2021
|
-
return
|
|
2722
|
+
return settleAndFinalizeRequest({
|
|
2022
2723
|
ctx,
|
|
2023
|
-
strategy
|
|
2724
|
+
strategy,
|
|
2024
2725
|
verifyOutcome,
|
|
2025
2726
|
scope: settleScope,
|
|
2026
2727
|
rawResult: result.rawResult,
|
|
2027
|
-
body
|
|
2728
|
+
body,
|
|
2729
|
+
billedAmount: price
|
|
2028
2730
|
});
|
|
2029
2731
|
}
|
|
2030
2732
|
if (result.response.status >= 400) {
|
|
2031
|
-
return finalize(ctx, result.response, result.rawResult, body
|
|
2733
|
+
return finalize(ctx, result.response, result.rawResult, body);
|
|
2032
2734
|
}
|
|
2033
2735
|
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2034
2736
|
if (beforeErr) return beforeErr;
|
|
2035
|
-
return
|
|
2737
|
+
return settleAndFinalizeRequest({
|
|
2036
2738
|
ctx,
|
|
2037
|
-
strategy
|
|
2739
|
+
strategy,
|
|
2038
2740
|
verifyOutcome,
|
|
2039
2741
|
scope: settleScope,
|
|
2040
2742
|
rawResult: result.rawResult,
|
|
2041
|
-
body
|
|
2743
|
+
body,
|
|
2744
|
+
billedAmount: price,
|
|
2042
2745
|
onSettleError: async (error, failMessage) => {
|
|
2043
2746
|
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2044
2747
|
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2045
2748
|
level: "critical",
|
|
2046
|
-
message: `${
|
|
2749
|
+
message: `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
|
|
2047
2750
|
route: routeEntry.key
|
|
2048
2751
|
});
|
|
2049
2752
|
}
|
|
2050
2753
|
});
|
|
2051
2754
|
}
|
|
2052
2755
|
|
|
2756
|
+
// src/pipeline/flows/static/static-paid.ts
|
|
2757
|
+
async function runStaticPaidFlow(ctx) {
|
|
2758
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
2759
|
+
const apiKeyGate = await runApiKeyGate(ctx);
|
|
2760
|
+
if (!apiKeyGate.ok) return apiKeyGate.response;
|
|
2761
|
+
const { account } = apiKeyGate;
|
|
2762
|
+
const pricing = selectPricing(routeEntry.pricing, {
|
|
2763
|
+
alert: report,
|
|
2764
|
+
maxPrice: routeEntry.maxPrice,
|
|
2765
|
+
minPrice: routeEntry.minPrice,
|
|
2766
|
+
route: routeEntry.key
|
|
2767
|
+
});
|
|
2768
|
+
const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
|
|
2769
|
+
const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
|
|
2770
|
+
if (!earlyResolution.ok) return earlyResolution.response;
|
|
2771
|
+
const { earlyBody } = earlyResolution;
|
|
2772
|
+
const siwxFastPath = await trySiwxFastPath(ctx, account);
|
|
2773
|
+
if (siwxFastPath) return siwxFastPath;
|
|
2774
|
+
if (!incomingStrategy) {
|
|
2775
|
+
const initError = protocolInitError(routeEntry, deps);
|
|
2776
|
+
if (initError) return fail(ctx, 500, initError);
|
|
2777
|
+
return build402(ctx, pricing, earlyBody);
|
|
2778
|
+
}
|
|
2779
|
+
const bodyAndPrice = await resolveStaticBodyAndPrice({ ctx, pricing });
|
|
2780
|
+
if (!bodyAndPrice.ok) return bodyAndPrice.response;
|
|
2781
|
+
const { parsedBody, price } = bodyAndPrice;
|
|
2782
|
+
const verifyOutcome = await incomingStrategy.verify({
|
|
2783
|
+
request,
|
|
2784
|
+
body: parsedBody,
|
|
2785
|
+
price,
|
|
2786
|
+
routeEntry,
|
|
2787
|
+
deps,
|
|
2788
|
+
report
|
|
2789
|
+
});
|
|
2790
|
+
if (verifyOutcome.ok === false) {
|
|
2791
|
+
if (verifyOutcome.kind === "config") {
|
|
2792
|
+
return fail(ctx, 500, verifyOutcome.message, parsedBody);
|
|
2793
|
+
}
|
|
2794
|
+
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
2795
|
+
}
|
|
2796
|
+
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2797
|
+
firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
|
|
2798
|
+
protocol: incomingStrategy.protocol,
|
|
2799
|
+
payer: verifyOutcome.wallet,
|
|
2800
|
+
amount: price,
|
|
2801
|
+
network: verifyOutcome.payment.network
|
|
2802
|
+
});
|
|
2803
|
+
const result = await invokePaidStatic(
|
|
2804
|
+
ctx,
|
|
2805
|
+
verifyOutcome.wallet,
|
|
2806
|
+
account,
|
|
2807
|
+
parsedBody,
|
|
2808
|
+
verifyOutcome.payment
|
|
2809
|
+
);
|
|
2810
|
+
return runStaticRequestFlow({
|
|
2811
|
+
ctx,
|
|
2812
|
+
strategy: incomingStrategy,
|
|
2813
|
+
verifyOutcome,
|
|
2814
|
+
account,
|
|
2815
|
+
body: parsedBody,
|
|
2816
|
+
price,
|
|
2817
|
+
result
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// src/pipeline/flows/paid.ts
|
|
2822
|
+
async function runPaidFlow(ctx) {
|
|
2823
|
+
const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
|
|
2824
|
+
switch (dynamicPrice) {
|
|
2825
|
+
case true:
|
|
2826
|
+
return runDynamicPaidFlow(ctx);
|
|
2827
|
+
case false:
|
|
2828
|
+
return runStaticPaidFlow(ctx);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2053
2832
|
// src/pipeline/flows/siwx-only.ts
|
|
2054
|
-
import { NextResponse as
|
|
2833
|
+
import { NextResponse as NextResponse7 } from "next/server";
|
|
2834
|
+
|
|
2835
|
+
// src/kv-store/client.ts
|
|
2836
|
+
function restKvStore(url, token) {
|
|
2837
|
+
const base = url.replace(/\/+$/, "");
|
|
2838
|
+
const authHeader = { Authorization: `Bearer ${token}` };
|
|
2839
|
+
const jsonHeaders = { ...authHeader, "Content-Type": "application/json" };
|
|
2840
|
+
async function exec(command) {
|
|
2841
|
+
const res = await fetch(base, {
|
|
2842
|
+
method: "POST",
|
|
2843
|
+
headers: jsonHeaders,
|
|
2844
|
+
body: JSON.stringify(command)
|
|
2845
|
+
});
|
|
2846
|
+
if (!res.ok) {
|
|
2847
|
+
throw new Error(`[kv-store] ${command[0]} ${command[1] ?? ""}: ${res.status}`);
|
|
2848
|
+
}
|
|
2849
|
+
const body = await res.json();
|
|
2850
|
+
if (body.error) throw new Error(`[kv-store] ${command[0]}: ${body.error}`);
|
|
2851
|
+
return body.result ?? null;
|
|
2852
|
+
}
|
|
2853
|
+
async function get(key) {
|
|
2854
|
+
const res = await fetch(`${base}/get/${encodeURIComponent(key)}`, { headers: authHeader });
|
|
2855
|
+
if (!res.ok) throw new Error(`[kv-store] GET ${key}: ${res.status}`);
|
|
2856
|
+
const { result } = await res.json();
|
|
2857
|
+
return result ?? null;
|
|
2858
|
+
}
|
|
2859
|
+
async function set(key, value) {
|
|
2860
|
+
await exec(["SET", key, JSON.stringify(value)]);
|
|
2861
|
+
}
|
|
2862
|
+
async function del(key) {
|
|
2863
|
+
await exec(["DEL", key]);
|
|
2864
|
+
}
|
|
2865
|
+
async function setNxEx(key, value, ttlSeconds) {
|
|
2866
|
+
const result = await exec(["SET", key, JSON.stringify(value), "EX", ttlSeconds, "NX"]);
|
|
2867
|
+
return result === "OK";
|
|
2868
|
+
}
|
|
2869
|
+
async function sadd(key, member) {
|
|
2870
|
+
await exec(["SADD", key, member]);
|
|
2871
|
+
}
|
|
2872
|
+
async function sismember(key, member) {
|
|
2873
|
+
const result = await exec(["SISMEMBER", key, member]);
|
|
2874
|
+
return result === 1;
|
|
2875
|
+
}
|
|
2876
|
+
async function update(key, fn) {
|
|
2877
|
+
const current = await get(key);
|
|
2878
|
+
const change = fn(current);
|
|
2879
|
+
if (change.op === "set") await set(key, change.value);
|
|
2880
|
+
if (change.op === "delete") await del(key);
|
|
2881
|
+
return change.result;
|
|
2882
|
+
}
|
|
2883
|
+
return { get, set, del, setNxEx, sadd, sismember, update };
|
|
2884
|
+
}
|
|
2885
|
+
function isRestConfig(input) {
|
|
2886
|
+
return typeof input === "object" && input !== null && typeof input.url === "string" && typeof input.token === "string" && typeof input.get !== "function";
|
|
2887
|
+
}
|
|
2888
|
+
function resolveKvStore(input, env = process.env) {
|
|
2889
|
+
if (input) {
|
|
2890
|
+
if (isRestConfig(input)) return restKvStore(input.url, input.token);
|
|
2891
|
+
return input;
|
|
2892
|
+
}
|
|
2893
|
+
const url = env.KV_REST_API_URL;
|
|
2894
|
+
const token = env.KV_REST_API_TOKEN;
|
|
2895
|
+
if (url && token) return restKvStore(url, token);
|
|
2896
|
+
return void 0;
|
|
2897
|
+
}
|
|
2898
|
+
function withPrefix(kv, prefix) {
|
|
2899
|
+
const k = (key) => `${prefix}${key}`;
|
|
2900
|
+
return {
|
|
2901
|
+
get: (key) => kv.get(k(key)),
|
|
2902
|
+
set: (key, value) => kv.set(k(key), value),
|
|
2903
|
+
del: (key) => kv.del(k(key)),
|
|
2904
|
+
setNxEx: (key, value, ttl) => kv.setNxEx(k(key), value, ttl),
|
|
2905
|
+
sadd: (key, member) => kv.sadd(k(key), member),
|
|
2906
|
+
sismember: (key, member) => kv.sismember(k(key), member),
|
|
2907
|
+
update: (key, fn) => kv.update(k(key), fn)
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2055
2910
|
|
|
2056
|
-
// src/
|
|
2911
|
+
// src/kv-store/nonce.ts
|
|
2057
2912
|
var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
|
|
2058
2913
|
var MemoryNonceStore = class {
|
|
2059
2914
|
seen = /* @__PURE__ */ new Map();
|
|
@@ -2070,48 +2925,59 @@ var MemoryNonceStore = class {
|
|
|
2070
2925
|
}
|
|
2071
2926
|
}
|
|
2072
2927
|
};
|
|
2073
|
-
function
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
)
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2928
|
+
function createKvNonceStore(kv, options) {
|
|
2929
|
+
const prefix = options?.prefix ?? "siwx:nonce:";
|
|
2930
|
+
const ttlSeconds = Math.ceil((options?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
2931
|
+
return {
|
|
2932
|
+
async check(nonce) {
|
|
2933
|
+
return kv.setNxEx(`${prefix}${nonce}`, 1, ttlSeconds);
|
|
2934
|
+
}
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
// src/kv-store/entitlement.ts
|
|
2939
|
+
var MemoryEntitlementStore = class {
|
|
2940
|
+
routeToWallets = /* @__PURE__ */ new Map();
|
|
2941
|
+
async has(route, wallet) {
|
|
2942
|
+
const wallets = this.routeToWallets.get(route);
|
|
2943
|
+
if (!wallets) return false;
|
|
2944
|
+
return wallets.has(normalizeWalletAddress(wallet));
|
|
2085
2945
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2946
|
+
async grant(route, wallet) {
|
|
2947
|
+
const normalized = normalizeWalletAddress(wallet);
|
|
2948
|
+
let wallets = this.routeToWallets.get(route);
|
|
2949
|
+
if (!wallets) {
|
|
2950
|
+
wallets = /* @__PURE__ */ new Set();
|
|
2951
|
+
this.routeToWallets.set(route, wallets);
|
|
2952
|
+
}
|
|
2953
|
+
wallets.add(normalized);
|
|
2088
2954
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
}
|
|
2093
|
-
function createRedisNonceStore(client, opts) {
|
|
2094
|
-
const prefix = opts?.prefix ?? "siwx:nonce:";
|
|
2095
|
-
const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
2096
|
-
const clientType = detectRedisClientType(client);
|
|
2955
|
+
};
|
|
2956
|
+
function createKvEntitlementStore(kv, options) {
|
|
2957
|
+
const prefix = options?.prefix ?? "siwx:ent:";
|
|
2097
2958
|
return {
|
|
2098
|
-
async
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
return result !== null;
|
|
2104
|
-
}
|
|
2105
|
-
if (clientType === "ioredis") {
|
|
2106
|
-
const redis = client;
|
|
2107
|
-
const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
|
|
2108
|
-
return result === "OK";
|
|
2109
|
-
}
|
|
2110
|
-
throw new Error("Unknown Redis client type");
|
|
2959
|
+
async has(route, wallet) {
|
|
2960
|
+
return kv.sismember(`${prefix}${route}`, normalizeWalletAddress(wallet));
|
|
2961
|
+
},
|
|
2962
|
+
async grant(route, wallet) {
|
|
2963
|
+
await kv.sadd(`${prefix}${route}`, normalizeWalletAddress(wallet));
|
|
2111
2964
|
}
|
|
2112
2965
|
};
|
|
2113
2966
|
}
|
|
2114
2967
|
|
|
2968
|
+
// src/kv-store/mpp.ts
|
|
2969
|
+
async function createKvMppStore(kv, options) {
|
|
2970
|
+
const prefix = options?.prefix ?? "mpp:";
|
|
2971
|
+
const namespaced = withPrefix(kv, prefix);
|
|
2972
|
+
const { Store: StoreNs } = await import("mppx");
|
|
2973
|
+
return StoreNs.upstash({
|
|
2974
|
+
get: (key) => namespaced.get(key),
|
|
2975
|
+
set: (key, value) => namespaced.set(key, value),
|
|
2976
|
+
del: (key) => namespaced.del(key),
|
|
2977
|
+
update: (key, fn) => namespaced.update(key, fn)
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2115
2981
|
// src/protocols/mpp/siwx-mode.ts
|
|
2116
2982
|
import { Credential as Credential2 } from "mppx";
|
|
2117
2983
|
async function verifyMppSiwx(request, mppx) {
|
|
@@ -2130,12 +2996,11 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2130
2996
|
const { request, routeEntry, deps } = ctx;
|
|
2131
2997
|
if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get(HEADERS.SIWX)) {
|
|
2132
2998
|
const earlyClone = request.clone();
|
|
2133
|
-
const earlyBody = await parseBody(
|
|
2999
|
+
const earlyBody = await parseBody(ctx, earlyClone);
|
|
2134
3000
|
if (earlyBody.ok) {
|
|
2135
3001
|
const validateErr = await runValidate(ctx, earlyBody.data);
|
|
2136
3002
|
if (validateErr) return validateErr;
|
|
2137
3003
|
} else {
|
|
2138
|
-
firePluginResponse(ctx, earlyBody.response);
|
|
2139
3004
|
return earlyBody.response;
|
|
2140
3005
|
}
|
|
2141
3006
|
}
|
|
@@ -2147,11 +3012,7 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2147
3012
|
mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
|
|
2148
3013
|
} catch (err) {
|
|
2149
3014
|
const message = err instanceof Error ? err.message : String(err);
|
|
2150
|
-
|
|
2151
|
-
level: "critical",
|
|
2152
|
-
message: `MPP SIWX verification failed: ${message}`,
|
|
2153
|
-
route: routeEntry.key
|
|
2154
|
-
});
|
|
3015
|
+
ctx.report("critical", `MPP SIWX verification failed: ${message}`);
|
|
2155
3016
|
return fail(ctx, 500, `MPP SIWX verification failed: ${message}`);
|
|
2156
3017
|
}
|
|
2157
3018
|
if (mppSiwxResult.valid) {
|
|
@@ -2173,7 +3034,7 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2173
3034
|
}
|
|
2174
3035
|
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
2175
3036
|
if (!siwx.valid) {
|
|
2176
|
-
const response =
|
|
3037
|
+
const response = NextResponse7.json(
|
|
2177
3038
|
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
2178
3039
|
{ status: 402 }
|
|
2179
3040
|
);
|
|
@@ -2223,7 +3084,6 @@ async function buildSiwxChallenge(ctx) {
|
|
|
2223
3084
|
extensions: {
|
|
2224
3085
|
"sign-in-with-x": {
|
|
2225
3086
|
info: siwxInfo,
|
|
2226
|
-
// Required by MCP tools at the top level for chain detection.
|
|
2227
3087
|
supportedChains,
|
|
2228
3088
|
...siwxSchema ? { schema: siwxSchema } : {}
|
|
2229
3089
|
}
|
|
@@ -2234,13 +3094,12 @@ async function buildSiwxChallenge(ctx) {
|
|
|
2234
3094
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
2235
3095
|
encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
2236
3096
|
} catch (err) {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
});
|
|
3097
|
+
ctx.report(
|
|
3098
|
+
"warn",
|
|
3099
|
+
`SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3100
|
+
);
|
|
2242
3101
|
}
|
|
2243
|
-
const response = new
|
|
3102
|
+
const response = new NextResponse7(JSON.stringify(paymentRequired), {
|
|
2244
3103
|
status: 402,
|
|
2245
3104
|
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
2246
3105
|
});
|
|
@@ -2346,6 +3205,12 @@ var RouteBuilder = class {
|
|
|
2346
3205
|
/** @internal */
|
|
2347
3206
|
_minPrice;
|
|
2348
3207
|
/** @internal */
|
|
3208
|
+
_dynamicPrice = false;
|
|
3209
|
+
/** @internal */
|
|
3210
|
+
_tickCost;
|
|
3211
|
+
/** @internal */
|
|
3212
|
+
_unitType;
|
|
3213
|
+
/** @internal */
|
|
2349
3214
|
_payTo;
|
|
2350
3215
|
/** @internal */
|
|
2351
3216
|
_bodySchema;
|
|
@@ -2390,7 +3255,8 @@ var RouteBuilder = class {
|
|
|
2390
3255
|
next._protocols = [...this._protocols];
|
|
2391
3256
|
return next;
|
|
2392
3257
|
}
|
|
2393
|
-
paid(
|
|
3258
|
+
paid(pricingOrOptions, options) {
|
|
3259
|
+
const { pricing, resolvedOptions } = resolvePaidArgs(this._key, pricingOrOptions, options);
|
|
2394
3260
|
if (this._authMode === "unprotected") {
|
|
2395
3261
|
throw new Error(
|
|
2396
3262
|
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
@@ -2404,16 +3270,24 @@ var RouteBuilder = class {
|
|
|
2404
3270
|
const next = this.fork();
|
|
2405
3271
|
next._authMode = "paid";
|
|
2406
3272
|
next._pricing = pricing;
|
|
2407
|
-
if (
|
|
2408
|
-
next._protocols = [...
|
|
3273
|
+
if (resolvedOptions?.protocols) {
|
|
3274
|
+
next._protocols = [...resolvedOptions.protocols];
|
|
2409
3275
|
} else if (next._protocols.length === 0) {
|
|
2410
3276
|
next._protocols = ["x402"];
|
|
2411
3277
|
}
|
|
2412
|
-
if (
|
|
2413
|
-
if (
|
|
2414
|
-
if (
|
|
2415
|
-
if (
|
|
3278
|
+
if (resolvedOptions?.maxPrice) next._maxPrice = resolvedOptions.maxPrice;
|
|
3279
|
+
if (resolvedOptions?.minPrice) next._minPrice = resolvedOptions.minPrice;
|
|
3280
|
+
if (resolvedOptions?.payTo) next._payTo = resolvedOptions.payTo;
|
|
3281
|
+
if (resolvedOptions?.mpp) next._mppInfo = resolvedOptions.mpp;
|
|
3282
|
+
if (resolvedOptions?.dynamic) next._dynamicPrice = true;
|
|
3283
|
+
if (resolvedOptions?.tickCost) next._tickCost = resolvedOptions.tickCost;
|
|
3284
|
+
if (resolvedOptions?.unitType) next._unitType = resolvedOptions.unitType;
|
|
2416
3285
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
3286
|
+
if (next._dynamicPrice) {
|
|
3287
|
+
throw new Error(
|
|
3288
|
+
`route '${this._key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
|
|
3289
|
+
);
|
|
3290
|
+
}
|
|
2417
3291
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
2418
3292
|
if (!tierKey) {
|
|
2419
3293
|
throw new Error(`route '${this._key}': tier key cannot be empty`);
|
|
@@ -2426,16 +3300,40 @@ var RouteBuilder = class {
|
|
|
2426
3300
|
}
|
|
2427
3301
|
}
|
|
2428
3302
|
}
|
|
2429
|
-
if (
|
|
2430
|
-
const parsed = parseFloat(
|
|
3303
|
+
if (resolvedOptions?.maxPrice !== void 0) {
|
|
3304
|
+
const parsed = parseFloat(resolvedOptions.maxPrice);
|
|
2431
3305
|
if (isNaN(parsed) || parsed <= 0) {
|
|
2432
3306
|
throw new Error(
|
|
2433
|
-
`route '${this._key}': maxPrice '${
|
|
3307
|
+
`route '${this._key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
|
|
2434
3308
|
);
|
|
2435
3309
|
}
|
|
2436
3310
|
}
|
|
3311
|
+
if (resolvedOptions?.tickCost !== void 0) {
|
|
3312
|
+
const parsed = parseFloat(resolvedOptions.tickCost);
|
|
3313
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
3314
|
+
throw new Error(
|
|
3315
|
+
`route '${this._key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
if (next._dynamicPrice && !next._maxPrice) {
|
|
3320
|
+
throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires maxPrice`);
|
|
3321
|
+
}
|
|
3322
|
+
if (next._dynamicPrice && !next._tickCost) {
|
|
3323
|
+
throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires tickCost`);
|
|
3324
|
+
}
|
|
2437
3325
|
return next;
|
|
2438
3326
|
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Require Sign-In-with-X wallet identity on this route — clients prove
|
|
3329
|
+
* control of a wallet via a signed challenge. Combine with `.paid()` to gate
|
|
3330
|
+
* a paid route on a verified wallet identity.
|
|
3331
|
+
*
|
|
3332
|
+
* @example
|
|
3333
|
+
* ```ts
|
|
3334
|
+
* router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
|
|
3335
|
+
* ```
|
|
3336
|
+
*/
|
|
2439
3337
|
siwx() {
|
|
2440
3338
|
if (this._authMode === "unprotected") {
|
|
2441
3339
|
throw new Error(
|
|
@@ -2458,6 +3356,19 @@ var RouteBuilder = class {
|
|
|
2458
3356
|
next._protocols = [];
|
|
2459
3357
|
return next;
|
|
2460
3358
|
}
|
|
3359
|
+
/**
|
|
3360
|
+
* Require an `X-API-Key` header (or `Authorization: Bearer <key>`); the
|
|
3361
|
+
* resolver returns the account record, or `null` for 401. Composes with
|
|
3362
|
+
* `.paid()` — key is checked first, payment second.
|
|
3363
|
+
*
|
|
3364
|
+
* @example
|
|
3365
|
+
* ```ts
|
|
3366
|
+
* router
|
|
3367
|
+
* .route('admin/users')
|
|
3368
|
+
* .apiKey(async (key) => db.admin.findByKey(key))
|
|
3369
|
+
* .handler(async ({ account }) => db.user.list(account.orgId));
|
|
3370
|
+
* ```
|
|
3371
|
+
*/
|
|
2461
3372
|
apiKey(resolver) {
|
|
2462
3373
|
if (this._siwxEnabled) {
|
|
2463
3374
|
throw new Error(
|
|
@@ -2469,6 +3380,15 @@ var RouteBuilder = class {
|
|
|
2469
3380
|
next._apiKeyResolver = resolver;
|
|
2470
3381
|
return next;
|
|
2471
3382
|
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Mark the route as public — no auth, no payment, no SIWX. The handler
|
|
3385
|
+
* receives `null` for `wallet`, `payment`, and `account`.
|
|
3386
|
+
*
|
|
3387
|
+
* @example
|
|
3388
|
+
* ```ts
|
|
3389
|
+
* router.route('health').unprotected().handler(async () => ({ status: 'ok' }));
|
|
3390
|
+
* ```
|
|
3391
|
+
*/
|
|
2472
3392
|
unprotected() {
|
|
2473
3393
|
if (this._authMode && this._authMode !== "unprotected") {
|
|
2474
3394
|
throw new Error(
|
|
@@ -2485,60 +3405,82 @@ var RouteBuilder = class {
|
|
|
2485
3405
|
next._protocols = [];
|
|
2486
3406
|
return next;
|
|
2487
3407
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
3408
|
+
/**
|
|
3409
|
+
* Tag the route with an upstream provider for discovery and provider-side
|
|
3410
|
+
* monitoring. The provider name and config surface in `well-known` and
|
|
3411
|
+
* OpenAPI output.
|
|
3412
|
+
*
|
|
3413
|
+
* @example
|
|
3414
|
+
* ```ts
|
|
3415
|
+
* router
|
|
3416
|
+
* .route('search')
|
|
3417
|
+
* .paid('0.01')
|
|
3418
|
+
* .provider('exa', { quotaPerMonth: 1000 })
|
|
3419
|
+
* .handler(handler);
|
|
3420
|
+
* ```
|
|
3421
|
+
*/
|
|
2491
3422
|
provider(name, config) {
|
|
2492
3423
|
const next = this.fork();
|
|
2493
3424
|
next._providerName = name;
|
|
2494
3425
|
next._providerConfig = config ?? {};
|
|
2495
3426
|
return next;
|
|
2496
3427
|
}
|
|
2497
|
-
|
|
3428
|
+
/**
|
|
3429
|
+
* Declare the request body's Zod schema. Parsed body is typed as `ctx.body`
|
|
3430
|
+
* in the handler. Use `.inputExample()` to attach a discovery example.
|
|
3431
|
+
*
|
|
3432
|
+
* @example
|
|
3433
|
+
* ```ts
|
|
3434
|
+
* .body(z.object({ query: z.string() }))
|
|
3435
|
+
* .handler(async ({ body }) => search(body.query));
|
|
3436
|
+
* ```
|
|
3437
|
+
*/
|
|
3438
|
+
body(schema) {
|
|
2498
3439
|
const next = this.fork();
|
|
2499
3440
|
next._bodySchema = schema;
|
|
2500
|
-
if (example !== void 0) {
|
|
2501
|
-
next._inputExample = example;
|
|
2502
|
-
next._hasInputExample = true;
|
|
2503
|
-
}
|
|
2504
3441
|
return next;
|
|
2505
3442
|
}
|
|
2506
|
-
|
|
3443
|
+
/**
|
|
3444
|
+
* Declare a query-string Zod schema and switch the route to `GET`. Parsed
|
|
3445
|
+
* query is typed as `ctx.query` in the handler. Use `.inputExample()` to
|
|
3446
|
+
* attach a discovery example.
|
|
3447
|
+
*
|
|
3448
|
+
* @example
|
|
3449
|
+
* ```ts
|
|
3450
|
+
* .query(z.object({ id: z.string() }))
|
|
3451
|
+
* .handler(async ({ query }) => getById(query.id));
|
|
3452
|
+
* ```
|
|
3453
|
+
*/
|
|
3454
|
+
query(schema) {
|
|
2507
3455
|
const next = this.fork();
|
|
2508
3456
|
next._querySchema = schema;
|
|
2509
|
-
if (example !== void 0) {
|
|
2510
|
-
next._inputExample = example;
|
|
2511
|
-
next._hasInputExample = true;
|
|
2512
|
-
}
|
|
2513
3457
|
next._method = "GET";
|
|
2514
3458
|
return next;
|
|
2515
3459
|
}
|
|
2516
|
-
|
|
3460
|
+
/**
|
|
3461
|
+
* Declare the response output's Zod schema for OpenAPI generation. The
|
|
3462
|
+
* runtime does not validate handler return values — use Zod's `.parse()`
|
|
3463
|
+
* inside the handler if strict output validation is required. Use
|
|
3464
|
+
* `.outputExample()` to attach a discovery example.
|
|
3465
|
+
*
|
|
3466
|
+
* @example
|
|
3467
|
+
* ```ts
|
|
3468
|
+
* .output(z.object({ result: z.string() }))
|
|
3469
|
+
* .handler(async () => ({ result: 'ok' }));
|
|
3470
|
+
* ```
|
|
3471
|
+
*/
|
|
3472
|
+
output(schema) {
|
|
2517
3473
|
const next = this.fork();
|
|
2518
3474
|
next._outputSchema = schema;
|
|
2519
|
-
if (example !== void 0) {
|
|
2520
|
-
next._outputExample = example;
|
|
2521
|
-
next._hasOutputExample = true;
|
|
2522
|
-
}
|
|
2523
3475
|
return next;
|
|
2524
3476
|
}
|
|
2525
3477
|
/**
|
|
2526
|
-
*
|
|
2527
|
-
*
|
|
2528
|
-
* Optional. When provided, the example is validated against the request schema
|
|
2529
|
-
* at route registration and embedded in the bazaar discovery extension so
|
|
2530
|
-
* indexers can advertise a working sample call.
|
|
2531
|
-
*
|
|
2532
|
-
* For the common case, pass the example directly to `.body(schema, example)` or
|
|
2533
|
-
* `.query(schema, example)` instead.
|
|
3478
|
+
* Attach an example of the request body or query for discovery output,
|
|
3479
|
+
* validated against the registered schema at registration.
|
|
2534
3480
|
*
|
|
2535
3481
|
* @example
|
|
2536
3482
|
* ```ts
|
|
2537
|
-
*
|
|
2538
|
-
* .paid('0.01')
|
|
2539
|
-
* .body(z.object({ q: z.string() }))
|
|
2540
|
-
* .inputExample({ q: 'hello world' })
|
|
2541
|
-
* .handler(async ({ body }) => { ... });
|
|
3483
|
+
* .body(searchSchema).inputExample({ query: 'cats' });
|
|
2542
3484
|
* ```
|
|
2543
3485
|
*/
|
|
2544
3486
|
inputExample(example) {
|
|
@@ -2548,32 +3490,12 @@ var RouteBuilder = class {
|
|
|
2548
3490
|
return next;
|
|
2549
3491
|
}
|
|
2550
3492
|
/**
|
|
2551
|
-
*
|
|
2552
|
-
*
|
|
2553
|
-
* Optional. When provided, the example is validated against the output schema
|
|
2554
|
-
* at route registration and embedded in the bazaar discovery extension so
|
|
2555
|
-
* indexers can advertise the response shape.
|
|
2556
|
-
*
|
|
2557
|
-
* For the common case, pass the example directly to `.output(schema, example)` instead.
|
|
2558
|
-
*
|
|
2559
|
-
* Accepts any JSON value (objects, arrays, or primitives) — top-level array
|
|
2560
|
-
* or primitive responses (e.g. `z.array(...)`) are supported alongside the
|
|
2561
|
-
* common object case.
|
|
3493
|
+
* Attach an example response for discovery output, validated against the
|
|
3494
|
+
* registered output schema at registration.
|
|
2562
3495
|
*
|
|
2563
3496
|
* @example
|
|
2564
3497
|
* ```ts
|
|
2565
|
-
*
|
|
2566
|
-
* .paid('0.01')
|
|
2567
|
-
* .output(z.object({ results: z.array(z.string()) }))
|
|
2568
|
-
* .outputExample({ results: ['a', 'b'] })
|
|
2569
|
-
* .handler(async () => { ... });
|
|
2570
|
-
*
|
|
2571
|
-
* // Top-level array response
|
|
2572
|
-
* router.route('chains')
|
|
2573
|
-
* .paid('0.01')
|
|
2574
|
-
* .output(z.array(z.object({ name: z.string() })))
|
|
2575
|
-
* .outputExample([{ name: 'Ethereum' }])
|
|
2576
|
-
* .handler(async () => { ... });
|
|
3498
|
+
* .output(resultSchema).outputExample({ result: 'ok' });
|
|
2577
3499
|
* ```
|
|
2578
3500
|
*/
|
|
2579
3501
|
outputExample(example) {
|
|
@@ -2582,43 +3504,60 @@ var RouteBuilder = class {
|
|
|
2582
3504
|
next._hasOutputExample = true;
|
|
2583
3505
|
return next;
|
|
2584
3506
|
}
|
|
3507
|
+
/**
|
|
3508
|
+
* Set a human-readable summary of the route. Surfaces in OpenAPI,
|
|
3509
|
+
* `well-known`, and `llms.txt` discovery output.
|
|
3510
|
+
*
|
|
3511
|
+
* @example
|
|
3512
|
+
* ```ts
|
|
3513
|
+
* .description('Search indexed web pages by full-text query');
|
|
3514
|
+
* ```
|
|
3515
|
+
*/
|
|
2585
3516
|
description(text) {
|
|
2586
3517
|
const next = this.fork();
|
|
2587
3518
|
next._description = text;
|
|
2588
3519
|
return next;
|
|
2589
3520
|
}
|
|
3521
|
+
/**
|
|
3522
|
+
* Override the URL path advertised in discovery output. Defaults to the
|
|
3523
|
+
* registry key passed to `.route()`.
|
|
3524
|
+
*
|
|
3525
|
+
* @example
|
|
3526
|
+
* ```ts
|
|
3527
|
+
* router.route('search').path('/v2/search').handler(handler);
|
|
3528
|
+
* ```
|
|
3529
|
+
*/
|
|
2590
3530
|
path(p) {
|
|
2591
3531
|
const next = this.fork();
|
|
2592
3532
|
next._path = p;
|
|
2593
3533
|
return next;
|
|
2594
3534
|
}
|
|
3535
|
+
/**
|
|
3536
|
+
* Override the HTTP method advertised in discovery. Defaults to `POST`, or
|
|
3537
|
+
* `GET` when `.query()` has been called.
|
|
3538
|
+
*
|
|
3539
|
+
* @example
|
|
3540
|
+
* ```ts
|
|
3541
|
+
* router.route('items/delete').method('DELETE').handler(handler);
|
|
3542
|
+
* ```
|
|
3543
|
+
*/
|
|
2595
3544
|
method(m) {
|
|
2596
3545
|
const next = this.fork();
|
|
2597
3546
|
next._method = m;
|
|
2598
3547
|
return next;
|
|
2599
3548
|
}
|
|
2600
|
-
// -------------------------------------------------------------------------
|
|
2601
|
-
// Pre-payment validation
|
|
2602
|
-
// -------------------------------------------------------------------------
|
|
2603
3549
|
/**
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
2607
|
-
*
|
|
2608
|
-
* Requires `.body()` — call `.body()` before `.validate()` for type inference.
|
|
3550
|
+
* Run validation against the parsed body before the 402 challenge. Throw
|
|
3551
|
+
* `Object.assign(new Error('...'), { status })` to reject with a custom
|
|
3552
|
+
* status code; defaults to 400. Requires `.body()` to be called first.
|
|
2609
3553
|
*
|
|
2610
3554
|
* @example
|
|
2611
|
-
* ```
|
|
2612
|
-
*
|
|
2613
|
-
* .
|
|
2614
|
-
*
|
|
2615
|
-
*
|
|
2616
|
-
*
|
|
2617
|
-
* if (await isDomainTaken(body.domain)) {
|
|
2618
|
-
* throw Object.assign(new Error('Domain taken'), { status: 409 });
|
|
2619
|
-
* }
|
|
2620
|
-
* })
|
|
2621
|
-
* .handler(async ({ body }) => { ... });
|
|
3555
|
+
* ```ts
|
|
3556
|
+
* .body(RegisterSchema).validate(async (body) => {
|
|
3557
|
+
* if (await isTaken(body.name)) {
|
|
3558
|
+
* throw Object.assign(new Error('taken'), { status: 409 });
|
|
3559
|
+
* }
|
|
3560
|
+
* });
|
|
2622
3561
|
* ```
|
|
2623
3562
|
*/
|
|
2624
3563
|
validate(fn) {
|
|
@@ -2626,27 +3565,64 @@ var RouteBuilder = class {
|
|
|
2626
3565
|
next._validateFn = fn;
|
|
2627
3566
|
return next;
|
|
2628
3567
|
}
|
|
2629
|
-
// -------------------------------------------------------------------------
|
|
2630
|
-
// Settlement lifecycle
|
|
2631
|
-
// -------------------------------------------------------------------------
|
|
2632
3568
|
/**
|
|
2633
|
-
*
|
|
3569
|
+
* Hook into the settlement lifecycle. `beforeSettle` runs after the handler
|
|
3570
|
+
* succeeds but before on-chain settlement and can cancel the charge;
|
|
3571
|
+
* `afterSettle` runs after settlement completes (success or failure).
|
|
2634
3572
|
*
|
|
2635
|
-
*
|
|
2636
|
-
*
|
|
2637
|
-
*
|
|
2638
|
-
*
|
|
3573
|
+
* @example
|
|
3574
|
+
* ```ts
|
|
3575
|
+
* .settlement({
|
|
3576
|
+
* beforeSettle: ({ result }) => (result.refund ? 'skip' : 'continue'),
|
|
3577
|
+
* afterSettle: ({ tx }) => analytics.track('settled', { tx }),
|
|
3578
|
+
* });
|
|
3579
|
+
* ```
|
|
2639
3580
|
*/
|
|
2640
3581
|
settlement(lifecycle) {
|
|
2641
3582
|
const next = this.fork();
|
|
2642
3583
|
next._settlement = lifecycle;
|
|
2643
3584
|
return next;
|
|
2644
3585
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
3586
|
+
/**
|
|
3587
|
+
* Register the request handler and return the Next.js route function. The
|
|
3588
|
+
* handler receives a typed context and may return a value (serialized to
|
|
3589
|
+
* JSON), a raw `Response`, or throw an `HttpError` for a non-2xx status.
|
|
3590
|
+
*
|
|
3591
|
+
* @example
|
|
3592
|
+
* ```ts
|
|
3593
|
+
* export const POST = router
|
|
3594
|
+
* .route('search')
|
|
3595
|
+
* .paid('0.01')
|
|
3596
|
+
* .body(schema)
|
|
3597
|
+
* .handler(async ({ body, wallet }) => searchService(body, wallet));
|
|
3598
|
+
* ```
|
|
3599
|
+
*/
|
|
2648
3600
|
handler(fn) {
|
|
2649
|
-
|
|
3601
|
+
return this.register(fn, false);
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* Register a streaming handler (`async function*`) and return the Next.js
|
|
3605
|
+
* route function. Each `charge()` call bills one tick (`tickCost` USDC) up
|
|
3606
|
+
* to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
|
|
3607
|
+
*
|
|
3608
|
+
* @example
|
|
3609
|
+
* ```ts
|
|
3610
|
+
* export const POST = router
|
|
3611
|
+
* .route('llm/stream')
|
|
3612
|
+
* .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
|
|
3613
|
+
* .body(schema)
|
|
3614
|
+
* .stream(async function* ({ body, charge }) {
|
|
3615
|
+
* for await (const token of streamLLM(body.prompt)) {
|
|
3616
|
+
* await charge();
|
|
3617
|
+
* yield token;
|
|
3618
|
+
* }
|
|
3619
|
+
* });
|
|
3620
|
+
* ```
|
|
3621
|
+
*/
|
|
3622
|
+
stream(fn) {
|
|
3623
|
+
return this.register(fn, true);
|
|
3624
|
+
}
|
|
3625
|
+
register(handlerFn, streaming) {
|
|
2650
3626
|
if (!this._authMode) {
|
|
2651
3627
|
throw new Error(
|
|
2652
3628
|
`route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
@@ -2660,6 +3636,26 @@ var RouteBuilder = class {
|
|
|
2660
3636
|
if (this._settlement && !this._pricing) {
|
|
2661
3637
|
throw new Error(`route '${this._key}': .settlement() requires a paid route`);
|
|
2662
3638
|
}
|
|
3639
|
+
if (this._dynamicPrice && this._protocols.includes("x402")) {
|
|
3640
|
+
const hasUpto = this._deps.x402Accepts.some((accept) => accept.scheme === "upto");
|
|
3641
|
+
if (!hasUpto) {
|
|
3642
|
+
throw new Error(
|
|
3643
|
+
`route '${this._key}': .paid({ dynamic: true }) on an x402 route requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
if (this._dynamicPrice && this._protocols.includes("mpp")) {
|
|
3648
|
+
if (!this._deps.mppSessionConfig) {
|
|
3649
|
+
throw new Error(
|
|
3650
|
+
`route '${this._key}': .paid({ dynamic: true }) on an MPP route requires session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
|
|
3651
|
+
);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
if (streaming && !this._dynamicPrice) {
|
|
3655
|
+
throw new Error(
|
|
3656
|
+
`route '${this._key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
|
|
3657
|
+
);
|
|
3658
|
+
}
|
|
2663
3659
|
validateExamples(
|
|
2664
3660
|
this._key,
|
|
2665
3661
|
this._bodySchema,
|
|
@@ -2675,6 +3671,8 @@ var RouteBuilder = class {
|
|
|
2675
3671
|
authMode: this._authMode,
|
|
2676
3672
|
siwxEnabled: this._siwxEnabled,
|
|
2677
3673
|
pricing: this._pricing,
|
|
3674
|
+
dynamicPrice: this._dynamicPrice ? true : void 0,
|
|
3675
|
+
streaming: streaming ? true : void 0,
|
|
2678
3676
|
protocols: this._protocols,
|
|
2679
3677
|
bodySchema: this._bodySchema,
|
|
2680
3678
|
querySchema: this._querySchema,
|
|
@@ -2692,81 +3690,28 @@ var RouteBuilder = class {
|
|
|
2692
3690
|
providerConfig: this._providerConfig,
|
|
2693
3691
|
validateFn: this._validateFn,
|
|
2694
3692
|
settlement: this._settlement,
|
|
2695
|
-
mppInfo: this._mppInfo
|
|
3693
|
+
mppInfo: this._mppInfo,
|
|
3694
|
+
tickCost: this._tickCost,
|
|
3695
|
+
unitType: this._unitType
|
|
2696
3696
|
};
|
|
2697
3697
|
this._registry.register(entry);
|
|
2698
|
-
return createRequestHandler(
|
|
2699
|
-
entry,
|
|
2700
|
-
handlerFn,
|
|
2701
|
-
this._deps
|
|
2702
|
-
);
|
|
3698
|
+
return createRequestHandler(entry, handlerFn, this._deps);
|
|
2703
3699
|
}
|
|
2704
3700
|
};
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
if (!wallets) return false;
|
|
2712
|
-
return wallets.has(normalizeWalletAddress(wallet));
|
|
2713
|
-
}
|
|
2714
|
-
async grant(route, wallet) {
|
|
2715
|
-
const normalized = normalizeWalletAddress(wallet);
|
|
2716
|
-
let wallets = this.routeToWallets.get(route);
|
|
2717
|
-
if (!wallets) {
|
|
2718
|
-
wallets = /* @__PURE__ */ new Set();
|
|
2719
|
-
this.routeToWallets.set(route, wallets);
|
|
3701
|
+
function resolvePaidArgs(routeKey, pricingOrOptions, options) {
|
|
3702
|
+
const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
|
|
3703
|
+
if (isHandlerDynamicShape) {
|
|
3704
|
+
const opts = pricingOrOptions;
|
|
3705
|
+
if (!opts.maxPrice) {
|
|
3706
|
+
throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
|
|
2720
3707
|
}
|
|
2721
|
-
|
|
3708
|
+
return { pricing: opts.maxPrice, resolvedOptions: opts };
|
|
2722
3709
|
}
|
|
2723
|
-
};
|
|
2724
|
-
function detectRedisClientType2(client) {
|
|
2725
|
-
if (!client || typeof client !== "object") {
|
|
2726
|
-
throw new Error(
|
|
2727
|
-
"createRedisEntitlementStore requires a Redis client. Supported: @upstash/redis, ioredis."
|
|
2728
|
-
);
|
|
2729
|
-
}
|
|
2730
|
-
if ("options" in client && "status" in client) return "ioredis";
|
|
2731
|
-
const constructor = client.constructor?.name;
|
|
2732
|
-
if (constructor === "Redis" && "url" in client) return "upstash";
|
|
2733
|
-
if (typeof client.sadd === "function" && typeof client.sismember === "function") {
|
|
2734
|
-
return "upstash";
|
|
2735
|
-
}
|
|
2736
|
-
throw new Error("Unrecognized Redis client for entitlement store.");
|
|
2737
|
-
}
|
|
2738
|
-
function createRedisEntitlementStore(client, options) {
|
|
2739
|
-
const clientType = detectRedisClientType2(client);
|
|
2740
|
-
const prefix = options?.prefix ?? "siwx:entitlement:";
|
|
2741
|
-
return {
|
|
2742
|
-
async has(route, wallet) {
|
|
2743
|
-
const key = `${prefix}${route}`;
|
|
2744
|
-
const normalized = normalizeWalletAddress(wallet);
|
|
2745
|
-
if (clientType === "upstash") {
|
|
2746
|
-
const redis2 = client;
|
|
2747
|
-
const result2 = await redis2.sismember(key, normalized);
|
|
2748
|
-
return result2 === 1 || result2 === true;
|
|
2749
|
-
}
|
|
2750
|
-
const redis = client;
|
|
2751
|
-
const result = await redis.sismember(key, normalized);
|
|
2752
|
-
return result === 1;
|
|
2753
|
-
},
|
|
2754
|
-
async grant(route, wallet) {
|
|
2755
|
-
const key = `${prefix}${route}`;
|
|
2756
|
-
const normalized = normalizeWalletAddress(wallet);
|
|
2757
|
-
if (clientType === "upstash") {
|
|
2758
|
-
const redis2 = client;
|
|
2759
|
-
await redis2.sadd(key, normalized);
|
|
2760
|
-
return;
|
|
2761
|
-
}
|
|
2762
|
-
const redis = client;
|
|
2763
|
-
await redis.sadd(key, normalized);
|
|
2764
|
-
}
|
|
2765
|
-
};
|
|
3710
|
+
return { pricing: pricingOrOptions, resolvedOptions: options };
|
|
2766
3711
|
}
|
|
2767
3712
|
|
|
2768
3713
|
// src/discovery/well-known.ts
|
|
2769
|
-
import { NextResponse as
|
|
3714
|
+
import { NextResponse as NextResponse8 } from "next/server";
|
|
2770
3715
|
|
|
2771
3716
|
// src/discovery/utils/guidance.ts
|
|
2772
3717
|
async function resolveGuidance(discovery) {
|
|
@@ -2810,7 +3755,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2810
3755
|
if (instructions) {
|
|
2811
3756
|
body.instructions = instructions;
|
|
2812
3757
|
}
|
|
2813
|
-
return
|
|
3758
|
+
return NextResponse8.json(body, {
|
|
2814
3759
|
headers: {
|
|
2815
3760
|
"Access-Control-Allow-Origin": "*",
|
|
2816
3761
|
"Access-Control-Allow-Methods": "GET",
|
|
@@ -2828,13 +3773,13 @@ function toDiscoveryResource(method, url, mode) {
|
|
|
2828
3773
|
|
|
2829
3774
|
// src/discovery/openapi.ts
|
|
2830
3775
|
init_constants();
|
|
2831
|
-
import { NextResponse as
|
|
3776
|
+
import { NextResponse as NextResponse9 } from "next/server";
|
|
2832
3777
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
2833
3778
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
2834
3779
|
let cached = null;
|
|
2835
3780
|
let validated = false;
|
|
2836
3781
|
return async (_request) => {
|
|
2837
|
-
if (cached) return
|
|
3782
|
+
if (cached) return NextResponse9.json(cached);
|
|
2838
3783
|
if (!validated && pricesKeys) {
|
|
2839
3784
|
registry.validate(pricesKeys);
|
|
2840
3785
|
validated = true;
|
|
@@ -2897,7 +3842,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2897
3842
|
paths
|
|
2898
3843
|
};
|
|
2899
3844
|
cached = createDocument(openApiDocument);
|
|
2900
|
-
return
|
|
3845
|
+
return NextResponse9.json(cached);
|
|
2901
3846
|
};
|
|
2902
3847
|
}
|
|
2903
3848
|
function deriveTag(routeKey) {
|
|
@@ -2959,97 +3904,336 @@ function buildOperation(routeKey, entry, tag) {
|
|
|
2959
3904
|
};
|
|
2960
3905
|
}
|
|
2961
3906
|
return {
|
|
2962
|
-
operation,
|
|
2963
|
-
requiresSiwxScheme,
|
|
2964
|
-
requiresApiKeyScheme
|
|
3907
|
+
operation,
|
|
3908
|
+
requiresSiwxScheme,
|
|
3909
|
+
requiresApiKeyScheme
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
function toProtocolObject(protocol, mppInfo) {
|
|
3913
|
+
if (protocol === "mpp") {
|
|
3914
|
+
return {
|
|
3915
|
+
mpp: {
|
|
3916
|
+
method: mppInfo?.method ?? "tempo",
|
|
3917
|
+
intent: mppInfo?.intent ?? "charge",
|
|
3918
|
+
currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
|
|
3919
|
+
}
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
return { [protocol]: {} };
|
|
3923
|
+
}
|
|
3924
|
+
function buildPricingInfo(entry) {
|
|
3925
|
+
if (!entry.pricing) return void 0;
|
|
3926
|
+
if (typeof entry.pricing === "string") {
|
|
3927
|
+
return {
|
|
3928
|
+
price: { mode: "fixed", currency: "USD", amount: entry.pricing }
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
if (typeof entry.pricing === "function") {
|
|
3932
|
+
return {
|
|
3933
|
+
price: {
|
|
3934
|
+
mode: "dynamic",
|
|
3935
|
+
currency: "USD",
|
|
3936
|
+
min: entry.minPrice ?? "0",
|
|
3937
|
+
max: entry.maxPrice ?? "0"
|
|
3938
|
+
}
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
if ("tiers" in entry.pricing) {
|
|
3942
|
+
const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
|
|
3943
|
+
const min = Math.min(...tierPrices);
|
|
3944
|
+
const max = Math.max(...tierPrices);
|
|
3945
|
+
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
3946
|
+
if (min === max) {
|
|
3947
|
+
return {
|
|
3948
|
+
price: { mode: "fixed", currency: "USD", amount: String(min) }
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
return {
|
|
3952
|
+
price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
return {
|
|
3956
|
+
price: {
|
|
3957
|
+
mode: "dynamic",
|
|
3958
|
+
currency: "USD",
|
|
3959
|
+
min: "0",
|
|
3960
|
+
max: entry.maxPrice ?? "0"
|
|
3961
|
+
}
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
return void 0;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
// src/discovery/llms-txt.ts
|
|
3968
|
+
import { NextResponse as NextResponse10 } from "next/server";
|
|
3969
|
+
function createLlmsTxtHandler(discovery) {
|
|
3970
|
+
return async (_request) => {
|
|
3971
|
+
const guidance = await resolveGuidance(discovery) ?? "";
|
|
3972
|
+
return new NextResponse10(guidance, {
|
|
3973
|
+
headers: {
|
|
3974
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
3975
|
+
"Access-Control-Allow-Origin": "*"
|
|
3976
|
+
}
|
|
3977
|
+
});
|
|
3978
|
+
};
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
// src/index.ts
|
|
3982
|
+
init_accepts();
|
|
3983
|
+
init_constants();
|
|
3984
|
+
|
|
3985
|
+
// src/config/error.ts
|
|
3986
|
+
var RouterConfigError = class extends Error {
|
|
3987
|
+
issues;
|
|
3988
|
+
constructor(issues) {
|
|
3989
|
+
super(formatRouterConfigIssues(issues));
|
|
3990
|
+
this.name = "RouterConfigError";
|
|
3991
|
+
this.issues = issues;
|
|
3992
|
+
}
|
|
3993
|
+
};
|
|
3994
|
+
function formatRouterConfigIssues(issues) {
|
|
3995
|
+
return issues.map((issue) => issue.message).join("\n");
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
// src/config/validators/x402.ts
|
|
3999
|
+
init_accepts();
|
|
4000
|
+
|
|
4001
|
+
// src/config/validators/shared.ts
|
|
4002
|
+
init_evm();
|
|
4003
|
+
init_solana();
|
|
4004
|
+
init_accepts();
|
|
4005
|
+
function isEvmAddress(value) {
|
|
4006
|
+
return /^0x[a-fA-F0-9]{40}$/.test(value);
|
|
4007
|
+
}
|
|
4008
|
+
function isEvmPrivateKey(value) {
|
|
4009
|
+
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
4010
|
+
}
|
|
4011
|
+
function isSupportedX402Network(network) {
|
|
4012
|
+
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
4013
|
+
}
|
|
4014
|
+
function findPlaceholderPayee(values) {
|
|
4015
|
+
return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
|
|
4016
|
+
}
|
|
4017
|
+
function usesDefaultEvmFacilitator(config) {
|
|
4018
|
+
return getConfiguredX402Networks(config).some(
|
|
4019
|
+
(network) => typeof network === "string" && isEvmNetwork(network)
|
|
4020
|
+
) && config.x402?.facilitators?.evm === void 0;
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
// src/config/validators/x402.ts
|
|
4024
|
+
var CHECKS = [
|
|
4025
|
+
checkAcceptNetworkPresent,
|
|
4026
|
+
checkAcceptNetworkSupported,
|
|
4027
|
+
checkNonExactRequiresAsset,
|
|
4028
|
+
checkDecimalsAreValid,
|
|
4029
|
+
checkPayee,
|
|
4030
|
+
checkPlaceholderPayeeAddress,
|
|
4031
|
+
checkCdpKeys
|
|
4032
|
+
];
|
|
4033
|
+
function validateX402Config(config, env, options) {
|
|
4034
|
+
const accepts = getConfiguredX402Accepts(config);
|
|
4035
|
+
if (accepts.length === 0) {
|
|
4036
|
+
return [
|
|
4037
|
+
{
|
|
4038
|
+
code: "missing_x402_accepts",
|
|
4039
|
+
protocol: "x402",
|
|
4040
|
+
message: "x402 requires at least one accept configuration."
|
|
4041
|
+
}
|
|
4042
|
+
];
|
|
4043
|
+
}
|
|
4044
|
+
const args = { config, accepts, env, options };
|
|
4045
|
+
return CHECKS.map((check) => check(args)).filter(
|
|
4046
|
+
(issue) => issue !== null
|
|
4047
|
+
);
|
|
4048
|
+
}
|
|
4049
|
+
function checkAcceptNetworkPresent({ accepts }) {
|
|
4050
|
+
return accepts.some((accept) => !accept.network) ? { code: "missing_x402_network", protocol: "x402", message: "x402 accepts require a network." } : null;
|
|
4051
|
+
}
|
|
4052
|
+
function checkAcceptNetworkSupported({ accepts }) {
|
|
4053
|
+
const unsupported = accepts.find(
|
|
4054
|
+
(accept) => accept.network && !isSupportedX402Network(accept.network)
|
|
4055
|
+
);
|
|
4056
|
+
if (!unsupported) return null;
|
|
4057
|
+
return {
|
|
4058
|
+
code: "unsupported_x402_network",
|
|
4059
|
+
protocol: "x402",
|
|
4060
|
+
message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
function checkNonExactRequiresAsset({ accepts }) {
|
|
4064
|
+
return accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset) ? {
|
|
4065
|
+
code: "missing_x402_asset",
|
|
4066
|
+
protocol: "x402",
|
|
4067
|
+
message: "non-exact x402 accepts require an asset."
|
|
4068
|
+
} : null;
|
|
4069
|
+
}
|
|
4070
|
+
function checkDecimalsAreValid({ accepts }) {
|
|
4071
|
+
const invalid = accepts.find(
|
|
4072
|
+
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
4073
|
+
);
|
|
4074
|
+
if (!invalid) return null;
|
|
4075
|
+
return {
|
|
4076
|
+
code: "invalid_x402_decimals",
|
|
4077
|
+
protocol: "x402",
|
|
4078
|
+
message: "x402 accept decimals must be a non-negative integer."
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
function checkPayee({ config, accepts }) {
|
|
4082
|
+
if (config.payeeAddress) return null;
|
|
4083
|
+
return accepts.some((accept) => !accept.payTo) ? {
|
|
4084
|
+
code: "missing_x402_payee",
|
|
4085
|
+
protocol: "x402",
|
|
4086
|
+
message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
4087
|
+
} : null;
|
|
4088
|
+
}
|
|
4089
|
+
function checkPlaceholderPayeeAddress({
|
|
4090
|
+
config,
|
|
4091
|
+
accepts
|
|
4092
|
+
}) {
|
|
4093
|
+
const placeholder = findPlaceholderPayee([
|
|
4094
|
+
config.payeeAddress,
|
|
4095
|
+
...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
|
|
4096
|
+
]);
|
|
4097
|
+
if (!placeholder) return null;
|
|
4098
|
+
return {
|
|
4099
|
+
code: "placeholder_payee",
|
|
4100
|
+
protocol: "x402",
|
|
4101
|
+
message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
function checkCdpKeys({ config, env, options }) {
|
|
4105
|
+
if (options.requireCdpKeys === false) return null;
|
|
4106
|
+
if (!usesDefaultEvmFacilitator(config)) return null;
|
|
4107
|
+
const missing = [
|
|
4108
|
+
env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
|
|
4109
|
+
env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
|
|
4110
|
+
].filter(Boolean);
|
|
4111
|
+
if (missing.length === 0) return null;
|
|
4112
|
+
return {
|
|
4113
|
+
code: "missing_cdp_keys",
|
|
4114
|
+
protocol: "x402",
|
|
4115
|
+
message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
// src/config/validators/mpp.ts
|
|
4120
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
4121
|
+
var CHECKS2 = [
|
|
4122
|
+
checkSecretKey,
|
|
4123
|
+
checkCurrency,
|
|
4124
|
+
checkRecipient,
|
|
4125
|
+
checkPlaceholderRecipient,
|
|
4126
|
+
checkRpcUrl,
|
|
4127
|
+
checkFeePayerKey,
|
|
4128
|
+
checkOperatorKey,
|
|
4129
|
+
checkOperatorMatchesFeePayer
|
|
4130
|
+
];
|
|
4131
|
+
function validateMppConfig(config, env) {
|
|
4132
|
+
const mpp = config.mpp;
|
|
4133
|
+
if (!mpp) {
|
|
4134
|
+
return [
|
|
4135
|
+
{
|
|
4136
|
+
code: "missing_mpp_config",
|
|
4137
|
+
protocol: "mpp",
|
|
4138
|
+
message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
4139
|
+
}
|
|
4140
|
+
];
|
|
4141
|
+
}
|
|
4142
|
+
const args = { config, mpp, env };
|
|
4143
|
+
return CHECKS2.map((check) => check(args)).filter(
|
|
4144
|
+
(issue) => issue !== null
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
function checkSecretKey({ mpp }) {
|
|
4148
|
+
if (mpp.secretKey) return null;
|
|
4149
|
+
return {
|
|
4150
|
+
code: "missing_mpp_secret_key",
|
|
4151
|
+
protocol: "mpp",
|
|
4152
|
+
message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
2965
4153
|
};
|
|
2966
4154
|
}
|
|
2967
|
-
function
|
|
2968
|
-
if (
|
|
4155
|
+
function checkCurrency({ mpp }) {
|
|
4156
|
+
if (!mpp.currency) {
|
|
2969
4157
|
return {
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
|
|
2974
|
-
}
|
|
4158
|
+
code: "missing_mpp_currency",
|
|
4159
|
+
protocol: "mpp",
|
|
4160
|
+
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
2975
4161
|
};
|
|
2976
4162
|
}
|
|
2977
|
-
|
|
2978
|
-
}
|
|
2979
|
-
function buildPricingInfo(entry) {
|
|
2980
|
-
if (!entry.pricing) return void 0;
|
|
2981
|
-
if (typeof entry.pricing === "string") {
|
|
4163
|
+
if (!isEvmAddress(mpp.currency)) {
|
|
2982
4164
|
return {
|
|
2983
|
-
|
|
4165
|
+
code: "invalid_mpp_currency",
|
|
4166
|
+
protocol: "mpp",
|
|
4167
|
+
message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
|
|
2984
4168
|
};
|
|
2985
4169
|
}
|
|
2986
|
-
|
|
4170
|
+
return null;
|
|
4171
|
+
}
|
|
4172
|
+
function checkRecipient({ config, mpp }) {
|
|
4173
|
+
const recipient = mpp.recipient ?? config.payeeAddress;
|
|
4174
|
+
if (!recipient) {
|
|
2987
4175
|
return {
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
min: entry.minPrice ?? "0",
|
|
2992
|
-
max: entry.maxPrice ?? "0"
|
|
2993
|
-
}
|
|
4176
|
+
code: "missing_mpp_recipient",
|
|
4177
|
+
protocol: "mpp",
|
|
4178
|
+
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
2994
4179
|
};
|
|
2995
4180
|
}
|
|
2996
|
-
if (
|
|
2997
|
-
const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
|
|
2998
|
-
const min = Math.min(...tierPrices);
|
|
2999
|
-
const max = Math.max(...tierPrices);
|
|
3000
|
-
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
3001
|
-
if (min === max) {
|
|
3002
|
-
return {
|
|
3003
|
-
price: { mode: "fixed", currency: "USD", amount: String(min) }
|
|
3004
|
-
};
|
|
3005
|
-
}
|
|
3006
|
-
return {
|
|
3007
|
-
price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
|
|
3008
|
-
};
|
|
3009
|
-
}
|
|
4181
|
+
if (!isEvmAddress(recipient)) {
|
|
3010
4182
|
return {
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
min: "0",
|
|
3015
|
-
max: entry.maxPrice ?? "0"
|
|
3016
|
-
}
|
|
4183
|
+
code: "invalid_mpp_recipient",
|
|
4184
|
+
protocol: "mpp",
|
|
4185
|
+
message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
3017
4186
|
};
|
|
3018
4187
|
}
|
|
3019
|
-
return
|
|
4188
|
+
return null;
|
|
3020
4189
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
4190
|
+
function checkPlaceholderRecipient({ config, mpp }) {
|
|
4191
|
+
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
4192
|
+
if (!placeholder) return null;
|
|
4193
|
+
return {
|
|
4194
|
+
code: "placeholder_payee",
|
|
4195
|
+
protocol: "mpp",
|
|
4196
|
+
message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
function checkRpcUrl({ mpp, env }) {
|
|
4200
|
+
if (mpp.rpcUrl ?? env.TEMPO_RPC_URL) return null;
|
|
4201
|
+
return {
|
|
4202
|
+
code: "missing_mpp_rpc_url",
|
|
4203
|
+
protocol: "mpp",
|
|
4204
|
+
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4207
|
+
function checkFeePayerKey({ mpp }) {
|
|
4208
|
+
if (!mpp.feePayerKey || isEvmPrivateKey(mpp.feePayerKey)) return null;
|
|
4209
|
+
return {
|
|
4210
|
+
code: "invalid_mpp_fee_payer_key",
|
|
4211
|
+
protocol: "mpp",
|
|
4212
|
+
message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
4215
|
+
function checkOperatorKey({ mpp }) {
|
|
4216
|
+
if (!mpp.operatorKey || isEvmPrivateKey(mpp.operatorKey)) return null;
|
|
4217
|
+
return {
|
|
4218
|
+
code: "invalid_mpp_operator_key",
|
|
4219
|
+
protocol: "mpp",
|
|
4220
|
+
message: "MPP operatorKey must be a 0x-prefixed 32-byte EVM private key."
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
function checkOperatorMatchesFeePayer({ mpp }) {
|
|
4224
|
+
if (!mpp.operatorKey || !mpp.feePayerKey) return null;
|
|
4225
|
+
if (!isEvmPrivateKey(mpp.operatorKey) || !isEvmPrivateKey(mpp.feePayerKey)) return null;
|
|
4226
|
+
const opAddr = privateKeyToAccount(mpp.operatorKey).address.toLowerCase();
|
|
4227
|
+
const fpAddr = privateKeyToAccount(mpp.feePayerKey).address.toLowerCase();
|
|
4228
|
+
if (opAddr !== fpAddr) return null;
|
|
4229
|
+
return {
|
|
4230
|
+
code: "mpp_operator_equals_fee_payer",
|
|
4231
|
+
protocol: "mpp",
|
|
4232
|
+
message: `MPP operatorKey and feePayerKey resolve to the same address (${opAddr}). Tempo rejects fee-delegated txs with sender === feePayer, so channel close/settle would fail at runtime. Either use two distinct wallets, or omit feePayerKey to disable gas sponsorship (clients then pay their own gas).`
|
|
3033
4233
|
};
|
|
3034
4234
|
}
|
|
3035
4235
|
|
|
3036
|
-
// src/
|
|
3037
|
-
init_accepts();
|
|
3038
|
-
init_constants();
|
|
3039
|
-
|
|
3040
|
-
// src/config.ts
|
|
3041
|
-
init_constants();
|
|
3042
|
-
init_evm();
|
|
3043
|
-
init_solana();
|
|
3044
|
-
init_accepts();
|
|
3045
|
-
var RouterConfigError = class extends Error {
|
|
3046
|
-
issues;
|
|
3047
|
-
constructor(issues) {
|
|
3048
|
-
super(formatRouterConfigIssues(issues));
|
|
3049
|
-
this.name = "RouterConfigError";
|
|
3050
|
-
this.issues = issues;
|
|
3051
|
-
}
|
|
3052
|
-
};
|
|
4236
|
+
// src/config/validate.ts
|
|
3053
4237
|
function validateRouterConfig(config, options = {}) {
|
|
3054
4238
|
const issues = getRouterConfigIssues(config, options);
|
|
3055
4239
|
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
@@ -3078,9 +4262,9 @@ function getRouterConfigIssues(config, options = {}) {
|
|
|
3078
4262
|
}
|
|
3079
4263
|
return issues;
|
|
3080
4264
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
4265
|
+
|
|
4266
|
+
// src/config/env.ts
|
|
4267
|
+
init_constants();
|
|
3084
4268
|
function mppFromEnv(env, options = {}) {
|
|
3085
4269
|
const secretKey = env.MPP_SECRET_KEY;
|
|
3086
4270
|
const currency = env.MPP_CURRENCY;
|
|
@@ -3111,8 +4295,7 @@ function mppFromEnv(env, options = {}) {
|
|
|
3111
4295
|
currency,
|
|
3112
4296
|
rpcUrl,
|
|
3113
4297
|
...options.recipient ? { recipient: options.recipient } : {},
|
|
3114
|
-
...feePayerKey ? { feePayerKey } : {}
|
|
3115
|
-
...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
|
|
4298
|
+
...feePayerKey ? { feePayerKey } : {}
|
|
3116
4299
|
};
|
|
3117
4300
|
}
|
|
3118
4301
|
function x402AcceptsFromEnv(env, options = {}) {
|
|
@@ -3142,189 +4325,145 @@ function x402AcceptsFromEnv(env, options = {}) {
|
|
|
3142
4325
|
function paidOptionsForProtocols(protocols) {
|
|
3143
4326
|
return { protocols: [...protocols] };
|
|
3144
4327
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
if (
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
code: "missing_x402_network",
|
|
3160
|
-
protocol: "x402",
|
|
3161
|
-
message: "x402 accepts require a network."
|
|
3162
|
-
});
|
|
3163
|
-
}
|
|
3164
|
-
const unsupported = accepts.find(
|
|
3165
|
-
(accept) => accept.network && !isSupportedX402Network(accept.network)
|
|
3166
|
-
);
|
|
3167
|
-
if (unsupported) {
|
|
3168
|
-
issues.push({
|
|
3169
|
-
code: "unsupported_x402_network",
|
|
3170
|
-
protocol: "x402",
|
|
3171
|
-
message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
3172
|
-
});
|
|
3173
|
-
}
|
|
3174
|
-
const missingAsset = accepts.find(
|
|
3175
|
-
(accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
|
|
3176
|
-
);
|
|
3177
|
-
if (missingAsset) {
|
|
3178
|
-
issues.push({
|
|
3179
|
-
code: "missing_x402_asset",
|
|
3180
|
-
protocol: "x402",
|
|
3181
|
-
message: "non-exact x402 accepts require an asset."
|
|
3182
|
-
});
|
|
3183
|
-
}
|
|
3184
|
-
const invalidDecimals = accepts.find(
|
|
3185
|
-
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
3186
|
-
);
|
|
3187
|
-
if (invalidDecimals) {
|
|
3188
|
-
issues.push({
|
|
3189
|
-
code: "invalid_x402_decimals",
|
|
3190
|
-
protocol: "x402",
|
|
3191
|
-
message: "x402 accept decimals must be a non-negative integer."
|
|
3192
|
-
});
|
|
3193
|
-
}
|
|
3194
|
-
if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
3195
|
-
issues.push({
|
|
3196
|
-
code: "missing_x402_payee",
|
|
3197
|
-
protocol: "x402",
|
|
3198
|
-
message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
3199
|
-
});
|
|
3200
|
-
}
|
|
3201
|
-
const placeholder = findPlaceholderPayee([
|
|
3202
|
-
config.payeeAddress,
|
|
3203
|
-
...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
|
|
3204
|
-
]);
|
|
3205
|
-
if (placeholder) {
|
|
3206
|
-
issues.push({
|
|
3207
|
-
code: "placeholder_payee",
|
|
3208
|
-
protocol: "x402",
|
|
3209
|
-
message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
3210
|
-
});
|
|
4328
|
+
|
|
4329
|
+
// src/init/x402.ts
|
|
4330
|
+
async function initX402(config, configError) {
|
|
4331
|
+
if (configError) return { initError: configError };
|
|
4332
|
+
try {
|
|
4333
|
+
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
4334
|
+
const result = await createX402Server2(config);
|
|
4335
|
+
await result.initPromise;
|
|
4336
|
+
return {
|
|
4337
|
+
server: result.server,
|
|
4338
|
+
facilitatorsByNetwork: result.facilitatorsByNetwork
|
|
4339
|
+
};
|
|
4340
|
+
} catch (err) {
|
|
4341
|
+
return { initError: err instanceof Error ? err.message : String(err) };
|
|
3211
4342
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
// src/mppx-init.ts
|
|
4346
|
+
function getMppxRequestContext(args) {
|
|
4347
|
+
const {
|
|
4348
|
+
Mppx,
|
|
4349
|
+
tempo,
|
|
4350
|
+
mppConfig,
|
|
4351
|
+
payeeAddress,
|
|
4352
|
+
getClient,
|
|
4353
|
+
feePayerAccount,
|
|
4354
|
+
resolvedStore,
|
|
4355
|
+
sessionEnabled,
|
|
4356
|
+
sharedSessionParams,
|
|
4357
|
+
realm
|
|
4358
|
+
} = args;
|
|
4359
|
+
const instance = Mppx.create({
|
|
4360
|
+
methods: [
|
|
4361
|
+
tempo.charge({
|
|
4362
|
+
currency: mppConfig.currency,
|
|
4363
|
+
recipient: mppConfig.recipient ?? payeeAddress,
|
|
4364
|
+
getClient,
|
|
4365
|
+
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
4366
|
+
...resolvedStore ? { store: resolvedStore } : {}
|
|
4367
|
+
}),
|
|
4368
|
+
...sessionEnabled ? [
|
|
4369
|
+
tempo.session({
|
|
4370
|
+
...sharedSessionParams,
|
|
4371
|
+
sse: false
|
|
4372
|
+
})
|
|
4373
|
+
] : []
|
|
4374
|
+
],
|
|
4375
|
+
secretKey: mppConfig.secretKey,
|
|
4376
|
+
realm
|
|
4377
|
+
});
|
|
4378
|
+
return instance;
|
|
4379
|
+
}
|
|
4380
|
+
function getMppxStreamingContext(args) {
|
|
4381
|
+
if (!args.sessionEnabled) return null;
|
|
4382
|
+
const { Mppx, tempo, mppConfig, sharedSessionParams, realm } = args;
|
|
4383
|
+
const instance = Mppx.create({
|
|
4384
|
+
methods: [
|
|
4385
|
+
tempo.session({
|
|
4386
|
+
...sharedSessionParams,
|
|
4387
|
+
sse: true
|
|
4388
|
+
})
|
|
4389
|
+
],
|
|
4390
|
+
secretKey: mppConfig.secretKey,
|
|
4391
|
+
realm
|
|
4392
|
+
});
|
|
4393
|
+
return instance;
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
// src/init/mpp.ts
|
|
4397
|
+
async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
|
|
4398
|
+
if (configError) return { initError: configError };
|
|
4399
|
+
if (!config.mpp) return {};
|
|
4400
|
+
try {
|
|
4401
|
+
const { Mppx, tempo } = await import("mppx/server");
|
|
4402
|
+
const { createClient, http } = await import("viem");
|
|
4403
|
+
const { tempo: tempoChain } = await import("viem/chains");
|
|
4404
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
4405
|
+
const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
4406
|
+
const tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
|
|
4407
|
+
const getClient = async () => tempoClient;
|
|
4408
|
+
const operatorAccount = config.mpp.operatorKey ? privateKeyToAccount2(config.mpp.operatorKey) : void 0;
|
|
4409
|
+
const feePayerAccount = config.mpp.feePayerKey ? privateKeyToAccount2(config.mpp.feePayerKey) : void 0;
|
|
4410
|
+
if (config.mpp.session && operatorAccount) {
|
|
4411
|
+
assertOperatorMatchesRecipient(config, operatorAccount.address);
|
|
3223
4412
|
}
|
|
4413
|
+
const resolvedStore = kvStore ? await createKvMppStore(kvStore) : void 0;
|
|
4414
|
+
const realm = new URL(resolvedBaseUrl).host;
|
|
4415
|
+
const mppConfig = config.mpp;
|
|
4416
|
+
const sessionEnabled = !!(mppConfig.session && operatorAccount);
|
|
4417
|
+
const sharedSessionParams = {
|
|
4418
|
+
currency: mppConfig.currency,
|
|
4419
|
+
decimals: 6,
|
|
4420
|
+
recipient: mppConfig.recipient ?? config.payeeAddress,
|
|
4421
|
+
getClient,
|
|
4422
|
+
...operatorAccount ? { account: operatorAccount } : {},
|
|
4423
|
+
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
4424
|
+
...resolvedStore ? { store: resolvedStore } : {}
|
|
4425
|
+
};
|
|
4426
|
+
const mppxArgs = {
|
|
4427
|
+
Mppx,
|
|
4428
|
+
tempo,
|
|
4429
|
+
mppConfig,
|
|
4430
|
+
payeeAddress: config.payeeAddress ?? "",
|
|
4431
|
+
getClient,
|
|
4432
|
+
feePayerAccount,
|
|
4433
|
+
resolvedStore,
|
|
4434
|
+
sessionEnabled,
|
|
4435
|
+
sharedSessionParams,
|
|
4436
|
+
realm
|
|
4437
|
+
};
|
|
4438
|
+
const primary = getMppxRequestContext(mppxArgs);
|
|
4439
|
+
const streaming = getMppxStreamingContext(mppxArgs);
|
|
4440
|
+
const mppx = {
|
|
4441
|
+
charge: primary.charge,
|
|
4442
|
+
...primary.session ? { sessionRequest: primary.session } : {},
|
|
4443
|
+
...streaming?.session ? { sessionStream: streaming.session } : {}
|
|
4444
|
+
};
|
|
4445
|
+
return { mppx, tempoClient };
|
|
4446
|
+
} catch (err) {
|
|
4447
|
+
return { initError: err instanceof Error ? err.message : String(err) };
|
|
3224
4448
|
}
|
|
3225
|
-
return issues;
|
|
3226
4449
|
}
|
|
3227
|
-
function
|
|
3228
|
-
const
|
|
3229
|
-
const
|
|
3230
|
-
if (
|
|
3231
|
-
|
|
3232
|
-
{
|
|
3233
|
-
|
|
3234
|
-
protocol: "mpp",
|
|
3235
|
-
message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
3236
|
-
}
|
|
3237
|
-
];
|
|
3238
|
-
}
|
|
3239
|
-
if (!mpp.secretKey) {
|
|
3240
|
-
issues.push({
|
|
3241
|
-
code: "missing_mpp_secret_key",
|
|
3242
|
-
protocol: "mpp",
|
|
3243
|
-
message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
3244
|
-
});
|
|
3245
|
-
}
|
|
3246
|
-
if (!mpp.currency) {
|
|
3247
|
-
issues.push({
|
|
3248
|
-
code: "missing_mpp_currency",
|
|
3249
|
-
protocol: "mpp",
|
|
3250
|
-
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
3251
|
-
});
|
|
3252
|
-
} else if (!isEvmAddress(mpp.currency)) {
|
|
3253
|
-
issues.push({
|
|
3254
|
-
code: "invalid_mpp_currency",
|
|
3255
|
-
protocol: "mpp",
|
|
3256
|
-
message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
|
|
3257
|
-
});
|
|
3258
|
-
}
|
|
3259
|
-
const mppRecipient = mpp.recipient ?? config.payeeAddress;
|
|
3260
|
-
if (!mppRecipient) {
|
|
3261
|
-
issues.push({
|
|
3262
|
-
code: "missing_mpp_recipient",
|
|
3263
|
-
protocol: "mpp",
|
|
3264
|
-
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
3265
|
-
});
|
|
3266
|
-
} else if (!isEvmAddress(mppRecipient)) {
|
|
3267
|
-
issues.push({
|
|
3268
|
-
code: "invalid_mpp_recipient",
|
|
3269
|
-
protocol: "mpp",
|
|
3270
|
-
message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
3271
|
-
});
|
|
3272
|
-
}
|
|
3273
|
-
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
3274
|
-
if (placeholder) {
|
|
3275
|
-
issues.push({
|
|
3276
|
-
code: "placeholder_payee",
|
|
3277
|
-
protocol: "mpp",
|
|
3278
|
-
message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
3279
|
-
});
|
|
3280
|
-
}
|
|
3281
|
-
if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
|
|
3282
|
-
issues.push({
|
|
3283
|
-
code: "missing_mpp_rpc_url",
|
|
3284
|
-
protocol: "mpp",
|
|
3285
|
-
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
3286
|
-
});
|
|
3287
|
-
}
|
|
3288
|
-
if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
|
|
3289
|
-
issues.push({
|
|
3290
|
-
code: "invalid_mpp_fee_payer_key",
|
|
3291
|
-
protocol: "mpp",
|
|
3292
|
-
message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
3293
|
-
});
|
|
3294
|
-
}
|
|
3295
|
-
if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
|
|
3296
|
-
issues.push({
|
|
3297
|
-
code: "missing_mpp_default_store_env",
|
|
3298
|
-
protocol: "mpp",
|
|
3299
|
-
message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
3300
|
-
});
|
|
4450
|
+
function assertOperatorMatchesRecipient(config, operatorAddress) {
|
|
4451
|
+
const recipient = (config.mpp?.recipient ?? config.payeeAddress)?.toLowerCase();
|
|
4452
|
+
const opAddr = operatorAddress.toLowerCase();
|
|
4453
|
+
if (recipient && opAddr !== recipient) {
|
|
4454
|
+
throw new Error(
|
|
4455
|
+
`MPP session config mismatch: operator address ${operatorAddress} must equal recipient/payee ${recipient}. mppx's channel-close handler asserts sender === payee. Set mpp.operatorKey to the private key for ${recipient}, or set mpp.recipient/payeeAddress to ${operatorAddress}.`
|
|
4456
|
+
);
|
|
3301
4457
|
}
|
|
3302
|
-
return issues;
|
|
3303
|
-
}
|
|
3304
|
-
function usesDefaultEvmFacilitator(config) {
|
|
3305
|
-
return getConfiguredX402Networks(config).some(
|
|
3306
|
-
(network) => typeof network === "string" && isEvmNetwork(network)
|
|
3307
|
-
) && config.x402?.facilitators?.evm === void 0;
|
|
3308
|
-
}
|
|
3309
|
-
function isSupportedX402Network(network) {
|
|
3310
|
-
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
3311
|
-
}
|
|
3312
|
-
function isEvmAddress(value) {
|
|
3313
|
-
return /^0x[a-fA-F0-9]{40}$/.test(value);
|
|
3314
|
-
}
|
|
3315
|
-
function isEvmPrivateKey(value) {
|
|
3316
|
-
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
3317
|
-
}
|
|
3318
|
-
function findPlaceholderPayee(values) {
|
|
3319
|
-
return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
|
|
3320
4458
|
}
|
|
3321
4459
|
|
|
3322
4460
|
// src/index.ts
|
|
3323
4461
|
init_constants();
|
|
3324
4462
|
function createRouter(config) {
|
|
3325
4463
|
const registry = new RouteRegistry();
|
|
3326
|
-
const
|
|
3327
|
-
const
|
|
4464
|
+
const kvStore = resolveKvStore(config.kvStore);
|
|
4465
|
+
const nonceStore = kvStore ? createKvNonceStore(kvStore) : new MemoryNonceStore();
|
|
4466
|
+
const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
|
|
3328
4467
|
const network = config.network ?? BASE_NETWORK;
|
|
3329
4468
|
const x402Accepts = getConfiguredX402Accepts(config);
|
|
3330
4469
|
const configIssues = getRouterConfigIssues(config, {
|
|
@@ -3370,69 +4509,20 @@ function createRouter(config) {
|
|
|
3370
4509
|
x402FacilitatorsByNetwork: void 0,
|
|
3371
4510
|
x402Accepts,
|
|
3372
4511
|
mppx: null,
|
|
3373
|
-
tempoClient: null
|
|
4512
|
+
tempoClient: null,
|
|
4513
|
+
mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
|
|
3374
4514
|
};
|
|
3375
4515
|
deps.initPromise = (async () => {
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
deps.x402Server = null;
|
|
3387
|
-
deps.x402InitError = err instanceof Error ? err.message : String(err);
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
if (mppConfigError) {
|
|
3391
|
-
deps.mppInitError = mppConfigError;
|
|
3392
|
-
} else if (config.mpp) {
|
|
3393
|
-
try {
|
|
3394
|
-
const { Mppx, tempo } = await import("mppx/server");
|
|
3395
|
-
const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
3396
|
-
const { createClient, http } = await import("viem");
|
|
3397
|
-
const { tempo: tempoChain } = await import("viem/chains");
|
|
3398
|
-
deps.tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
|
|
3399
|
-
const getClient = async () => deps.tempoClient;
|
|
3400
|
-
let feePayerAccount;
|
|
3401
|
-
if (config.mpp.feePayerKey) {
|
|
3402
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
3403
|
-
feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
|
|
3404
|
-
}
|
|
3405
|
-
let resolvedStore = config.mpp.store;
|
|
3406
|
-
if (!resolvedStore && config.mpp.useDefaultStore) {
|
|
3407
|
-
const kvUrl = process.env.KV_REST_API_URL;
|
|
3408
|
-
const kvToken = process.env.KV_REST_API_TOKEN;
|
|
3409
|
-
if (!kvUrl || !kvToken) {
|
|
3410
|
-
throw new Error(
|
|
3411
|
-
"mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
3412
|
-
);
|
|
3413
|
-
}
|
|
3414
|
-
const { Store } = await import("mppx");
|
|
3415
|
-
const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
|
|
3416
|
-
resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
|
|
3417
|
-
}
|
|
3418
|
-
deps.mppx = Mppx.create({
|
|
3419
|
-
methods: [
|
|
3420
|
-
tempo.charge({
|
|
3421
|
-
currency: config.mpp.currency,
|
|
3422
|
-
recipient: config.mpp.recipient ?? config.payeeAddress,
|
|
3423
|
-
getClient,
|
|
3424
|
-
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
3425
|
-
...resolvedStore ? { store: resolvedStore } : {}
|
|
3426
|
-
})
|
|
3427
|
-
],
|
|
3428
|
-
secretKey: config.mpp.secretKey,
|
|
3429
|
-
realm: new URL(resolvedBaseUrl).host
|
|
3430
|
-
});
|
|
3431
|
-
} catch (err) {
|
|
3432
|
-
deps.mppx = null;
|
|
3433
|
-
deps.mppInitError = err instanceof Error ? err.message : String(err);
|
|
3434
|
-
console.error(`[router] MPP initialization failed: ${deps.mppInitError}`);
|
|
3435
|
-
}
|
|
4516
|
+
const x402Result = await initX402(config, x402ConfigError);
|
|
4517
|
+
deps.x402Server = x402Result.server ?? null;
|
|
4518
|
+
deps.x402FacilitatorsByNetwork = x402Result.facilitatorsByNetwork;
|
|
4519
|
+
if (x402Result.initError) deps.x402InitError = x402Result.initError;
|
|
4520
|
+
const mppResult = await initMpp(config, resolvedBaseUrl, kvStore, mppConfigError);
|
|
4521
|
+
deps.mppx = mppResult.mppx ?? null;
|
|
4522
|
+
deps.tempoClient = mppResult.tempoClient ?? null;
|
|
4523
|
+
if (mppResult.initError) {
|
|
4524
|
+
deps.mppInitError = mppResult.initError;
|
|
4525
|
+
console.error(`[router] MPP initialization failed: ${mppResult.initError}`);
|
|
3436
4526
|
}
|
|
3437
4527
|
})();
|
|
3438
4528
|
const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
|
|
@@ -3513,13 +4603,15 @@ export {
|
|
|
3513
4603
|
TEMPO_USDC_CURRENCY,
|
|
3514
4604
|
ZERO_EVM_ADDRESS,
|
|
3515
4605
|
consolePlugin,
|
|
3516
|
-
|
|
3517
|
-
|
|
4606
|
+
createKvEntitlementStore,
|
|
4607
|
+
createKvMppStore,
|
|
4608
|
+
createKvNonceStore,
|
|
3518
4609
|
createRouter,
|
|
3519
4610
|
formatRouterConfigIssues,
|
|
3520
4611
|
getRouterConfigIssues,
|
|
3521
4612
|
mppFromEnv,
|
|
3522
4613
|
paidOptionsForProtocols,
|
|
3523
4614
|
validateRouterConfig,
|
|
4615
|
+
withPrefix,
|
|
3524
4616
|
x402AcceptsFromEnv
|
|
3525
4617
|
};
|