@agentcash/router 1.5.1 → 1.5.2
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/dist/index.cjs +1636 -1420
- package/dist/index.d.cts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +1636 -1420
- package/package.json +1 -1
- package/dist/client/index.cjs +0 -94
- package/dist/client/index.d.cts +0 -86
- package/dist/client/index.d.ts +0 -86
- package/dist/client/index.js +0 -56
- package/dist/siwx-BMlja_nt.d.cts +0 -9
- package/dist/siwx-BMlja_nt.d.ts +0 -9
package/dist/index.js
CHANGED
|
@@ -8,7 +8,65 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
// src/
|
|
11
|
+
// src/constants.ts
|
|
12
|
+
var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
|
|
13
|
+
var init_constants = __esm({
|
|
14
|
+
"src/constants.ts"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
BASE_NETWORK = "eip155:8453";
|
|
17
|
+
SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
18
|
+
TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
19
|
+
ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// src/protocols/x402/accepts.ts
|
|
24
|
+
async function resolvePayToValue(payTo, request, fallback, body) {
|
|
25
|
+
if (!payTo) return fallback;
|
|
26
|
+
if (typeof payTo === "string") return payTo;
|
|
27
|
+
return payTo(request, body);
|
|
28
|
+
}
|
|
29
|
+
function getConfiguredX402Accepts(config) {
|
|
30
|
+
if (config.x402?.accepts?.length) {
|
|
31
|
+
return [...config.x402.accepts];
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
scheme: "exact",
|
|
36
|
+
network: config.network ?? BASE_NETWORK,
|
|
37
|
+
payTo: config.payeeAddress
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
function getConfiguredX402Networks(config) {
|
|
42
|
+
return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
|
|
43
|
+
}
|
|
44
|
+
async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
|
|
45
|
+
return Promise.all(
|
|
46
|
+
accepts.map(async (accept) => ({
|
|
47
|
+
network: accept.network,
|
|
48
|
+
scheme: accept.scheme ?? "exact",
|
|
49
|
+
payTo: await resolvePayToValue(
|
|
50
|
+
routeEntry.payTo ?? accept.payTo,
|
|
51
|
+
request,
|
|
52
|
+
fallbackPayTo,
|
|
53
|
+
body
|
|
54
|
+
),
|
|
55
|
+
...accept.asset ? { asset: accept.asset } : {},
|
|
56
|
+
...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
|
|
57
|
+
...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
|
|
58
|
+
...accept.extra ? { extra: accept.extra } : {}
|
|
59
|
+
}))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
var init_accepts = __esm({
|
|
63
|
+
"src/protocols/x402/accepts.ts"() {
|
|
64
|
+
"use strict";
|
|
65
|
+
init_constants();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// src/protocols/x402/evm.ts
|
|
12
70
|
function isEvmNetwork(network) {
|
|
13
71
|
return network.startsWith("eip155:");
|
|
14
72
|
}
|
|
@@ -26,12 +84,12 @@ function buildEvmExactOptions(accepts, price) {
|
|
|
26
84
|
}));
|
|
27
85
|
}
|
|
28
86
|
var init_evm = __esm({
|
|
29
|
-
"src/protocols/evm.ts"() {
|
|
87
|
+
"src/protocols/x402/evm.ts"() {
|
|
30
88
|
"use strict";
|
|
31
89
|
}
|
|
32
90
|
});
|
|
33
91
|
|
|
34
|
-
// src/protocols/solana.ts
|
|
92
|
+
// src/protocols/x402/solana.ts
|
|
35
93
|
function isSolanaNetwork(network) {
|
|
36
94
|
return network.startsWith("solana:");
|
|
37
95
|
}
|
|
@@ -81,13 +139,13 @@ function isSolanaRequirement(requirement) {
|
|
|
81
139
|
return isSolanaNetwork(requirement.network);
|
|
82
140
|
}
|
|
83
141
|
var init_solana = __esm({
|
|
84
|
-
"src/protocols/solana.ts"() {
|
|
142
|
+
"src/protocols/x402/solana.ts"() {
|
|
85
143
|
"use strict";
|
|
86
|
-
|
|
144
|
+
init_facilitators();
|
|
87
145
|
}
|
|
88
146
|
});
|
|
89
147
|
|
|
90
|
-
// src/x402
|
|
148
|
+
// src/protocols/x402/facilitators.ts
|
|
91
149
|
function getResolvedX402Facilitator(config, network, defaultEvmFacilitator) {
|
|
92
150
|
const family = getNetworkFamily(network);
|
|
93
151
|
if (!family) return null;
|
|
@@ -157,8 +215,8 @@ function sameFacilitatorConfig(a, b) {
|
|
|
157
215
|
return a.url === b.url && a.createAuthHeaders === b.createAuthHeaders && a.createAcceptsHeaders === b.createAcceptsHeaders;
|
|
158
216
|
}
|
|
159
217
|
var DEFAULT_SOLANA_FACILITATOR_URL;
|
|
160
|
-
var
|
|
161
|
-
"src/x402
|
|
218
|
+
var init_facilitators = __esm({
|
|
219
|
+
"src/protocols/x402/facilitators.ts"() {
|
|
162
220
|
"use strict";
|
|
163
221
|
init_evm();
|
|
164
222
|
init_solana();
|
|
@@ -166,64 +224,6 @@ var init_x402_facilitators = __esm({
|
|
|
166
224
|
}
|
|
167
225
|
});
|
|
168
226
|
|
|
169
|
-
// src/constants.ts
|
|
170
|
-
var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
|
|
171
|
-
var init_constants = __esm({
|
|
172
|
-
"src/constants.ts"() {
|
|
173
|
-
"use strict";
|
|
174
|
-
BASE_NETWORK = "eip155:8453";
|
|
175
|
-
SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
176
|
-
TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
177
|
-
ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// src/x402-config.ts
|
|
182
|
-
async function resolvePayToValue(payTo, request, fallback, body) {
|
|
183
|
-
if (!payTo) return fallback;
|
|
184
|
-
if (typeof payTo === "string") return payTo;
|
|
185
|
-
return payTo(request, body);
|
|
186
|
-
}
|
|
187
|
-
function getConfiguredX402Accepts(config) {
|
|
188
|
-
if (config.x402?.accepts?.length) {
|
|
189
|
-
return [...config.x402.accepts];
|
|
190
|
-
}
|
|
191
|
-
return [
|
|
192
|
-
{
|
|
193
|
-
scheme: "exact",
|
|
194
|
-
network: config.network ?? BASE_NETWORK,
|
|
195
|
-
payTo: config.payeeAddress
|
|
196
|
-
}
|
|
197
|
-
];
|
|
198
|
-
}
|
|
199
|
-
function getConfiguredX402Networks(config) {
|
|
200
|
-
return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
|
|
201
|
-
}
|
|
202
|
-
async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
|
|
203
|
-
return Promise.all(
|
|
204
|
-
accepts.map(async (accept) => ({
|
|
205
|
-
network: accept.network,
|
|
206
|
-
scheme: accept.scheme ?? "exact",
|
|
207
|
-
payTo: await resolvePayToValue(
|
|
208
|
-
routeEntry.payTo ?? accept.payTo,
|
|
209
|
-
request,
|
|
210
|
-
fallbackPayTo,
|
|
211
|
-
body
|
|
212
|
-
),
|
|
213
|
-
...accept.asset ? { asset: accept.asset } : {},
|
|
214
|
-
...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
|
|
215
|
-
...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
|
|
216
|
-
...accept.extra ? { extra: accept.extra } : {}
|
|
217
|
-
}))
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
var init_x402_config = __esm({
|
|
221
|
-
"src/x402-config.ts"() {
|
|
222
|
-
"use strict";
|
|
223
|
-
init_constants();
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
|
|
227
227
|
// src/server.ts
|
|
228
228
|
var server_exports = {};
|
|
229
229
|
__export(server_exports, {
|
|
@@ -308,8 +308,8 @@ var init_server = __esm({
|
|
|
308
308
|
"use strict";
|
|
309
309
|
init_evm();
|
|
310
310
|
init_solana();
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
init_facilitators();
|
|
312
|
+
init_accepts();
|
|
313
313
|
}
|
|
314
314
|
});
|
|
315
315
|
|
|
@@ -415,13 +415,32 @@ var RouteRegistry = class {
|
|
|
415
415
|
}
|
|
416
416
|
};
|
|
417
417
|
|
|
418
|
-
// src/
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
418
|
+
// src/headers.ts
|
|
419
|
+
var HEADERS = {
|
|
420
|
+
// ---- Standard HTTP ----
|
|
421
|
+
AUTHORIZATION: "Authorization",
|
|
422
|
+
WWW_AUTHENTICATE: "WWW-Authenticate",
|
|
423
|
+
// ---- Auth ----
|
|
424
|
+
API_KEY: "X-API-Key",
|
|
425
|
+
// ---- Request meta (used by plugin/observability) ----
|
|
426
|
+
WALLET_ADDRESS: "X-Wallet-Address",
|
|
427
|
+
CLIENT_ID: "X-Client-ID",
|
|
428
|
+
SESSION_ID: "X-Session-ID",
|
|
429
|
+
// ---- SIWX ----
|
|
430
|
+
SIWX: "SIGN-IN-WITH-X",
|
|
431
|
+
// ---- x402 (payment) ----
|
|
432
|
+
X402_PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
|
|
433
|
+
/** Legacy x402 payment header — accepted alongside PAYMENT-SIGNATURE. */
|
|
434
|
+
X402_PAYMENT_LEGACY: "X-PAYMENT",
|
|
435
|
+
X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
|
|
436
|
+
X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
|
|
437
|
+
// ---- MPP (payment) ----
|
|
438
|
+
MPP_PAYMENT_RECEIPT: "Payment-Receipt"
|
|
439
|
+
};
|
|
440
|
+
var AUTH_SCHEME = {
|
|
441
|
+
BEARER: "Bearer ",
|
|
442
|
+
MPP_PAYMENT: "Payment "
|
|
443
|
+
};
|
|
425
444
|
|
|
426
445
|
// src/plugin.ts
|
|
427
446
|
function createDefaultContext(meta) {
|
|
@@ -498,106 +517,31 @@ function consolePlugin() {
|
|
|
498
517
|
};
|
|
499
518
|
}
|
|
500
519
|
|
|
501
|
-
// src/
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
this.evict();
|
|
507
|
-
if (this.seen.has(nonce)) return false;
|
|
508
|
-
this.seen.set(nonce, Date.now() + SIWX_CHALLENGE_EXPIRY_MS);
|
|
509
|
-
return true;
|
|
510
|
-
}
|
|
511
|
-
evict() {
|
|
512
|
-
const now = Date.now();
|
|
513
|
-
for (const [n, exp] of this.seen) {
|
|
514
|
-
if (exp < now) this.seen.delete(n);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
};
|
|
518
|
-
function detectRedisClientType(client) {
|
|
519
|
-
if (!client || typeof client !== "object") {
|
|
520
|
-
throw new Error(
|
|
521
|
-
"createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
if ("options" in client && "status" in client) {
|
|
525
|
-
return "ioredis";
|
|
526
|
-
}
|
|
527
|
-
const constructor = client.constructor?.name;
|
|
528
|
-
if (constructor === "Redis" && "url" in client) {
|
|
529
|
-
return "upstash";
|
|
530
|
-
}
|
|
531
|
-
if (typeof client.set === "function") {
|
|
532
|
-
return "upstash";
|
|
533
|
-
}
|
|
534
|
-
throw new Error(
|
|
535
|
-
"Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
|
|
536
|
-
);
|
|
520
|
+
// src/pipeline/context/preflight.ts
|
|
521
|
+
function preflight(routeEntry, handler, deps, request) {
|
|
522
|
+
const meta = buildMeta(request, routeEntry);
|
|
523
|
+
const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
|
|
524
|
+
return { routeEntry, handler, deps, request, meta, pluginCtx };
|
|
537
525
|
}
|
|
538
|
-
function
|
|
539
|
-
const prefix = opts?.prefix ?? "siwx:nonce:";
|
|
540
|
-
const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
541
|
-
const clientType = detectRedisClientType(client);
|
|
526
|
+
function buildMeta(request, routeEntry) {
|
|
542
527
|
return {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
throw new Error("Unknown Redis client type");
|
|
556
|
-
}
|
|
528
|
+
requestId: crypto.randomUUID(),
|
|
529
|
+
method: request.method,
|
|
530
|
+
route: routeEntry.key,
|
|
531
|
+
origin: request.headers.get("origin") ?? new URL(request.url).origin,
|
|
532
|
+
referer: request.headers.get("referer"),
|
|
533
|
+
walletAddress: request.headers.get(HEADERS.WALLET_ADDRESS),
|
|
534
|
+
clientId: request.headers.get(HEADERS.CLIENT_ID),
|
|
535
|
+
sessionId: request.headers.get(HEADERS.SESSION_ID),
|
|
536
|
+
contentType: request.headers.get("content-type"),
|
|
537
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
538
|
+
startTime: Date.now()
|
|
557
539
|
};
|
|
558
540
|
}
|
|
559
541
|
|
|
560
|
-
// src/
|
|
561
|
-
function detectProtocol(request) {
|
|
562
|
-
if (request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT")) {
|
|
563
|
-
return "x402";
|
|
564
|
-
}
|
|
565
|
-
const auth = request.headers.get("Authorization");
|
|
566
|
-
if (auth && auth.startsWith("Payment ")) {
|
|
567
|
-
return "mpp";
|
|
568
|
-
}
|
|
569
|
-
if (request.headers.get("SIGN-IN-WITH-X")) {
|
|
570
|
-
return "siwx";
|
|
571
|
-
}
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// src/handler.ts
|
|
542
|
+
// src/pipeline/context/parse-body.ts
|
|
576
543
|
import { NextResponse } from "next/server";
|
|
577
544
|
|
|
578
|
-
// src/types.ts
|
|
579
|
-
var HttpError = class extends Error {
|
|
580
|
-
constructor(message, status) {
|
|
581
|
-
super(message);
|
|
582
|
-
this.status = status;
|
|
583
|
-
this.name = "HttpError";
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
// src/handler.ts
|
|
588
|
-
async function safeCallHandler(handler, ctx, options = {}) {
|
|
589
|
-
try {
|
|
590
|
-
const result = await handler(ctx);
|
|
591
|
-
if (result instanceof Response) return result;
|
|
592
|
-
return NextResponse.json(result);
|
|
593
|
-
} catch (error) {
|
|
594
|
-
options.onError?.(error);
|
|
595
|
-
const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
|
|
596
|
-
const message = error instanceof Error ? error.message : "Internal error";
|
|
597
|
-
return NextResponse.json({ success: false, error: message }, { status });
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
545
|
// src/body.ts
|
|
602
546
|
async function bufferBody(request) {
|
|
603
547
|
const text = await request.text();
|
|
@@ -622,299 +566,329 @@ function validateBody(parsed, schema) {
|
|
|
622
566
|
};
|
|
623
567
|
}
|
|
624
568
|
|
|
625
|
-
// src/
|
|
626
|
-
async function
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
if (defaultTier && tiers[defaultTier]) {
|
|
639
|
-
return tiers[defaultTier].price;
|
|
640
|
-
}
|
|
641
|
-
if (!tierKey) {
|
|
642
|
-
throw Object.assign(new Error(`Missing required field '${field}' for tier pricing`), {
|
|
643
|
-
status: 400
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
throw Object.assign(
|
|
647
|
-
new Error(
|
|
648
|
-
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
649
|
-
),
|
|
650
|
-
{ status: 400 }
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
function resolveMaxPrice(pricing) {
|
|
654
|
-
if (typeof pricing === "string") return pricing;
|
|
655
|
-
if (typeof pricing === "function") {
|
|
656
|
-
throw new Error("Dynamic pricing requires maxPrice \u2014 this should be caught at registration");
|
|
657
|
-
}
|
|
658
|
-
const { tiers } = pricing;
|
|
659
|
-
let max = "0";
|
|
660
|
-
for (const tier of Object.values(tiers)) {
|
|
661
|
-
if (parseFloat(tier.price) > parseFloat(max)) {
|
|
662
|
-
max = tier.price;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
return max;
|
|
569
|
+
// src/pipeline/context/parse-body.ts
|
|
570
|
+
async function parseBody(request, routeEntry) {
|
|
571
|
+
if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
|
|
572
|
+
const raw = await bufferBody(request);
|
|
573
|
+
const result = validateBody(raw, routeEntry.bodySchema);
|
|
574
|
+
if (result.success) return { ok: true, data: result.data };
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
response: NextResponse.json(
|
|
578
|
+
{ success: false, error: result.error, issues: result.issues },
|
|
579
|
+
{ status: 400 }
|
|
580
|
+
)
|
|
581
|
+
};
|
|
666
582
|
}
|
|
667
583
|
|
|
668
|
-
// src/
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
584
|
+
// src/pipeline/context/parse-query.ts
|
|
585
|
+
function parseQuery(request, routeEntry) {
|
|
586
|
+
if (!routeEntry.querySchema) return void 0;
|
|
587
|
+
const params = Object.fromEntries(request.nextUrl.searchParams.entries());
|
|
588
|
+
const result = routeEntry.querySchema.safeParse(params);
|
|
589
|
+
return result.success ? result.data : params;
|
|
590
|
+
}
|
|
673
591
|
|
|
674
|
-
// src/
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
async function buildX402Challenge(opts) {
|
|
679
|
-
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
|
|
680
|
-
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
681
|
-
const resource = buildChallengeResource(request, routeEntry);
|
|
682
|
-
const requirements = await buildChallengeRequirements(
|
|
683
|
-
server,
|
|
684
|
-
request,
|
|
685
|
-
price,
|
|
686
|
-
accepts,
|
|
687
|
-
resource,
|
|
688
|
-
facilitatorsByNetwork
|
|
689
|
-
);
|
|
690
|
-
const paymentRequired = await server.createPaymentRequiredResponse(
|
|
691
|
-
requirements,
|
|
692
|
-
resource,
|
|
693
|
-
void 0,
|
|
694
|
-
extensions
|
|
695
|
-
);
|
|
696
|
-
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
697
|
-
return { encoded, requirements };
|
|
592
|
+
// src/pipeline/context/errors.ts
|
|
593
|
+
function errorStatus(error, fallback) {
|
|
594
|
+
const status = error?.status;
|
|
595
|
+
return typeof status === "number" ? status : fallback;
|
|
698
596
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
597
|
+
function errorMessage(error, fallback) {
|
|
598
|
+
return error instanceof Error ? error.message : fallback;
|
|
599
|
+
}
|
|
600
|
+
function handlerFailureError(response) {
|
|
601
|
+
const message = response.statusText || `Handler returned HTTP ${response.status}`;
|
|
602
|
+
return Object.assign(new Error(message), { status: response.status });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/pipeline/context/fail.ts
|
|
606
|
+
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
|
+
});
|
|
707
625
|
}
|
|
708
|
-
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/pipeline/context/fail.ts
|
|
629
|
+
function fail(ctx, status, message, requestBody) {
|
|
630
|
+
const response = NextResponse2.json({ success: false, error: message }, { status });
|
|
631
|
+
firePluginResponse(ctx, response, requestBody);
|
|
632
|
+
return response;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/pipeline/context/run-validate.ts
|
|
636
|
+
async function runValidate(ctx, body) {
|
|
637
|
+
if (!ctx.routeEntry.validateFn) return null;
|
|
709
638
|
try {
|
|
710
|
-
|
|
639
|
+
await ctx.routeEntry.validateFn(body);
|
|
640
|
+
return null;
|
|
711
641
|
} catch (err) {
|
|
712
|
-
|
|
713
|
-
if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
|
|
714
|
-
throw err;
|
|
715
|
-
}
|
|
716
|
-
if (!verify.isValid) return invalidPaymentVerification();
|
|
717
|
-
if (typeof verify.payer !== "string" || verify.payer.length === 0) {
|
|
718
|
-
throw new Error("x402 verification succeeded without a payer address");
|
|
642
|
+
return fail(ctx, errorStatus(err, 400), errorMessage(err, "Validation failed"), body);
|
|
719
643
|
}
|
|
720
|
-
return {
|
|
721
|
-
valid: true,
|
|
722
|
-
payer: verify.payer,
|
|
723
|
-
payload,
|
|
724
|
-
requirements: matching
|
|
725
|
-
};
|
|
726
644
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
645
|
+
|
|
646
|
+
// src/handler.ts
|
|
647
|
+
import { NextResponse as NextResponse3 } from "next/server";
|
|
648
|
+
|
|
649
|
+
// src/types.ts
|
|
650
|
+
var HttpError = class extends Error {
|
|
651
|
+
constructor(message, status) {
|
|
652
|
+
super(message);
|
|
653
|
+
this.status = status;
|
|
654
|
+
this.name = "HttpError";
|
|
731
655
|
}
|
|
732
|
-
|
|
733
|
-
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// src/handler.ts
|
|
659
|
+
async function safeCallHandler(handler, ctx, options = {}) {
|
|
660
|
+
try {
|
|
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 });
|
|
734
669
|
}
|
|
735
|
-
const stableMatch = requirements.find(
|
|
736
|
-
(requirement) => matchesStableFields(requirement, payload.accepted)
|
|
737
|
-
);
|
|
738
|
-
return stableMatch ? payload.accepted : null;
|
|
739
|
-
}
|
|
740
|
-
function matchesStableFields(requirement, accepted) {
|
|
741
|
-
return requirement.scheme === accepted.scheme && requirement.network === accepted.network && requirement.payTo === accepted.payTo && requirement.asset === accepted.asset && requirement.amount === accepted.amount && requirement.maxTimeoutSeconds === accepted.maxTimeoutSeconds;
|
|
742
|
-
}
|
|
743
|
-
async function buildExpectedRequirements(server, request, price, accepts) {
|
|
744
|
-
const exactRequirements = await buildExactRequirements(server, request, price, accepts);
|
|
745
|
-
const customRequirements = buildCustomRequirements(price, accepts);
|
|
746
|
-
return [...exactRequirements, ...customRequirements];
|
|
747
670
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
671
|
+
|
|
672
|
+
// src/pipeline/context/invoke.ts
|
|
673
|
+
async function invoke(ctx, wallet, account, body, payment) {
|
|
674
|
+
const handlerCtx = {
|
|
675
|
+
body,
|
|
676
|
+
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
677
|
+
request: ctx.request,
|
|
678
|
+
requestId: ctx.meta.requestId,
|
|
679
|
+
route: ctx.routeEntry.key,
|
|
680
|
+
wallet,
|
|
681
|
+
payment,
|
|
682
|
+
account,
|
|
683
|
+
alert(level, message, alertMeta) {
|
|
684
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
685
|
+
level,
|
|
686
|
+
message,
|
|
687
|
+
route: ctx.routeEntry.key,
|
|
688
|
+
meta: alertMeta
|
|
689
|
+
});
|
|
690
|
+
},
|
|
691
|
+
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
692
|
+
};
|
|
693
|
+
let rawResult;
|
|
694
|
+
let handlerError;
|
|
695
|
+
const response = await safeCallHandler(
|
|
696
|
+
async (c) => {
|
|
697
|
+
rawResult = await ctx.handler(c);
|
|
698
|
+
return rawResult;
|
|
699
|
+
},
|
|
700
|
+
handlerCtx,
|
|
701
|
+
{
|
|
702
|
+
onError(error) {
|
|
703
|
+
handlerError = error;
|
|
766
704
|
}
|
|
767
|
-
console.warn(
|
|
768
|
-
`[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
|
|
769
|
-
);
|
|
770
705
|
}
|
|
706
|
+
);
|
|
707
|
+
return { response, rawResult, handlerError };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/pipeline/context/fire-provider-quota.ts
|
|
711
|
+
function fireProviderQuota(ctx, response, handlerResult) {
|
|
712
|
+
const { providerName, providerConfig } = ctx.routeEntry;
|
|
713
|
+
if (!providerName || !providerConfig?.extractQuota) return;
|
|
714
|
+
if (response.status >= 400) return;
|
|
715
|
+
try {
|
|
716
|
+
const quota = providerConfig.extractQuota(handlerResult, response.headers);
|
|
717
|
+
if (!quota) return;
|
|
718
|
+
const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
|
|
719
|
+
const overage = providerConfig.overage ?? "same-rate";
|
|
720
|
+
const event = {
|
|
721
|
+
provider: providerName,
|
|
722
|
+
route: ctx.routeEntry.key,
|
|
723
|
+
remaining: quota.remaining,
|
|
724
|
+
limit: quota.limit,
|
|
725
|
+
spend: quota.spend,
|
|
726
|
+
level,
|
|
727
|
+
overage,
|
|
728
|
+
message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
|
|
729
|
+
};
|
|
730
|
+
firePluginHook(ctx.deps.plugin, "onProviderQuota", ctx.pluginCtx, event);
|
|
731
|
+
} catch {
|
|
771
732
|
}
|
|
772
|
-
if (requirements.length > 0) {
|
|
773
|
-
return requirements;
|
|
774
|
-
}
|
|
775
|
-
throw failures[0] ?? new Error("Failed to build x402 exact requirements");
|
|
776
733
|
}
|
|
777
|
-
function
|
|
778
|
-
|
|
734
|
+
function computeQuotaLevel(remaining, warn, critical) {
|
|
735
|
+
if (remaining === null) return "healthy";
|
|
736
|
+
if (critical !== void 0 && remaining <= critical) return "critical";
|
|
737
|
+
if (warn !== void 0 && remaining <= warn) return "warn";
|
|
738
|
+
return "healthy";
|
|
779
739
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
740
|
+
|
|
741
|
+
// src/pipeline/context/finalize.ts
|
|
742
|
+
function finalize(ctx, response, rawResult, requestBody) {
|
|
743
|
+
fireProviderQuota(ctx, response, rawResult);
|
|
744
|
+
firePluginResponse(ctx, response, requestBody, rawResult);
|
|
745
|
+
return response;
|
|
784
746
|
}
|
|
785
|
-
|
|
786
|
-
|
|
747
|
+
|
|
748
|
+
// src/pipeline/context/run-handler-only.ts
|
|
749
|
+
async function runHandlerOnly(ctx, wallet, account) {
|
|
750
|
+
const body = await parseBody(ctx.request, ctx.routeEntry);
|
|
751
|
+
if (!body.ok) {
|
|
752
|
+
firePluginResponse(ctx, body.response);
|
|
753
|
+
return body.response;
|
|
754
|
+
}
|
|
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);
|
|
787
759
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
760
|
+
|
|
761
|
+
// src/pipeline/context/settlement-context.ts
|
|
762
|
+
function settlementContext(ctx, scope) {
|
|
763
|
+
return {
|
|
764
|
+
route: ctx.routeEntry.key,
|
|
765
|
+
request: ctx.request,
|
|
766
|
+
body: scope.body,
|
|
767
|
+
wallet: scope.wallet,
|
|
768
|
+
account: scope.account,
|
|
769
|
+
payment: scope.payment,
|
|
770
|
+
response: scope.response,
|
|
771
|
+
result: scope.rawResult
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
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
|
|
797
788
|
);
|
|
798
789
|
}
|
|
799
|
-
return accepted;
|
|
800
790
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
}
|
|
816
|
-
})
|
|
817
|
-
);
|
|
818
|
-
const enriched = [...requirements];
|
|
819
|
-
results.filter((r) => r.success).forEach(({ group, accepted }) => {
|
|
820
|
-
accepted.forEach((req, offset) => {
|
|
821
|
-
const index = group.items[offset]?.index;
|
|
822
|
-
if (index !== void 0) enriched[index] = req;
|
|
791
|
+
|
|
792
|
+
// src/pipeline/context/run-settlement-error.ts
|
|
793
|
+
async function runSettlementError(ctx, scope, error, phase) {
|
|
794
|
+
const hook = ctx.routeEntry.settlement?.onSettlementError;
|
|
795
|
+
if (!hook) return;
|
|
796
|
+
try {
|
|
797
|
+
await hook({ ...settlementContext(ctx, scope), error, phase });
|
|
798
|
+
} catch (hookError) {
|
|
799
|
+
const message = errorMessage(hookError, "Settlement error hook failed");
|
|
800
|
+
console.error(`[router] ${ctx.routeEntry.key}: onSettlementError failed: ${message}`);
|
|
801
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
802
|
+
level: "error",
|
|
803
|
+
message: `Settlement error hook failed: ${message}`,
|
|
804
|
+
route: ctx.routeEntry.key
|
|
823
805
|
});
|
|
824
|
-
});
|
|
825
|
-
const failedIndices = new Set(
|
|
826
|
-
results.filter((r) => !r.success).flatMap(({ group }) => group.items.map(({ index }) => index))
|
|
827
|
-
);
|
|
828
|
-
const remaining = enriched.filter((_, i) => !failedIndices.has(i));
|
|
829
|
-
if (remaining.length === 0) {
|
|
830
|
-
throw new Error(
|
|
831
|
-
"All facilitator enrichments failed; no payment requirements remain for challenge"
|
|
832
|
-
);
|
|
833
806
|
}
|
|
834
|
-
return remaining;
|
|
835
807
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
items: [{ index, requirement }]
|
|
808
|
+
|
|
809
|
+
// src/pipeline/context/run-after-settle.ts
|
|
810
|
+
async function runAfterSettle(ctx, scope) {
|
|
811
|
+
const hook = ctx.routeEntry.settlement?.afterSettle;
|
|
812
|
+
if (!hook) return;
|
|
813
|
+
try {
|
|
814
|
+
await hook(settlementContext(ctx, scope));
|
|
815
|
+
} catch (error) {
|
|
816
|
+
const message = errorMessage(error, "Post-settlement hook failed");
|
|
817
|
+
console.error(`[router] ${ctx.routeEntry.key}: afterSettle failed: ${message}`);
|
|
818
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
819
|
+
level: "error",
|
|
820
|
+
message: `Post-settlement hook failed: ${message}`,
|
|
821
|
+
route: ctx.routeEntry.key
|
|
851
822
|
});
|
|
852
|
-
|
|
853
|
-
return groups;
|
|
854
|
-
}
|
|
855
|
-
function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
|
|
856
|
-
const facilitator = getFacilitatorForRequirement(facilitatorsByNetwork, requirement);
|
|
857
|
-
if (!facilitator) {
|
|
858
|
-
throw new Error(
|
|
859
|
-
`Missing x402 facilitator for ${requirement.scheme} requirement on ${requirement.network}`
|
|
860
|
-
);
|
|
823
|
+
await runSettlementError(ctx, scope, error, "afterSettle");
|
|
861
824
|
}
|
|
862
|
-
return facilitator;
|
|
863
|
-
}
|
|
864
|
-
function requiresFacilitatorEnrichment(requirement) {
|
|
865
|
-
return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
|
|
866
825
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
826
|
+
|
|
827
|
+
// src/pipeline/context/run-settled-handler-error.ts
|
|
828
|
+
async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
|
|
829
|
+
const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
|
|
830
|
+
if (!hook) return;
|
|
831
|
+
try {
|
|
832
|
+
await hook({ ...settlementContext(ctx, scope), error });
|
|
833
|
+
} catch (hookError) {
|
|
834
|
+
const message = errorMessage(hookError, "Settled handler error hook failed");
|
|
835
|
+
console.error(`[router] ${ctx.routeEntry.key}: onSettledHandlerError failed: ${message}`);
|
|
836
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
837
|
+
level: "error",
|
|
838
|
+
message: `Settled handler error hook failed: ${message}`,
|
|
839
|
+
route: ctx.routeEntry.key
|
|
840
|
+
});
|
|
872
841
|
}
|
|
873
|
-
return {
|
|
874
|
-
scheme: accept.scheme,
|
|
875
|
-
network: accept.network,
|
|
876
|
-
amount: decimalToAtomicUnits(price, accept.decimals ?? 6),
|
|
877
|
-
asset: accept.asset,
|
|
878
|
-
payTo: accept.payTo,
|
|
879
|
-
maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
|
|
880
|
-
extra: accept.extra ?? {}
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
function buildChallengeResource(request, routeEntry) {
|
|
884
|
-
return {
|
|
885
|
-
url: request.url,
|
|
886
|
-
method: routeEntry.method,
|
|
887
|
-
description: routeEntry.description,
|
|
888
|
-
mimeType: "application/json"
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
async function readPaymentPayload(request) {
|
|
892
|
-
const paymentHeader = request.headers.get("PAYMENT-SIGNATURE") ?? request.headers.get("X-PAYMENT");
|
|
893
|
-
if (!paymentHeader) return null;
|
|
894
|
-
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
895
|
-
return decodePaymentSignatureHeader(paymentHeader);
|
|
896
|
-
}
|
|
897
|
-
function invalidPaymentVerification() {
|
|
898
|
-
return { valid: false, payload: null, requirements: null, payer: null };
|
|
899
842
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
+
});
|
|
909
855
|
}
|
|
910
|
-
const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
|
|
911
|
-
return normalized === "" ? "0" : normalized;
|
|
912
856
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const
|
|
917
|
-
|
|
857
|
+
|
|
858
|
+
// src/pipeline/context/settle-and-finalize.ts
|
|
859
|
+
async function settleAndFinalize(args) {
|
|
860
|
+
const { ctx, strategy, verifyOutcome, scope, rawResult, body, onSettleError } = args;
|
|
861
|
+
const { request, routeEntry, deps } = ctx;
|
|
862
|
+
const settle = await strategy.settle({
|
|
863
|
+
request,
|
|
864
|
+
response: scope.response,
|
|
865
|
+
payment: verifyOutcome.payment,
|
|
866
|
+
token: verifyOutcome.token,
|
|
867
|
+
routeEntry,
|
|
868
|
+
deps
|
|
869
|
+
});
|
|
870
|
+
if (!settle.ok) {
|
|
871
|
+
if (onSettleError) await onSettleError(settle.error, settle.failMessage);
|
|
872
|
+
return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
|
|
873
|
+
}
|
|
874
|
+
await grantEntitlementIfSiwx(ctx, verifyOutcome.wallet);
|
|
875
|
+
firePluginHook(deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
|
|
876
|
+
protocol: strategy.protocol,
|
|
877
|
+
payer: verifyOutcome.wallet,
|
|
878
|
+
transaction: settle.settledPayment.transaction ?? "",
|
|
879
|
+
network: settle.settledPayment.network
|
|
880
|
+
});
|
|
881
|
+
await runAfterSettle(ctx, {
|
|
882
|
+
...scope,
|
|
883
|
+
payment: settle.settledPayment,
|
|
884
|
+
response: settle.response
|
|
885
|
+
});
|
|
886
|
+
return finalize(ctx, settle.response, rawResult, body);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/auth/normalize-wallet.ts
|
|
890
|
+
function normalizeWalletAddress(address) {
|
|
891
|
+
return /^0x/i.test(address) ? address.toLowerCase() : address;
|
|
918
892
|
}
|
|
919
893
|
|
|
920
894
|
// src/auth/siwx.ts
|
|
@@ -938,7 +912,7 @@ function categorizeValidationError(error) {
|
|
|
938
912
|
}
|
|
939
913
|
async function verifySIWX(request, _routeEntry, nonceStore) {
|
|
940
914
|
const { parseSIWxHeader, validateSIWxMessage, verifySIWxSignature } = await import("@x402/extensions/sign-in-with-x");
|
|
941
|
-
const header = request.headers.get(
|
|
915
|
+
const header = request.headers.get(HEADERS.SIWX);
|
|
942
916
|
if (!header) {
|
|
943
917
|
return { valid: false, wallet: null, code: "siwx_missing_header" };
|
|
944
918
|
}
|
|
@@ -967,25 +941,52 @@ async function buildSIWXExtension() {
|
|
|
967
941
|
return declareSIWxExtension();
|
|
968
942
|
}
|
|
969
943
|
|
|
970
|
-
// src/
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
if (
|
|
976
|
-
|
|
944
|
+
// src/pipeline/context/try-siwx-fast-path.ts
|
|
945
|
+
async function trySiwxFastPath(ctx, account) {
|
|
946
|
+
const { request, routeEntry, deps } = ctx;
|
|
947
|
+
if (!routeEntry.siwxEnabled) return null;
|
|
948
|
+
const siwxHeader = request.headers.get(HEADERS.SIWX);
|
|
949
|
+
if (!siwxHeader) return null;
|
|
950
|
+
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
951
|
+
if (!siwx.valid) return null;
|
|
952
|
+
const wallet = normalizeWalletAddress(siwx.wallet);
|
|
953
|
+
ctx.pluginCtx.setVerifiedWallet(wallet);
|
|
954
|
+
const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
|
|
955
|
+
if (!entitled) return null;
|
|
956
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
957
|
+
authMode: "siwx",
|
|
958
|
+
wallet,
|
|
959
|
+
route: routeEntry.key
|
|
960
|
+
});
|
|
961
|
+
return runHandlerOnly(ctx, wallet, account);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/pipeline/context/should-parse-body-early.ts
|
|
965
|
+
function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
|
|
966
|
+
if (incomingStrategy) return false;
|
|
967
|
+
if (!routeEntry.bodySchema) return false;
|
|
968
|
+
return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/pipeline/context/protocol-init-error.ts
|
|
972
|
+
function protocolInitError(routeEntry, deps) {
|
|
973
|
+
if (!routeEntry.pricing) return null;
|
|
974
|
+
const errors = [];
|
|
975
|
+
for (const protocol of routeEntry.protocols) {
|
|
976
|
+
if (protocol === "x402" && deps.x402InitError) {
|
|
977
|
+
errors.push(`x402: ${deps.x402InitError}`);
|
|
978
|
+
}
|
|
979
|
+
if (protocol === "mpp" && deps.mppInitError) {
|
|
980
|
+
errors.push(`mpp: ${deps.mppInitError}`);
|
|
981
|
+
}
|
|
977
982
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const didParts = rawSource.split(":");
|
|
981
|
-
const lastPart = didParts[didParts.length - 1];
|
|
982
|
-
const wallet = normalizeWalletAddress(isAddress(lastPart) ? getAddress(lastPart) : rawSource);
|
|
983
|
-
return { valid: true, wallet, withReceipt: result.withReceipt };
|
|
983
|
+
if (errors.length === 0) return null;
|
|
984
|
+
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
984
985
|
}
|
|
985
986
|
|
|
986
987
|
// src/auth/api-key.ts
|
|
987
988
|
async function verifyApiKey(request, resolver) {
|
|
988
|
-
const apiKey = request.headers.get(
|
|
989
|
+
const apiKey = request.headers.get(HEADERS.API_KEY) ?? extractBearerToken(request.headers.get(HEADERS.AUTHORIZATION));
|
|
989
990
|
if (!apiKey) {
|
|
990
991
|
return { valid: false, account: null };
|
|
991
992
|
}
|
|
@@ -997,947 +998,873 @@ async function verifyApiKey(request, resolver) {
|
|
|
997
998
|
}
|
|
998
999
|
function extractBearerToken(header) {
|
|
999
1000
|
if (!header) return null;
|
|
1000
|
-
if (header.startsWith(
|
|
1001
|
+
if (header.startsWith(AUTH_SCHEME.BEARER)) return header.slice(AUTH_SCHEME.BEARER.length);
|
|
1001
1002
|
return null;
|
|
1002
1003
|
}
|
|
1003
1004
|
|
|
1004
|
-
// src/
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1005
|
+
// src/pipeline/flows/api-key-only.ts
|
|
1006
|
+
async function runApiKeyOnlyFlow(ctx) {
|
|
1007
|
+
if (!ctx.routeEntry.apiKeyResolver) {
|
|
1008
|
+
return fail(ctx, 401, "API key resolver not configured");
|
|
1009
|
+
}
|
|
1010
|
+
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
1011
|
+
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
1012
|
+
firePluginHook(ctx.deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
1013
|
+
authMode: "apiKey",
|
|
1014
|
+
wallet: null,
|
|
1015
|
+
route: ctx.routeEntry.key,
|
|
1016
|
+
account: result.account
|
|
1017
|
+
});
|
|
1018
|
+
return runHandlerOnly(ctx, null, result.account);
|
|
1009
1019
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1020
|
+
|
|
1021
|
+
// src/pricing/dynamic.ts
|
|
1022
|
+
var DynamicPricing = class {
|
|
1023
|
+
constructor(opts) {
|
|
1024
|
+
this.opts = opts;
|
|
1025
|
+
}
|
|
1026
|
+
needsBody = true;
|
|
1027
|
+
async quote(body) {
|
|
1028
|
+
try {
|
|
1029
|
+
const raw = await this.opts.fn(body);
|
|
1030
|
+
return this.cap(raw, body);
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
1033
|
+
error: err instanceof Error ? err.stack : String(err),
|
|
1034
|
+
body
|
|
1035
|
+
});
|
|
1036
|
+
if (this.opts.maxPrice) {
|
|
1037
|
+
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
1038
|
+
return this.opts.maxPrice;
|
|
1039
|
+
}
|
|
1040
|
+
throw err;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
challengeQuote(body) {
|
|
1044
|
+
if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
|
|
1045
|
+
return this.quote(body);
|
|
1046
|
+
}
|
|
1047
|
+
describe() {
|
|
1048
|
+
return {
|
|
1049
|
+
mode: "dynamic",
|
|
1050
|
+
min: this.opts.minPrice ?? "0",
|
|
1051
|
+
max: this.opts.maxPrice ?? "0"
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
cap(raw, body) {
|
|
1055
|
+
if (!this.opts.maxPrice) return raw;
|
|
1056
|
+
const n = parseFloat(raw);
|
|
1057
|
+
const max = parseFloat(this.opts.maxPrice);
|
|
1058
|
+
if (!Number.isFinite(n) || n > max) {
|
|
1059
|
+
this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
|
|
1060
|
+
calculated: raw,
|
|
1061
|
+
maxPrice: this.opts.maxPrice,
|
|
1062
|
+
body
|
|
1063
|
+
});
|
|
1064
|
+
return this.opts.maxPrice;
|
|
1065
|
+
}
|
|
1066
|
+
return raw;
|
|
1067
|
+
}
|
|
1068
|
+
alert(level, message, meta) {
|
|
1069
|
+
this.opts.alert?.(level, message, meta);
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
function msg(err) {
|
|
1073
|
+
return err instanceof Error ? err.message : String(err);
|
|
1013
1074
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1075
|
+
|
|
1076
|
+
// src/pricing/fixed.ts
|
|
1077
|
+
var FixedPricing = class {
|
|
1078
|
+
constructor(price) {
|
|
1079
|
+
this.price = price;
|
|
1080
|
+
}
|
|
1081
|
+
needsBody = false;
|
|
1082
|
+
quote() {
|
|
1083
|
+
return Promise.resolve(this.price);
|
|
1084
|
+
}
|
|
1085
|
+
challengeQuote() {
|
|
1086
|
+
return Promise.resolve(this.price);
|
|
1087
|
+
}
|
|
1088
|
+
describe() {
|
|
1089
|
+
return { mode: "fixed", amount: this.price };
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
// src/pricing/tiered.ts
|
|
1094
|
+
var TieredPricing = class {
|
|
1095
|
+
constructor(opts) {
|
|
1096
|
+
this.opts = opts;
|
|
1097
|
+
}
|
|
1098
|
+
needsBody = true;
|
|
1099
|
+
async quote(body) {
|
|
1100
|
+
const { field, tiers, default: defaultTier } = this.opts;
|
|
1101
|
+
const tierKey = body != null ? String(body[field] ?? "") : "";
|
|
1102
|
+
if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
|
|
1103
|
+
if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
|
|
1104
|
+
if (!tierKey) {
|
|
1105
|
+
throw httpError(400, `Missing required field '${field}' for tier pricing`);
|
|
1106
|
+
}
|
|
1107
|
+
throw httpError(
|
|
1108
|
+
400,
|
|
1109
|
+
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
challengeQuote(body) {
|
|
1113
|
+
if (body !== void 0) {
|
|
1114
|
+
try {
|
|
1115
|
+
return this.quote(body);
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return Promise.resolve(this.maxTierPrice());
|
|
1120
|
+
}
|
|
1121
|
+
describe() {
|
|
1122
|
+
return {
|
|
1123
|
+
mode: "tiered",
|
|
1124
|
+
tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
|
|
1125
|
+
key,
|
|
1126
|
+
price: tier.price,
|
|
1127
|
+
...tier.label !== void 0 ? { label: tier.label } : {}
|
|
1128
|
+
})),
|
|
1129
|
+
...this.opts.default !== void 0 ? { default: this.opts.default } : {}
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
maxTierPrice() {
|
|
1133
|
+
let max = "0";
|
|
1134
|
+
for (const tier of Object.values(this.opts.tiers)) {
|
|
1135
|
+
if (parseFloat(tier.price) > parseFloat(max)) max = tier.price;
|
|
1136
|
+
}
|
|
1137
|
+
return max;
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
function httpError(status, message) {
|
|
1141
|
+
return Object.assign(new Error(message), { status });
|
|
1017
1142
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1143
|
+
|
|
1144
|
+
// src/pricing/index.ts
|
|
1145
|
+
function selectPricing(raw, deps = {}) {
|
|
1146
|
+
if (raw == null) return null;
|
|
1147
|
+
if (typeof raw === "string") {
|
|
1148
|
+
return new FixedPricing(raw);
|
|
1149
|
+
}
|
|
1150
|
+
if (typeof raw === "function") {
|
|
1151
|
+
return new DynamicPricing({
|
|
1152
|
+
fn: raw,
|
|
1153
|
+
maxPrice: deps.maxPrice,
|
|
1154
|
+
minPrice: deps.minPrice,
|
|
1155
|
+
route: deps.route,
|
|
1156
|
+
alert: deps.alert
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
if (typeof raw === "object" && "tiers" in raw) {
|
|
1160
|
+
return new TieredPricing({
|
|
1161
|
+
field: raw.field,
|
|
1162
|
+
tiers: raw.tiers,
|
|
1163
|
+
default: raw.default,
|
|
1164
|
+
maxPrice: deps.maxPrice,
|
|
1165
|
+
minPrice: deps.minPrice
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
|
|
1020
1169
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1170
|
+
|
|
1171
|
+
// src/protocols/mpp/credential.ts
|
|
1172
|
+
import { Credential } from "mppx";
|
|
1173
|
+
import { getAddress, isAddress } from "viem";
|
|
1174
|
+
function readMppCredential(request) {
|
|
1175
|
+
const credential = Credential.fromRequest(request);
|
|
1176
|
+
if (!credential) return null;
|
|
1177
|
+
const wallet = walletFromDid(credential.source ?? "");
|
|
1178
|
+
const rawType = credential.payload?.type;
|
|
1179
|
+
const payloadType = rawType === "transaction" ? "transaction" : rawType === "hash" ? "hash" : "unknown";
|
|
1180
|
+
return { credential, wallet, payloadType };
|
|
1024
1181
|
}
|
|
1025
|
-
function
|
|
1026
|
-
|
|
1182
|
+
function walletFromDid(rawSource) {
|
|
1183
|
+
const parts = rawSource.split(":");
|
|
1184
|
+
const last = parts[parts.length - 1];
|
|
1185
|
+
return normalizeWalletAddress(isAddress(last) ? getAddress(last) : rawSource);
|
|
1027
1186
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1187
|
+
|
|
1188
|
+
// src/protocols/mpp/transaction-mode.ts
|
|
1189
|
+
import { Transaction as TempoTransaction } from "viem/tempo";
|
|
1190
|
+
import { call as viemCall } from "viem/actions";
|
|
1191
|
+
|
|
1192
|
+
// src/protocols/mpp/receipt.ts
|
|
1193
|
+
import { Receipt } from "mppx";
|
|
1194
|
+
function extractTxHash(receiptHeader) {
|
|
1195
|
+
if (!receiptHeader) return "";
|
|
1196
|
+
try {
|
|
1197
|
+
return Receipt.deserialize(receiptHeader).reference;
|
|
1198
|
+
} catch {
|
|
1199
|
+
return "";
|
|
1036
1200
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1201
|
+
}
|
|
1202
|
+
async function readChallengeReason(challenge) {
|
|
1203
|
+
try {
|
|
1204
|
+
const text = await challenge.clone().text();
|
|
1205
|
+
if (!text) return "";
|
|
1206
|
+
const problem = JSON.parse(text);
|
|
1207
|
+
return problem.detail || problem.title || "";
|
|
1208
|
+
} catch {
|
|
1209
|
+
return "";
|
|
1039
1210
|
}
|
|
1040
|
-
return chains;
|
|
1041
1211
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
payment,
|
|
1052
|
-
account,
|
|
1053
|
-
alert(level, message, alertMeta) {
|
|
1054
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1055
|
-
level,
|
|
1056
|
-
message,
|
|
1057
|
-
route: routeEntry.key,
|
|
1058
|
-
meta: alertMeta
|
|
1059
|
-
});
|
|
1060
|
-
},
|
|
1061
|
-
setVerifiedWallet: (addr) => pluginCtx.setVerifiedWallet(addr)
|
|
1212
|
+
|
|
1213
|
+
// src/protocols/mpp/transaction-mode.ts
|
|
1214
|
+
async function verifyTxMode(args, info) {
|
|
1215
|
+
const { deps, price, routeEntry } = args;
|
|
1216
|
+
if (!deps.tempoClient) {
|
|
1217
|
+
return {
|
|
1218
|
+
ok: false,
|
|
1219
|
+
kind: "config",
|
|
1220
|
+
message: "tempoClient not configured for MPP transaction-payload mode"
|
|
1062
1221
|
};
|
|
1063
|
-
let rawResult;
|
|
1064
|
-
let handlerError;
|
|
1065
|
-
const response = await safeCallHandler(
|
|
1066
|
-
async (c) => {
|
|
1067
|
-
rawResult = await handler(c);
|
|
1068
|
-
return rawResult;
|
|
1069
|
-
},
|
|
1070
|
-
ctx,
|
|
1071
|
-
{
|
|
1072
|
-
onError(error) {
|
|
1073
|
-
handlerError = error;
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
);
|
|
1077
|
-
return { response, rawResult, handlerError };
|
|
1078
1222
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1223
|
+
try {
|
|
1224
|
+
const serializedTx = info.credential.payload.signature;
|
|
1225
|
+
const transaction = TempoTransaction.deserialize(serializedTx);
|
|
1226
|
+
await viemCall(deps.tempoClient, {
|
|
1227
|
+
...transaction,
|
|
1228
|
+
account: transaction.from,
|
|
1229
|
+
calls: transaction.calls ?? []
|
|
1230
|
+
});
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1233
|
+
console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
|
|
1234
|
+
return { ok: false, kind: "invalid" };
|
|
1235
|
+
}
|
|
1236
|
+
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1237
|
+
const payment = {
|
|
1238
|
+
protocol: "mpp",
|
|
1239
|
+
status: "verified",
|
|
1240
|
+
payer: info.wallet,
|
|
1241
|
+
amount: price,
|
|
1242
|
+
network: "tempo:4217",
|
|
1243
|
+
...mppRecipient ? { recipient: mppRecipient } : {}
|
|
1244
|
+
};
|
|
1245
|
+
return {
|
|
1246
|
+
ok: true,
|
|
1247
|
+
wallet: info.wallet,
|
|
1248
|
+
payment,
|
|
1249
|
+
token: { mode: "transaction", credential: info.credential },
|
|
1250
|
+
alreadySettled: false
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
async function settleTxMode(args) {
|
|
1254
|
+
const { request, response, payment, deps, routeEntry } = args;
|
|
1255
|
+
if (!deps.mppx) {
|
|
1256
|
+
return {
|
|
1257
|
+
ok: false,
|
|
1258
|
+
error: new Error("mppx unavailable"),
|
|
1259
|
+
failMessage: "MPP not initialized",
|
|
1260
|
+
failStatus: 500
|
|
1261
|
+
};
|
|
1082
1262
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1263
|
+
let result;
|
|
1264
|
+
try {
|
|
1265
|
+
result = await deps.mppx.charge({ amount: payment.amount })(request);
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1268
|
+
console.error(`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`);
|
|
1269
|
+
return {
|
|
1270
|
+
ok: false,
|
|
1271
|
+
error: err,
|
|
1272
|
+
failMessage: `MPP payment processing failed: ${message}`,
|
|
1273
|
+
failStatus: 500
|
|
1274
|
+
};
|
|
1087
1275
|
}
|
|
1088
|
-
|
|
1276
|
+
if (result.status === 402) {
|
|
1277
|
+
const reason = await readChallengeReason(result.challenge);
|
|
1278
|
+
const detail = reason || "transaction reverted on-chain after handler execution";
|
|
1279
|
+
const settlementError = Object.assign(new Error(detail), {
|
|
1280
|
+
status: 402,
|
|
1281
|
+
detail,
|
|
1282
|
+
mppResult: result,
|
|
1283
|
+
challenge: result.challenge
|
|
1284
|
+
});
|
|
1285
|
+
console.error(`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`);
|
|
1089
1286
|
return {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
account: scope.account,
|
|
1095
|
-
payment: scope.payment,
|
|
1096
|
-
response: scope.response,
|
|
1097
|
-
result: scope.rawResult
|
|
1287
|
+
ok: false,
|
|
1288
|
+
error: settlementError,
|
|
1289
|
+
failMessage: `MPP payment failed: ${detail}`,
|
|
1290
|
+
failStatus: 500
|
|
1098
1291
|
};
|
|
1099
1292
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1293
|
+
const receiptResponse = result.withReceipt(response);
|
|
1294
|
+
receiptResponse.headers.set("Cache-Control", "private");
|
|
1295
|
+
const receiptHeader = receiptResponse.headers.get(HEADERS.MPP_PAYMENT_RECEIPT) ?? void 0;
|
|
1296
|
+
const txHash = extractTxHash(receiptHeader);
|
|
1297
|
+
const settledPayment = {
|
|
1298
|
+
...payment,
|
|
1299
|
+
status: "settled",
|
|
1300
|
+
...txHash ? { transaction: txHash } : {},
|
|
1301
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1302
|
+
};
|
|
1303
|
+
return { ok: true, response: receiptResponse, settledPayment };
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/protocols/mpp/hash-mode.ts
|
|
1307
|
+
async function verifyHashMode(args, info) {
|
|
1308
|
+
const { deps, price, routeEntry, request } = args;
|
|
1309
|
+
if (!deps.mppx) {
|
|
1310
|
+
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
|
+
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
1312
|
+
return { ok: false, kind: "config", message: reason };
|
|
1313
|
+
}
|
|
1314
|
+
let chargeResult;
|
|
1315
|
+
try {
|
|
1316
|
+
chargeResult = await deps.mppx.charge({ amount: price })(request);
|
|
1317
|
+
} catch (err) {
|
|
1318
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1319
|
+
console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
|
|
1320
|
+
return { ok: false, kind: "config", message: `MPP payment processing failed: ${message}` };
|
|
1321
|
+
}
|
|
1322
|
+
if (chargeResult.status === 402) {
|
|
1323
|
+
const reason = await readChallengeReason(chargeResult.challenge);
|
|
1324
|
+
const detail = reason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
|
|
1325
|
+
console.warn(`[router] ${routeEntry.key}: MPP credential rejected \u2014 ${detail}`);
|
|
1326
|
+
return { ok: false, kind: "invalid" };
|
|
1327
|
+
}
|
|
1328
|
+
const receiptHeader = chargeResult.withReceipt(new Response()).headers.get(
|
|
1329
|
+
HEADERS.MPP_PAYMENT_RECEIPT
|
|
1330
|
+
);
|
|
1331
|
+
const txHash = extractTxHash(receiptHeader);
|
|
1332
|
+
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1333
|
+
const payment = {
|
|
1334
|
+
protocol: "mpp",
|
|
1335
|
+
status: "settled",
|
|
1336
|
+
payer: info.wallet,
|
|
1337
|
+
amount: price,
|
|
1338
|
+
network: "tempo:4217",
|
|
1339
|
+
...mppRecipient ? { recipient: mppRecipient } : {},
|
|
1340
|
+
...txHash ? { transaction: txHash } : {},
|
|
1341
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1342
|
+
};
|
|
1343
|
+
return {
|
|
1344
|
+
ok: true,
|
|
1345
|
+
wallet: info.wallet,
|
|
1346
|
+
payment,
|
|
1347
|
+
token: { mode: "hash", charge: chargeResult },
|
|
1348
|
+
alreadySettled: true
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function settleHashMode(args) {
|
|
1352
|
+
const { response, payment, token } = args;
|
|
1353
|
+
const hashToken = token;
|
|
1354
|
+
const receiptResponse = hashToken.charge.withReceipt(response);
|
|
1355
|
+
receiptResponse.headers.set("Cache-Control", "private");
|
|
1356
|
+
return {
|
|
1357
|
+
ok: true,
|
|
1358
|
+
response: receiptResponse,
|
|
1359
|
+
settledPayment: payment
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/protocols/mpp/strategy.ts
|
|
1364
|
+
var mppStrategy = {
|
|
1365
|
+
protocol: "mpp",
|
|
1366
|
+
detects(request) {
|
|
1367
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
1368
|
+
return Boolean(auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT));
|
|
1369
|
+
},
|
|
1370
|
+
async verify(args) {
|
|
1371
|
+
const info = readMppCredential(args.request);
|
|
1372
|
+
if (!info) return { ok: false, kind: "invalid" };
|
|
1373
|
+
if (info.payloadType === "transaction" && args.deps.tempoClient) {
|
|
1374
|
+
return verifyTxMode(args, info);
|
|
1375
|
+
}
|
|
1376
|
+
return verifyHashMode(args, info);
|
|
1377
|
+
},
|
|
1378
|
+
async settle(args) {
|
|
1379
|
+
const token = args.token;
|
|
1380
|
+
if (token.mode === "transaction") return settleTxMode(args);
|
|
1381
|
+
return settleHashMode(args);
|
|
1382
|
+
},
|
|
1383
|
+
async buildChallenge(args) {
|
|
1384
|
+
if (!args.deps.mppx) return {};
|
|
1103
1385
|
try {
|
|
1104
|
-
await
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
scope.parsedBody
|
|
1386
|
+
const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
|
|
1387
|
+
if (result.status === 402) {
|
|
1388
|
+
const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
1389
|
+
if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
|
|
1390
|
+
}
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
console.warn(
|
|
1393
|
+
`[router] MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1113
1394
|
);
|
|
1395
|
+
throw err;
|
|
1114
1396
|
}
|
|
1397
|
+
return {};
|
|
1115
1398
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// src/protocols/x402/strategy.ts
|
|
1402
|
+
init_accepts();
|
|
1403
|
+
|
|
1404
|
+
// src/protocols/x402/challenge.ts
|
|
1405
|
+
init_facilitators();
|
|
1406
|
+
init_solana();
|
|
1407
|
+
|
|
1408
|
+
// src/protocols/x402/requirements.ts
|
|
1409
|
+
init_evm();
|
|
1410
|
+
init_solana();
|
|
1411
|
+
async function buildExpectedRequirements(server, request, price, accepts) {
|
|
1412
|
+
const exactRequirements = await buildExactRequirements(server, request, price, accepts);
|
|
1413
|
+
const customRequirements = buildCustomRequirements(price, accepts);
|
|
1414
|
+
return [...exactRequirements, ...customRequirements];
|
|
1415
|
+
}
|
|
1416
|
+
async function buildExactRequirements(server, request, price, accepts) {
|
|
1417
|
+
const exactGroups = [
|
|
1418
|
+
buildEvmExactOptions(accepts, price),
|
|
1419
|
+
buildSolanaExactOptions(accepts, price)
|
|
1420
|
+
].filter((options) => options.length > 0);
|
|
1421
|
+
if (exactGroups.length === 0) return [];
|
|
1422
|
+
const requirements = [];
|
|
1423
|
+
const failures = [];
|
|
1424
|
+
for (const options of exactGroups) {
|
|
1119
1425
|
try {
|
|
1120
|
-
|
|
1121
|
-
...
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
});
|
|
1426
|
+
requirements.push(
|
|
1427
|
+
...await server.buildPaymentRequirementsFromOptions(options, { request })
|
|
1428
|
+
);
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1431
|
+
failures.push(err);
|
|
1432
|
+
if (exactGroups.length === 1) {
|
|
1433
|
+
throw err;
|
|
1434
|
+
}
|
|
1435
|
+
console.warn(
|
|
1436
|
+
`[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
|
|
1437
|
+
);
|
|
1133
1438
|
}
|
|
1134
1439
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
if (!hook) return;
|
|
1138
|
-
try {
|
|
1139
|
-
await hook(settlementContext(scope));
|
|
1140
|
-
} catch (error) {
|
|
1141
|
-
const message = errorMessage(error, "Post-settlement hook failed");
|
|
1142
|
-
console.error(`[router] ${routeEntry.key}: afterSettle failed: ${message}`);
|
|
1143
|
-
firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
|
|
1144
|
-
level: "error",
|
|
1145
|
-
message: `Post-settlement hook failed: ${message}`,
|
|
1146
|
-
route: routeEntry.key
|
|
1147
|
-
});
|
|
1148
|
-
await runSettlementError(scope, error, "afterSettle");
|
|
1149
|
-
}
|
|
1440
|
+
if (requirements.length > 0) {
|
|
1441
|
+
return requirements;
|
|
1150
1442
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
console.error(`[router] ${routeEntry.key}: onSettledHandlerError failed: ${message}`);
|
|
1162
|
-
firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
|
|
1163
|
-
level: "error",
|
|
1164
|
-
message: `Settled handler error hook failed: ${message}`,
|
|
1165
|
-
route: routeEntry.key
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1443
|
+
throw failures[0] ?? new Error("Failed to build x402 exact requirements");
|
|
1444
|
+
}
|
|
1445
|
+
function buildCustomRequirements(price, accepts) {
|
|
1446
|
+
return accepts.filter((accept) => accept.scheme !== "exact").map((accept) => buildCustomRequirement(price, accept));
|
|
1447
|
+
}
|
|
1448
|
+
function buildCustomRequirement(price, accept) {
|
|
1449
|
+
if (!accept.asset) {
|
|
1450
|
+
throw new Error(
|
|
1451
|
+
`Custom x402 accept '${accept.scheme}' on '${accept.network}' is missing asset`
|
|
1452
|
+
);
|
|
1168
1453
|
}
|
|
1169
|
-
return
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
} else {
|
|
1242
|
-
firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
|
|
1243
|
-
return earlyBodyResult.response;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
if (routeEntry.authMode === "siwx" || routeEntry.siwxEnabled) {
|
|
1247
|
-
if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get("SIGN-IN-WITH-X")) {
|
|
1248
|
-
const requestForValidation = request.clone();
|
|
1249
|
-
const earlyBodyResult = await parseBody(requestForValidation, routeEntry);
|
|
1250
|
-
if (earlyBodyResult.ok) {
|
|
1251
|
-
try {
|
|
1252
|
-
await routeEntry.validateFn(earlyBodyResult.data);
|
|
1253
|
-
} catch (err) {
|
|
1254
|
-
const status = err.status ?? 400;
|
|
1255
|
-
const message = err instanceof Error ? err.message : "Validation failed";
|
|
1256
|
-
return fail(status, message, meta, pluginCtx, earlyBodyResult.data);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
const siwxHeader = request.headers.get("SIGN-IN-WITH-X");
|
|
1261
|
-
if (!siwxHeader && protocol === "mpp" && routeEntry.authMode === "siwx" && deps.mppx) {
|
|
1262
|
-
let mppSiwxResult;
|
|
1263
|
-
try {
|
|
1264
|
-
mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
|
|
1265
|
-
} catch (err) {
|
|
1266
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1267
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1268
|
-
level: "critical",
|
|
1269
|
-
message: `MPP SIWX verification failed: ${message}`,
|
|
1270
|
-
route: routeEntry.key
|
|
1271
|
-
});
|
|
1272
|
-
return fail(500, `MPP SIWX verification failed: ${message}`, meta, pluginCtx);
|
|
1273
|
-
}
|
|
1274
|
-
if (mppSiwxResult.valid) {
|
|
1275
|
-
pluginCtx.setVerifiedWallet(mppSiwxResult.wallet);
|
|
1276
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1277
|
-
authMode: "siwx",
|
|
1278
|
-
wallet: mppSiwxResult.wallet,
|
|
1279
|
-
route: routeEntry.key
|
|
1280
|
-
});
|
|
1281
|
-
const authResponse = await handleAuth(mppSiwxResult.wallet, void 0);
|
|
1282
|
-
if (authResponse.status < 400) {
|
|
1283
|
-
return mppSiwxResult.withReceipt(authResponse);
|
|
1284
|
-
}
|
|
1285
|
-
return authResponse;
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
if (!siwxHeader && routeEntry.authMode === "siwx") {
|
|
1289
|
-
const url = new URL(request.url);
|
|
1290
|
-
const nonce = crypto.randomUUID().replace(/-/g, "");
|
|
1291
|
-
const supportedChains = getSupportedChains(deps.x402Accepts, deps.network);
|
|
1292
|
-
const primaryChain = supportedChains[0];
|
|
1293
|
-
const siwxInfo = {
|
|
1294
|
-
domain: url.hostname,
|
|
1295
|
-
uri: request.url,
|
|
1296
|
-
version: "1",
|
|
1297
|
-
chainId: primaryChain.chainId,
|
|
1298
|
-
type: primaryChain.type,
|
|
1299
|
-
nonce,
|
|
1300
|
-
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1301
|
-
expirationTime: new Date(Date.now() + SIWX_CHALLENGE_EXPIRY_MS).toISOString(),
|
|
1302
|
-
statement: "Sign in to verify your wallet identity"
|
|
1303
|
-
};
|
|
1304
|
-
let siwxSchema;
|
|
1305
|
-
try {
|
|
1306
|
-
siwxSchema = await buildSIWXExtension();
|
|
1307
|
-
} catch {
|
|
1308
|
-
}
|
|
1309
|
-
const paymentRequired = {
|
|
1310
|
-
x402Version: 2,
|
|
1311
|
-
error: "SIWX authentication required",
|
|
1312
|
-
resource: {
|
|
1313
|
-
url: request.url,
|
|
1314
|
-
description: routeEntry.description ?? "SIWX-protected endpoint",
|
|
1315
|
-
mimeType: "application/json"
|
|
1316
|
-
},
|
|
1317
|
-
accepts: [],
|
|
1318
|
-
extensions: {
|
|
1319
|
-
"sign-in-with-x": {
|
|
1320
|
-
info: siwxInfo,
|
|
1321
|
-
// supportedChains at top level required by MCP tools for chain detection
|
|
1322
|
-
supportedChains,
|
|
1323
|
-
...siwxSchema ? { schema: siwxSchema } : {}
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
};
|
|
1327
|
-
let encoded;
|
|
1328
|
-
try {
|
|
1329
|
-
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
1330
|
-
encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1331
|
-
} catch (err) {
|
|
1332
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1333
|
-
level: "warn",
|
|
1334
|
-
message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1335
|
-
route: routeEntry.key
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
const response = new NextResponse2(JSON.stringify(paymentRequired), {
|
|
1339
|
-
status: 402,
|
|
1340
|
-
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
1341
|
-
});
|
|
1342
|
-
if (encoded) response.headers.set("PAYMENT-REQUIRED", encoded);
|
|
1343
|
-
if (deps.mppx) {
|
|
1344
|
-
try {
|
|
1345
|
-
const mppChallenge = await deps.mppx.charge({ amount: "0" })(request);
|
|
1346
|
-
if (mppChallenge.status === 402) {
|
|
1347
|
-
const wwwAuth = mppChallenge.challenge.headers.get("WWW-Authenticate");
|
|
1348
|
-
if (wwwAuth) response.headers.set("WWW-Authenticate", wwwAuth);
|
|
1349
|
-
}
|
|
1350
|
-
} catch {
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
firePluginResponse(deps, pluginCtx, meta, response);
|
|
1354
|
-
return response;
|
|
1355
|
-
}
|
|
1356
|
-
if (siwxHeader) {
|
|
1357
|
-
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
1358
|
-
if (!siwx.valid) {
|
|
1359
|
-
if (routeEntry.authMode === "siwx") {
|
|
1360
|
-
const response = NextResponse2.json(
|
|
1361
|
-
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
1362
|
-
{ status: 402 }
|
|
1363
|
-
);
|
|
1364
|
-
firePluginResponse(deps, pluginCtx, meta, response);
|
|
1365
|
-
return response;
|
|
1366
|
-
}
|
|
1367
|
-
} else {
|
|
1368
|
-
const wallet = normalizeWalletAddress(siwx.wallet);
|
|
1369
|
-
pluginCtx.setVerifiedWallet(wallet);
|
|
1370
|
-
if (routeEntry.authMode === "siwx") {
|
|
1371
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1372
|
-
authMode: "siwx",
|
|
1373
|
-
wallet,
|
|
1374
|
-
route: routeEntry.key
|
|
1375
|
-
});
|
|
1376
|
-
return handleAuth(wallet, void 0);
|
|
1377
|
-
}
|
|
1378
|
-
if (routeEntry.siwxEnabled && routeEntry.pricing) {
|
|
1379
|
-
const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
|
|
1380
|
-
if (entitled) {
|
|
1381
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1382
|
-
authMode: "siwx",
|
|
1383
|
-
wallet,
|
|
1384
|
-
route: routeEntry.key
|
|
1385
|
-
});
|
|
1386
|
-
return handleAuth(wallet, account);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
if (!protocol || protocol === "siwx") {
|
|
1393
|
-
if (routeEntry.pricing) {
|
|
1394
|
-
const initErrors = routeEntry.protocols.map((p) => {
|
|
1395
|
-
if (p === "x402" && deps.x402InitError) return `x402: ${deps.x402InitError}`;
|
|
1396
|
-
if (p === "mpp" && deps.mppInitError) return `mpp: ${deps.mppInitError}`;
|
|
1397
|
-
return null;
|
|
1398
|
-
}).filter(Boolean);
|
|
1399
|
-
if (initErrors.length > 0) {
|
|
1400
|
-
return fail(
|
|
1401
|
-
500,
|
|
1402
|
-
`Payment protocol initialization failed. ${initErrors.join("; ")}`,
|
|
1403
|
-
meta,
|
|
1404
|
-
pluginCtx
|
|
1405
|
-
);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
1409
|
-
}
|
|
1410
|
-
const body = await parseBody(request, routeEntry);
|
|
1411
|
-
if (!body.ok) {
|
|
1412
|
-
firePluginResponse(deps, pluginCtx, meta, body.response);
|
|
1413
|
-
return body.response;
|
|
1414
|
-
}
|
|
1415
|
-
if (routeEntry.validateFn) {
|
|
1454
|
+
return {
|
|
1455
|
+
scheme: accept.scheme,
|
|
1456
|
+
network: accept.network,
|
|
1457
|
+
amount: decimalToAtomicUnits(price, accept.decimals ?? 6),
|
|
1458
|
+
asset: accept.asset,
|
|
1459
|
+
payTo: accept.payTo,
|
|
1460
|
+
maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
|
|
1461
|
+
extra: accept.extra ?? {}
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function decimalToAtomicUnits(amount, decimals) {
|
|
1465
|
+
const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
|
|
1466
|
+
if (!match?.groups) {
|
|
1467
|
+
throw new Error(`Invalid decimal amount '${amount}'`);
|
|
1468
|
+
}
|
|
1469
|
+
const whole = match.groups.whole;
|
|
1470
|
+
const fraction = match.groups.fraction ?? "";
|
|
1471
|
+
if (fraction.length > decimals) {
|
|
1472
|
+
throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
|
|
1473
|
+
}
|
|
1474
|
+
const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
|
|
1475
|
+
return normalized === "" ? "0" : normalized;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// src/protocols/x402/challenge.ts
|
|
1479
|
+
async function buildX402Challenge(opts) {
|
|
1480
|
+
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
|
|
1481
|
+
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
1482
|
+
const resource = buildChallengeResource(request, routeEntry);
|
|
1483
|
+
const requirements = await buildChallengeRequirements(
|
|
1484
|
+
server,
|
|
1485
|
+
request,
|
|
1486
|
+
price,
|
|
1487
|
+
accepts,
|
|
1488
|
+
resource,
|
|
1489
|
+
facilitatorsByNetwork
|
|
1490
|
+
);
|
|
1491
|
+
const paymentRequired = await server.createPaymentRequiredResponse(
|
|
1492
|
+
requirements,
|
|
1493
|
+
resource,
|
|
1494
|
+
void 0,
|
|
1495
|
+
extensions
|
|
1496
|
+
);
|
|
1497
|
+
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1498
|
+
return { encoded, requirements };
|
|
1499
|
+
}
|
|
1500
|
+
async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
|
|
1501
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1502
|
+
if (!needsFacilitatorEnrichment(accepts)) return requirements;
|
|
1503
|
+
return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
|
|
1504
|
+
}
|
|
1505
|
+
function needsFacilitatorEnrichment(accepts) {
|
|
1506
|
+
return accepts.some((accept) => accept.scheme !== "exact") || hasSolanaAccepts(accepts);
|
|
1507
|
+
}
|
|
1508
|
+
async function enrichGroup(group, resource) {
|
|
1509
|
+
const accepted = await enrichRequirementsWithFacilitatorAccepts(
|
|
1510
|
+
group.facilitator,
|
|
1511
|
+
resource,
|
|
1512
|
+
group.items.map(({ requirement }) => requirement)
|
|
1513
|
+
);
|
|
1514
|
+
if (accepted.length !== group.items.length) {
|
|
1515
|
+
throw new Error(
|
|
1516
|
+
`Facilitator /accepts returned ${accepted.length} requirements for ${group.items.length} inputs on ${group.facilitator.url ?? group.facilitator.network}`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
return accepted;
|
|
1520
|
+
}
|
|
1521
|
+
async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
|
|
1522
|
+
const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
|
|
1523
|
+
if (groups.length === 0) return requirements;
|
|
1524
|
+
const results = await Promise.all(
|
|
1525
|
+
groups.map(async (group) => {
|
|
1416
1526
|
try {
|
|
1417
|
-
await
|
|
1527
|
+
return { success: true, group, accepted: await enrichGroup(group, resource) };
|
|
1418
1528
|
} catch (err) {
|
|
1419
|
-
const
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
}
|
|
1424
|
-
const priceResult = await resolveDynamicPrice(body.data, routeEntry, deps, pluginCtx, meta);
|
|
1425
|
-
if ("error" in priceResult) return priceResult.error;
|
|
1426
|
-
const price = priceResult.price;
|
|
1427
|
-
if (!routeEntry.protocols.includes(protocol)) {
|
|
1428
|
-
const accepted = routeEntry.protocols.join(", ") || "none";
|
|
1429
|
-
console.warn(
|
|
1430
|
-
`[router] ${routeEntry.key}: received ${protocol} payment but route accepts [${accepted}]`
|
|
1431
|
-
);
|
|
1432
|
-
return fail(
|
|
1433
|
-
400,
|
|
1434
|
-
`This route does not accept ${protocol} payments. Accepted protocols: ${accepted}`,
|
|
1435
|
-
meta,
|
|
1436
|
-
pluginCtx,
|
|
1437
|
-
body.data
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1440
|
-
if (protocol === "x402") {
|
|
1441
|
-
if (!deps.x402Server) {
|
|
1442
|
-
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";
|
|
1443
|
-
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
1444
|
-
return fail(500, reason, meta, pluginCtx, body.data);
|
|
1445
|
-
}
|
|
1446
|
-
const accepts = await resolveX402Accepts(
|
|
1447
|
-
request,
|
|
1448
|
-
routeEntry,
|
|
1449
|
-
deps.x402Accepts,
|
|
1450
|
-
deps.payeeAddress,
|
|
1451
|
-
body.data
|
|
1452
|
-
);
|
|
1453
|
-
const verify = await verifyX402Payment({
|
|
1454
|
-
server: deps.x402Server,
|
|
1455
|
-
request,
|
|
1456
|
-
price,
|
|
1457
|
-
accepts
|
|
1458
|
-
});
|
|
1459
|
-
if (!verify?.valid)
|
|
1460
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1461
|
-
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
1462
|
-
const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
|
|
1463
|
-
const matchedRecipient = getRequirementRecipient(verifyRequirements);
|
|
1464
|
-
const wallet = normalizeWalletAddress(verify.payer);
|
|
1465
|
-
const payment = {
|
|
1466
|
-
protocol: "x402",
|
|
1467
|
-
status: "verified",
|
|
1468
|
-
payer: wallet,
|
|
1469
|
-
amount: price,
|
|
1470
|
-
network: matchedNetwork,
|
|
1471
|
-
...matchedRecipient ? { recipient: matchedRecipient } : {}
|
|
1472
|
-
};
|
|
1473
|
-
pluginCtx.setVerifiedWallet(wallet);
|
|
1474
|
-
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1475
|
-
protocol: "x402",
|
|
1476
|
-
payer: wallet,
|
|
1477
|
-
amount: price,
|
|
1478
|
-
network: matchedNetwork
|
|
1479
|
-
});
|
|
1480
|
-
const { response, rawResult, handlerError } = await invoke(
|
|
1481
|
-
request,
|
|
1482
|
-
meta,
|
|
1483
|
-
pluginCtx,
|
|
1484
|
-
wallet,
|
|
1485
|
-
account,
|
|
1486
|
-
body.data,
|
|
1487
|
-
payment
|
|
1488
|
-
);
|
|
1489
|
-
const settleScope = {
|
|
1490
|
-
request,
|
|
1491
|
-
meta,
|
|
1492
|
-
pluginCtx,
|
|
1493
|
-
wallet,
|
|
1494
|
-
account,
|
|
1495
|
-
parsedBody: body.data,
|
|
1496
|
-
payment,
|
|
1497
|
-
response,
|
|
1498
|
-
rawResult,
|
|
1499
|
-
handlerError
|
|
1500
|
-
};
|
|
1501
|
-
if (response.status < 400) {
|
|
1502
|
-
const validationFailure = await runBeforeSettle(settleScope);
|
|
1503
|
-
if (validationFailure) return validationFailure;
|
|
1504
|
-
try {
|
|
1505
|
-
const settle = await settleX402Payment(
|
|
1506
|
-
deps.x402Server,
|
|
1507
|
-
verifyPayload,
|
|
1508
|
-
verifyRequirements
|
|
1509
|
-
);
|
|
1510
|
-
if (!settle.result?.success) {
|
|
1511
|
-
const reason = settle.result?.errorReason || "x402 settlement returned success=false";
|
|
1512
|
-
const error = new Error(reason);
|
|
1513
|
-
error.errorReason = reason;
|
|
1514
|
-
throw error;
|
|
1515
|
-
}
|
|
1516
|
-
if (routeEntry.siwxEnabled) {
|
|
1517
|
-
try {
|
|
1518
|
-
await deps.entitlementStore.grant(routeEntry.key, wallet);
|
|
1519
|
-
} catch (error) {
|
|
1520
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1521
|
-
level: "warn",
|
|
1522
|
-
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1523
|
-
route: routeEntry.key
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
response.headers.set("PAYMENT-RESPONSE", settle.encoded);
|
|
1528
|
-
response.headers.set("Cache-Control", "private");
|
|
1529
|
-
const transaction = String(settle.result?.transaction ?? "");
|
|
1530
|
-
const settledPayment = {
|
|
1531
|
-
...payment,
|
|
1532
|
-
status: "settled",
|
|
1533
|
-
...transaction ? { transaction } : {}
|
|
1534
|
-
};
|
|
1535
|
-
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1536
|
-
protocol: "x402",
|
|
1537
|
-
payer: wallet,
|
|
1538
|
-
transaction,
|
|
1539
|
-
network: matchedNetwork
|
|
1540
|
-
});
|
|
1541
|
-
await runAfterSettle({ ...settleScope, payment: settledPayment });
|
|
1542
|
-
} catch (err) {
|
|
1543
|
-
await runSettlementError(settleScope, err, "settle");
|
|
1544
|
-
const errObj = err;
|
|
1545
|
-
console.error("Settlement failed", {
|
|
1546
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1547
|
-
route: routeEntry.key,
|
|
1548
|
-
network: matchedNetwork,
|
|
1549
|
-
errorReason: errObj.errorReason,
|
|
1550
|
-
facilitatorStatus: errObj.response?.status,
|
|
1551
|
-
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
1552
|
-
});
|
|
1553
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1554
|
-
level: "critical",
|
|
1555
|
-
message: `Settlement failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1556
|
-
route: routeEntry.key
|
|
1557
|
-
});
|
|
1558
|
-
return fail(500, "Settlement failed", meta, pluginCtx, body.data);
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
finalize(response, rawResult, meta, pluginCtx, body.data);
|
|
1562
|
-
return response;
|
|
1563
|
-
}
|
|
1564
|
-
if (protocol === "mpp") {
|
|
1565
|
-
if (!deps.mppx) {
|
|
1566
|
-
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";
|
|
1567
|
-
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
1568
|
-
return fail(500, reason, meta, pluginCtx, body.data);
|
|
1569
|
-
}
|
|
1570
|
-
const mppCredential = Credential2.fromRequest(request);
|
|
1571
|
-
const rawSource = mppCredential?.source ?? "";
|
|
1572
|
-
const didParts = rawSource.split(":");
|
|
1573
|
-
const lastPart = didParts[didParts.length - 1];
|
|
1574
|
-
const wallet = normalizeWalletAddress(isAddress2(lastPart) ? getAddress2(lastPart) : rawSource);
|
|
1575
|
-
const payloadType = mppCredential?.payload?.type;
|
|
1576
|
-
if (payloadType === "transaction" && deps.tempoClient) {
|
|
1577
|
-
try {
|
|
1578
|
-
const serializedTx = mppCredential.payload.signature;
|
|
1579
|
-
const transaction = TempoTransaction.deserialize(serializedTx);
|
|
1580
|
-
await viemCall(deps.tempoClient, {
|
|
1581
|
-
...transaction,
|
|
1582
|
-
account: transaction.from,
|
|
1583
|
-
calls: transaction.calls ?? []
|
|
1584
|
-
});
|
|
1585
|
-
} catch (err) {
|
|
1586
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1587
|
-
console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
|
|
1588
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1589
|
-
level: "warn",
|
|
1590
|
-
message: `MPP simulation failed: ${message}`,
|
|
1591
|
-
route: routeEntry.key
|
|
1592
|
-
});
|
|
1593
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1594
|
-
}
|
|
1595
|
-
pluginCtx.setVerifiedWallet(wallet);
|
|
1596
|
-
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1597
|
-
protocol: "mpp",
|
|
1598
|
-
payer: wallet,
|
|
1599
|
-
amount: price,
|
|
1600
|
-
network: "tempo:4217"
|
|
1601
|
-
});
|
|
1602
|
-
const mppRecipient2 = deps.mppRecipient ?? deps.payeeAddress;
|
|
1603
|
-
const payment2 = {
|
|
1604
|
-
protocol: "mpp",
|
|
1605
|
-
status: "verified",
|
|
1606
|
-
payer: wallet,
|
|
1607
|
-
amount: price,
|
|
1608
|
-
network: "tempo:4217",
|
|
1609
|
-
...mppRecipient2 ? { recipient: mppRecipient2 } : {}
|
|
1610
|
-
};
|
|
1611
|
-
const { response: response2, rawResult: rawResult2, handlerError: handlerError2 } = await invoke(
|
|
1612
|
-
request,
|
|
1613
|
-
meta,
|
|
1614
|
-
pluginCtx,
|
|
1615
|
-
wallet,
|
|
1616
|
-
account,
|
|
1617
|
-
body.data,
|
|
1618
|
-
payment2
|
|
1529
|
+
const label = group.facilitator.url ?? group.facilitator.network;
|
|
1530
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1531
|
+
console.warn(
|
|
1532
|
+
`[router] ${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
|
|
1619
1533
|
);
|
|
1620
|
-
|
|
1621
|
-
request,
|
|
1622
|
-
meta,
|
|
1623
|
-
pluginCtx,
|
|
1624
|
-
wallet,
|
|
1625
|
-
account,
|
|
1626
|
-
parsedBody: body.data,
|
|
1627
|
-
payment: payment2,
|
|
1628
|
-
response: response2,
|
|
1629
|
-
rawResult: rawResult2,
|
|
1630
|
-
handlerError: handlerError2
|
|
1631
|
-
};
|
|
1632
|
-
if (response2.status < 400) {
|
|
1633
|
-
const validationFailure = await runBeforeSettle(settleScope2);
|
|
1634
|
-
if (validationFailure) return validationFailure;
|
|
1635
|
-
let mppResult2;
|
|
1636
|
-
try {
|
|
1637
|
-
mppResult2 = await deps.mppx.charge({ amount: price })(request);
|
|
1638
|
-
} catch (err) {
|
|
1639
|
-
await runSettlementError(settleScope2, err, "settle");
|
|
1640
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1641
|
-
console.error(
|
|
1642
|
-
`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`
|
|
1643
|
-
);
|
|
1644
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1645
|
-
level: "critical",
|
|
1646
|
-
message: `MPP broadcast failed after handler: ${message}`,
|
|
1647
|
-
route: routeEntry.key
|
|
1648
|
-
});
|
|
1649
|
-
return fail(
|
|
1650
|
-
500,
|
|
1651
|
-
`MPP payment processing failed: ${message}`,
|
|
1652
|
-
meta,
|
|
1653
|
-
pluginCtx,
|
|
1654
|
-
body.data
|
|
1655
|
-
);
|
|
1656
|
-
}
|
|
1657
|
-
if (mppResult2.status === 402) {
|
|
1658
|
-
let rejectReason = "";
|
|
1659
|
-
try {
|
|
1660
|
-
const problemBody = await mppResult2.challenge.clone().text();
|
|
1661
|
-
if (problemBody) {
|
|
1662
|
-
const problem = JSON.parse(problemBody);
|
|
1663
|
-
rejectReason = problem.detail || problem.title || "";
|
|
1664
|
-
}
|
|
1665
|
-
} catch {
|
|
1666
|
-
}
|
|
1667
|
-
const detail = rejectReason || "transaction reverted on-chain after handler execution";
|
|
1668
|
-
const settlementError = Object.assign(new Error(detail), {
|
|
1669
|
-
status: 402,
|
|
1670
|
-
detail,
|
|
1671
|
-
mppResult: mppResult2,
|
|
1672
|
-
challenge: mppResult2.challenge
|
|
1673
|
-
});
|
|
1674
|
-
await runSettlementError(settleScope2, settlementError, "settle");
|
|
1675
|
-
console.error(
|
|
1676
|
-
`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`
|
|
1677
|
-
);
|
|
1678
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1679
|
-
level: "critical",
|
|
1680
|
-
message: `MPP payment failed after handler: ${detail}`,
|
|
1681
|
-
route: routeEntry.key
|
|
1682
|
-
});
|
|
1683
|
-
return fail(500, `MPP payment failed: ${detail}`, meta, pluginCtx, body.data);
|
|
1684
|
-
}
|
|
1685
|
-
const receiptResponse = mppResult2.withReceipt(response2);
|
|
1686
|
-
receiptResponse.headers.set("Cache-Control", "private");
|
|
1687
|
-
let txHash2 = "";
|
|
1688
|
-
const receiptHeader2 = receiptResponse.headers.get("Payment-Receipt");
|
|
1689
|
-
if (receiptHeader2) {
|
|
1690
|
-
try {
|
|
1691
|
-
txHash2 = Receipt.deserialize(receiptHeader2).reference;
|
|
1692
|
-
} catch {
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
if (routeEntry.siwxEnabled) {
|
|
1696
|
-
try {
|
|
1697
|
-
await deps.entitlementStore.grant(routeEntry.key, wallet);
|
|
1698
|
-
} catch (error) {
|
|
1699
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1700
|
-
level: "warn",
|
|
1701
|
-
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1702
|
-
route: routeEntry.key
|
|
1703
|
-
});
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1707
|
-
protocol: "mpp",
|
|
1708
|
-
payer: wallet,
|
|
1709
|
-
transaction: txHash2,
|
|
1710
|
-
network: "tempo:4217"
|
|
1711
|
-
});
|
|
1712
|
-
const settledPayment = {
|
|
1713
|
-
...payment2,
|
|
1714
|
-
status: "settled",
|
|
1715
|
-
...txHash2 ? { transaction: txHash2 } : {},
|
|
1716
|
-
...receiptHeader2 ? { receipt: receiptHeader2 } : {}
|
|
1717
|
-
};
|
|
1718
|
-
await runAfterSettle({
|
|
1719
|
-
...settleScope2,
|
|
1720
|
-
payment: settledPayment,
|
|
1721
|
-
response: receiptResponse
|
|
1722
|
-
});
|
|
1723
|
-
finalize(receiptResponse, rawResult2, meta, pluginCtx, body.data);
|
|
1724
|
-
return receiptResponse;
|
|
1725
|
-
}
|
|
1726
|
-
finalize(response2, rawResult2, meta, pluginCtx, body.data);
|
|
1727
|
-
return response2;
|
|
1728
|
-
}
|
|
1729
|
-
let mppResult;
|
|
1730
|
-
try {
|
|
1731
|
-
mppResult = await deps.mppx.charge({ amount: price })(request);
|
|
1732
|
-
} catch (err) {
|
|
1733
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1734
|
-
console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
|
|
1735
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1736
|
-
level: "critical",
|
|
1737
|
-
message: `MPP charge failed: ${message}`,
|
|
1738
|
-
route: routeEntry.key
|
|
1739
|
-
});
|
|
1740
|
-
return fail(500, `MPP payment processing failed: ${message}`, meta, pluginCtx, body.data);
|
|
1534
|
+
return { success: false, group };
|
|
1741
1535
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1536
|
+
})
|
|
1537
|
+
);
|
|
1538
|
+
const enriched = [...requirements];
|
|
1539
|
+
results.filter((r) => r.success).forEach(({ group, accepted }) => {
|
|
1540
|
+
accepted.forEach((req, offset) => {
|
|
1541
|
+
const index = group.items[offset]?.index;
|
|
1542
|
+
if (index !== void 0) enriched[index] = req;
|
|
1543
|
+
});
|
|
1544
|
+
});
|
|
1545
|
+
const failedIndices = new Set(
|
|
1546
|
+
results.filter((r) => !r.success).flatMap(({ group }) => group.items.map(({ index }) => index))
|
|
1547
|
+
);
|
|
1548
|
+
const remaining = enriched.filter((_, i) => !failedIndices.has(i));
|
|
1549
|
+
if (remaining.length === 0) {
|
|
1550
|
+
throw new Error(
|
|
1551
|
+
"All facilitator enrichments failed; no payment requirements remain for challenge"
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
return remaining;
|
|
1555
|
+
}
|
|
1556
|
+
function collectEnrichmentGroups(requirements, facilitatorsByNetwork) {
|
|
1557
|
+
const groups = [];
|
|
1558
|
+
requirements.forEach((requirement, index) => {
|
|
1559
|
+
if (!requiresFacilitatorEnrichment(requirement)) return;
|
|
1560
|
+
const facilitator = getRequiredFacilitator(requirement, facilitatorsByNetwork);
|
|
1561
|
+
const existing = groups.find(
|
|
1562
|
+
(group) => sameResolvedX402Facilitator(group.facilitator, facilitator)
|
|
1563
|
+
);
|
|
1564
|
+
if (existing) {
|
|
1565
|
+
existing.items.push({ index, requirement });
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
groups.push({
|
|
1569
|
+
facilitator,
|
|
1570
|
+
items: [{ index, requirement }]
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
return groups;
|
|
1574
|
+
}
|
|
1575
|
+
function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
|
|
1576
|
+
const facilitator = getFacilitatorForRequirement(facilitatorsByNetwork, requirement);
|
|
1577
|
+
if (!facilitator) {
|
|
1578
|
+
throw new Error(
|
|
1579
|
+
`Missing x402 facilitator for ${requirement.scheme} requirement on ${requirement.network}`
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
return facilitator;
|
|
1583
|
+
}
|
|
1584
|
+
function requiresFacilitatorEnrichment(requirement) {
|
|
1585
|
+
return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
|
|
1586
|
+
}
|
|
1587
|
+
function buildChallengeResource(request, routeEntry) {
|
|
1588
|
+
return {
|
|
1589
|
+
url: request.url,
|
|
1590
|
+
method: routeEntry.method,
|
|
1591
|
+
description: routeEntry.description,
|
|
1592
|
+
mimeType: "application/json"
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// src/protocols/x402/settle.ts
|
|
1597
|
+
async function settleX402Payment(server, payload, requirements) {
|
|
1598
|
+
const { encodePaymentResponseHeader } = await import("@x402/core/http");
|
|
1599
|
+
const result = await server.settlePayment(payload, requirements);
|
|
1600
|
+
const encoded = encodePaymentResponseHeader(result);
|
|
1601
|
+
return { encoded, result };
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// src/protocols/x402/verify.ts
|
|
1605
|
+
async function verifyX402Payment(opts) {
|
|
1606
|
+
const { server, request, price, accepts } = opts;
|
|
1607
|
+
const payload = await readPaymentPayload(request);
|
|
1608
|
+
if (!payload) return null;
|
|
1609
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1610
|
+
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
1611
|
+
if (!matching) {
|
|
1612
|
+
return invalidPaymentVerification();
|
|
1613
|
+
}
|
|
1614
|
+
let verify;
|
|
1615
|
+
try {
|
|
1616
|
+
verify = await server.verifyPayment(payload, matching);
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
const sc = err.statusCode;
|
|
1619
|
+
if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
|
|
1620
|
+
throw err;
|
|
1621
|
+
}
|
|
1622
|
+
if (!verify.isValid) return invalidPaymentVerification();
|
|
1623
|
+
if (typeof verify.payer !== "string" || verify.payer.length === 0) {
|
|
1624
|
+
throw new Error("x402 verification succeeded without a payer address");
|
|
1625
|
+
}
|
|
1626
|
+
return {
|
|
1627
|
+
valid: true,
|
|
1628
|
+
payer: verify.payer,
|
|
1629
|
+
payload,
|
|
1630
|
+
requirements: matching
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function findVerifiableRequirements(server, requirements, payload) {
|
|
1634
|
+
const strictMatch = server.findMatchingRequirements(requirements, payload);
|
|
1635
|
+
if (strictMatch) {
|
|
1636
|
+
return payload.x402Version === 2 ? payload.accepted : strictMatch;
|
|
1637
|
+
}
|
|
1638
|
+
if (payload.x402Version !== 2) {
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
const stableMatch = requirements.find(
|
|
1642
|
+
(requirement) => matchesStableFields(requirement, payload.accepted)
|
|
1643
|
+
);
|
|
1644
|
+
return stableMatch ? payload.accepted : null;
|
|
1645
|
+
}
|
|
1646
|
+
function matchesStableFields(requirement, accepted) {
|
|
1647
|
+
return requirement.scheme === accepted.scheme && requirement.network === accepted.network && requirement.payTo === accepted.payTo && requirement.asset === accepted.asset && requirement.amount === accepted.amount && requirement.maxTimeoutSeconds === accepted.maxTimeoutSeconds;
|
|
1648
|
+
}
|
|
1649
|
+
async function readPaymentPayload(request) {
|
|
1650
|
+
const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
|
|
1651
|
+
if (!paymentHeader) return null;
|
|
1652
|
+
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
1653
|
+
return decodePaymentSignatureHeader(paymentHeader);
|
|
1654
|
+
}
|
|
1655
|
+
function invalidPaymentVerification() {
|
|
1656
|
+
return { valid: false, payload: null, requirements: null, payer: null };
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/protocols/x402/strategy.ts
|
|
1660
|
+
var x402Strategy = {
|
|
1661
|
+
protocol: "x402",
|
|
1662
|
+
detects(request) {
|
|
1663
|
+
return Boolean(
|
|
1664
|
+
request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)
|
|
1665
|
+
);
|
|
1666
|
+
},
|
|
1667
|
+
async verify(args) {
|
|
1668
|
+
const { request, body, price, routeEntry, deps } = args;
|
|
1669
|
+
if (!deps.x402Server) {
|
|
1670
|
+
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";
|
|
1671
|
+
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
1672
|
+
return { ok: false, kind: "config", message: reason };
|
|
1673
|
+
}
|
|
1674
|
+
const accepts = await resolveX402Accepts(
|
|
1675
|
+
request,
|
|
1676
|
+
routeEntry,
|
|
1677
|
+
deps.x402Accepts,
|
|
1678
|
+
deps.payeeAddress,
|
|
1679
|
+
body
|
|
1680
|
+
);
|
|
1681
|
+
const verifyResult = await verifyX402Payment({
|
|
1682
|
+
server: deps.x402Server,
|
|
1683
|
+
request,
|
|
1684
|
+
price,
|
|
1685
|
+
accepts
|
|
1686
|
+
});
|
|
1687
|
+
if (!verifyResult?.valid) return { ok: false, kind: "invalid" };
|
|
1688
|
+
const wallet = normalizeWalletAddress(verifyResult.payer);
|
|
1689
|
+
const matchedNetwork = getRequirementNetwork(verifyResult.requirements, deps.network);
|
|
1690
|
+
const matchedRecipient = getRequirementRecipient(verifyResult.requirements);
|
|
1691
|
+
const payment = {
|
|
1692
|
+
protocol: "x402",
|
|
1693
|
+
status: "verified",
|
|
1694
|
+
payer: wallet,
|
|
1695
|
+
amount: price,
|
|
1696
|
+
network: matchedNetwork,
|
|
1697
|
+
...matchedRecipient ? { recipient: matchedRecipient } : {}
|
|
1698
|
+
};
|
|
1699
|
+
return {
|
|
1700
|
+
ok: true,
|
|
1701
|
+
wallet,
|
|
1702
|
+
payment,
|
|
1703
|
+
token: {
|
|
1704
|
+
payload: verifyResult.payload,
|
|
1705
|
+
requirements: verifyResult.requirements
|
|
1760
1706
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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
|
|
1764
1717
|
);
|
|
1765
|
-
if (
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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;
|
|
1770
1723
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
network: "tempo:4217"
|
|
1777
|
-
});
|
|
1778
|
-
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1779
|
-
const payment = {
|
|
1780
|
-
protocol: "mpp",
|
|
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,
|
|
1781
1729
|
status: "settled",
|
|
1782
|
-
|
|
1783
|
-
amount: price,
|
|
1784
|
-
network: "tempo:4217",
|
|
1785
|
-
...mppRecipient ? { recipient: mppRecipient } : {},
|
|
1786
|
-
...txHash ? { transaction: txHash } : {},
|
|
1787
|
-
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1788
|
-
};
|
|
1789
|
-
const { response, rawResult, handlerError } = await invoke(
|
|
1790
|
-
request,
|
|
1791
|
-
meta,
|
|
1792
|
-
pluginCtx,
|
|
1793
|
-
wallet,
|
|
1794
|
-
account,
|
|
1795
|
-
body.data,
|
|
1796
|
-
payment
|
|
1797
|
-
);
|
|
1798
|
-
const settleScope = {
|
|
1799
|
-
request,
|
|
1800
|
-
meta,
|
|
1801
|
-
pluginCtx,
|
|
1802
|
-
wallet,
|
|
1803
|
-
account,
|
|
1804
|
-
parsedBody: body.data,
|
|
1805
|
-
payment,
|
|
1806
|
-
response,
|
|
1807
|
-
rawResult,
|
|
1808
|
-
handlerError
|
|
1730
|
+
...transaction ? { transaction } : {}
|
|
1809
1731
|
};
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
const receiptResponse = mppResult.withReceipt(response);
|
|
1823
|
-
receiptResponse.headers.set("Cache-Control", "private");
|
|
1824
|
-
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1825
|
-
protocol: "mpp",
|
|
1826
|
-
payer: wallet,
|
|
1827
|
-
transaction: txHash,
|
|
1828
|
-
network: "tempo:4217"
|
|
1829
|
-
});
|
|
1830
|
-
await runAfterSettle({ ...settleScope, response: receiptResponse });
|
|
1831
|
-
finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
|
|
1832
|
-
return receiptResponse;
|
|
1833
|
-
}
|
|
1834
|
-
await runSettledHandlerError(settleScope);
|
|
1835
|
-
finalize(response, rawResult, meta, pluginCtx, body.data);
|
|
1836
|
-
return response;
|
|
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" };
|
|
1837
1744
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1745
|
+
},
|
|
1746
|
+
async buildChallenge(args) {
|
|
1747
|
+
const { request, routeEntry, body, price, extensions, deps } = args;
|
|
1748
|
+
if (!deps.x402Server) return {};
|
|
1749
|
+
const accepts = await resolveX402Accepts(
|
|
1750
|
+
request,
|
|
1751
|
+
routeEntry,
|
|
1752
|
+
deps.x402Accepts,
|
|
1753
|
+
deps.payeeAddress,
|
|
1754
|
+
body
|
|
1755
|
+
);
|
|
1756
|
+
const { encoded } = await buildX402Challenge({
|
|
1757
|
+
server: deps.x402Server,
|
|
1758
|
+
routeEntry,
|
|
1759
|
+
request,
|
|
1760
|
+
price,
|
|
1761
|
+
accepts,
|
|
1762
|
+
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
1763
|
+
extensions
|
|
1764
|
+
});
|
|
1765
|
+
return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
function getRequirementNetwork(requirements, fallback) {
|
|
1769
|
+
const network = requirements?.network;
|
|
1770
|
+
return typeof network === "string" ? network : fallback;
|
|
1840
1771
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1772
|
+
function getRequirementRecipient(requirements) {
|
|
1773
|
+
const payTo = requirements?.payTo;
|
|
1774
|
+
return typeof payTo === "string" ? payTo : void 0;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/protocols/detect.ts
|
|
1778
|
+
function detectProtocol(request) {
|
|
1779
|
+
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
1780
|
+
return "x402";
|
|
1781
|
+
}
|
|
1782
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
1783
|
+
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
1784
|
+
return "mpp";
|
|
1785
|
+
}
|
|
1786
|
+
if (request.headers.get(HEADERS.SIWX)) {
|
|
1787
|
+
return "siwx";
|
|
1788
|
+
}
|
|
1789
|
+
return null;
|
|
1853
1790
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
startTime: Date.now()
|
|
1867
|
-
};
|
|
1791
|
+
|
|
1792
|
+
// src/protocols/index.ts
|
|
1793
|
+
var STRATEGIES = {
|
|
1794
|
+
x402: x402Strategy,
|
|
1795
|
+
mpp: mppStrategy
|
|
1796
|
+
};
|
|
1797
|
+
function selectIncomingStrategy(request, allowed) {
|
|
1798
|
+
for (const name of allowed) {
|
|
1799
|
+
const strategy = STRATEGIES[name];
|
|
1800
|
+
if (strategy.detects(request)) return strategy;
|
|
1801
|
+
}
|
|
1802
|
+
return null;
|
|
1868
1803
|
}
|
|
1869
|
-
function
|
|
1870
|
-
|
|
1871
|
-
const params = Object.fromEntries(request.nextUrl.searchParams.entries());
|
|
1872
|
-
const result = routeEntry.querySchema.safeParse(params);
|
|
1873
|
-
return result.success ? result.data : params;
|
|
1804
|
+
function getAllowedStrategies(allowed) {
|
|
1805
|
+
return allowed.map((name) => STRATEGIES[name]);
|
|
1874
1806
|
}
|
|
1875
|
-
|
|
1807
|
+
|
|
1808
|
+
// src/pipeline/challenge.ts
|
|
1809
|
+
import { NextResponse as NextResponse4 } from "next/server";
|
|
1810
|
+
async function build402(ctx, pricing, body) {
|
|
1811
|
+
let challengePrice;
|
|
1876
1812
|
try {
|
|
1877
|
-
|
|
1878
|
-
if (routeEntry.maxPrice) {
|
|
1879
|
-
const calculated = parseFloat(price);
|
|
1880
|
-
const max = parseFloat(routeEntry.maxPrice);
|
|
1881
|
-
if (!Number.isFinite(calculated) || calculated > max) {
|
|
1882
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1883
|
-
level: "warn",
|
|
1884
|
-
message: `Price ${price} exceeds maxPrice ${routeEntry.maxPrice}, capping`,
|
|
1885
|
-
route: routeEntry.key,
|
|
1886
|
-
meta: { calculated: price, maxPrice: routeEntry.maxPrice, body: bodyData }
|
|
1887
|
-
});
|
|
1888
|
-
price = routeEntry.maxPrice;
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
return { price };
|
|
1813
|
+
challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
|
|
1892
1814
|
} catch (err) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1901
|
-
level: "warn",
|
|
1902
|
-
message: `Using maxPrice ${routeEntry.maxPrice} as fallback after pricing error`,
|
|
1903
|
-
route: routeEntry.key
|
|
1904
|
-
});
|
|
1905
|
-
return { price: routeEntry.maxPrice };
|
|
1906
|
-
} else {
|
|
1907
|
-
const message = errorMessage(err, "Price calculation failed");
|
|
1908
|
-
const errorResponse = NextResponse2.json(
|
|
1909
|
-
{ success: false, error: message },
|
|
1910
|
-
{ status: errorStatus(err, 500) }
|
|
1911
|
-
);
|
|
1912
|
-
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
1913
|
-
return { error: errorResponse };
|
|
1914
|
-
}
|
|
1815
|
+
const message = errorMessage(err, "Price calculation failed");
|
|
1816
|
+
const errorResponse = NextResponse4.json(
|
|
1817
|
+
{ success: false, error: message },
|
|
1818
|
+
{ status: errorStatus(err, 500) }
|
|
1819
|
+
);
|
|
1820
|
+
firePluginResponse(ctx, errorResponse);
|
|
1821
|
+
return errorResponse;
|
|
1915
1822
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
const response = new NextResponse2(null, {
|
|
1823
|
+
const extensions = await buildChallengeExtensions(ctx);
|
|
1824
|
+
const response = new NextResponse4(null, {
|
|
1919
1825
|
status: 402,
|
|
1920
1826
|
headers: {
|
|
1921
1827
|
"Content-Type": "application/json",
|
|
1922
1828
|
"Cache-Control": "no-store"
|
|
1923
1829
|
}
|
|
1924
1830
|
});
|
|
1925
|
-
|
|
1926
|
-
if (bodyData !== void 0 && typeof routeEntry.pricing !== "string" && routeEntry.pricing != null) {
|
|
1927
|
-
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
|
1928
|
-
if ("error" in result) return result.error;
|
|
1929
|
-
challengePrice = result.price;
|
|
1930
|
-
} else if (routeEntry.maxPrice) {
|
|
1931
|
-
challengePrice = routeEntry.maxPrice;
|
|
1932
|
-
} else if (routeEntry.pricing) {
|
|
1831
|
+
for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
|
|
1933
1832
|
try {
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1833
|
+
const contribution = await strategy.buildChallenge({
|
|
1834
|
+
request: ctx.request,
|
|
1835
|
+
routeEntry: ctx.routeEntry,
|
|
1836
|
+
body,
|
|
1837
|
+
price: challengePrice,
|
|
1838
|
+
extensions,
|
|
1839
|
+
deps: ctx.deps
|
|
1840
|
+
});
|
|
1841
|
+
if (contribution.headers) {
|
|
1842
|
+
for (const [name, value] of Object.entries(contribution.headers)) {
|
|
1843
|
+
response.headers.set(name, value);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
} catch (err) {
|
|
1847
|
+
const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
|
|
1848
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
1849
|
+
level: "critical",
|
|
1850
|
+
message,
|
|
1851
|
+
route: ctx.routeEntry.key
|
|
1852
|
+
});
|
|
1853
|
+
if (strategy.protocol === "x402") {
|
|
1854
|
+
const errorResponse = NextResponse4.json(
|
|
1855
|
+
{ success: false, error: message },
|
|
1856
|
+
{ status: 500 }
|
|
1857
|
+
);
|
|
1858
|
+
firePluginResponse(ctx, errorResponse);
|
|
1859
|
+
return errorResponse;
|
|
1860
|
+
}
|
|
1937
1861
|
}
|
|
1938
|
-
} else {
|
|
1939
|
-
challengePrice = "0";
|
|
1940
1862
|
}
|
|
1863
|
+
firePluginResponse(ctx, response);
|
|
1864
|
+
return response;
|
|
1865
|
+
}
|
|
1866
|
+
async function buildChallengeExtensions(ctx) {
|
|
1867
|
+
const { routeEntry } = ctx;
|
|
1941
1868
|
let extensions;
|
|
1942
1869
|
try {
|
|
1943
1870
|
const { z } = await import("zod");
|
|
@@ -1963,7 +1890,7 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
|
1963
1890
|
extensions = declareDiscoveryExtension(config);
|
|
1964
1891
|
}
|
|
1965
1892
|
} catch (err) {
|
|
1966
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1893
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
1967
1894
|
level: "warn",
|
|
1968
1895
|
message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1969
1896
|
route: routeEntry.key
|
|
@@ -1981,101 +1908,390 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
|
1981
1908
|
} catch {
|
|
1982
1909
|
}
|
|
1983
1910
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
1911
|
+
return extensions;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/pipeline/flows/paid.ts
|
|
1915
|
+
async function runPaidFlow(ctx) {
|
|
1916
|
+
const { request, routeEntry, deps } = ctx;
|
|
1917
|
+
let account = void 0;
|
|
1918
|
+
if (routeEntry.apiKeyResolver) {
|
|
1919
|
+
const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
|
|
1920
|
+
if (!apiKeyResult.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
1921
|
+
account = apiKeyResult.account;
|
|
1922
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
1923
|
+
authMode: "apiKey",
|
|
1924
|
+
wallet: null,
|
|
1925
|
+
route: routeEntry.key,
|
|
1926
|
+
account
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
const alertFn = (level, message, meta) => {
|
|
1930
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
1931
|
+
level,
|
|
1932
|
+
message,
|
|
1933
|
+
route: routeEntry.key,
|
|
1934
|
+
meta
|
|
1935
|
+
});
|
|
1936
|
+
};
|
|
1937
|
+
const pricing = selectPricing(routeEntry.pricing, {
|
|
1938
|
+
alert: alertFn,
|
|
1939
|
+
maxPrice: routeEntry.maxPrice,
|
|
1940
|
+
minPrice: routeEntry.minPrice,
|
|
1941
|
+
route: routeEntry.key
|
|
1942
|
+
});
|
|
1943
|
+
const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
|
|
1944
|
+
let earlyBody = void 0;
|
|
1945
|
+
if (shouldParseBodyEarly(incomingStrategy, routeEntry, pricing)) {
|
|
1946
|
+
const earlyClone = request.clone();
|
|
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
|
+
}
|
|
1957
|
+
const siwxFastPath = await trySiwxFastPath(ctx, account);
|
|
1958
|
+
if (siwxFastPath) return siwxFastPath;
|
|
1959
|
+
if (!incomingStrategy) {
|
|
1960
|
+
const initError = protocolInitError(routeEntry, deps);
|
|
1961
|
+
if (initError) return fail(ctx, 500, initError);
|
|
1962
|
+
return build402(ctx, pricing, earlyBody);
|
|
1963
|
+
}
|
|
1964
|
+
const body = await parseBody(request, routeEntry);
|
|
1965
|
+
if (!body.ok) {
|
|
1966
|
+
firePluginResponse(ctx, body.response);
|
|
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(
|
|
1979
|
+
ctx,
|
|
1980
|
+
errorStatus(err, 500),
|
|
1981
|
+
errorMessage(err, "Price calculation failed"),
|
|
1982
|
+
body.data
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
const verifyOutcome = await incomingStrategy.verify({
|
|
1986
|
+
request,
|
|
1987
|
+
body: body.data,
|
|
1988
|
+
price,
|
|
1989
|
+
routeEntry,
|
|
1990
|
+
deps
|
|
1991
|
+
});
|
|
1992
|
+
if (verifyOutcome.ok === false) {
|
|
1993
|
+
if (verifyOutcome.kind === "config") {
|
|
1994
|
+
return fail(ctx, 500, verifyOutcome.message, body.data);
|
|
1995
|
+
}
|
|
1996
|
+
return build402(ctx, pricing, body.data);
|
|
1997
|
+
}
|
|
1998
|
+
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
1999
|
+
firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
|
|
2000
|
+
protocol: incomingStrategy.protocol,
|
|
2001
|
+
payer: verifyOutcome.wallet,
|
|
2002
|
+
amount: price,
|
|
2003
|
+
network: verifyOutcome.payment.network
|
|
2004
|
+
});
|
|
2005
|
+
const result = await invoke(ctx, verifyOutcome.wallet, account, body.data, verifyOutcome.payment);
|
|
2006
|
+
const settleScope = {
|
|
2007
|
+
wallet: verifyOutcome.wallet,
|
|
2008
|
+
account,
|
|
2009
|
+
body: body.data,
|
|
2010
|
+
payment: verifyOutcome.payment,
|
|
2011
|
+
response: result.response,
|
|
2012
|
+
rawResult: result.rawResult,
|
|
2013
|
+
handlerError: result.handlerError
|
|
2014
|
+
};
|
|
2015
|
+
if (verifyOutcome.alreadySettled) {
|
|
2016
|
+
if (result.response.status >= 400) {
|
|
2017
|
+
const settledScope = settleScope;
|
|
2018
|
+
await runSettledHandlerError(ctx, settledScope);
|
|
2019
|
+
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
2020
|
+
}
|
|
2021
|
+
return settleAndFinalize({
|
|
2022
|
+
ctx,
|
|
2023
|
+
strategy: incomingStrategy,
|
|
2024
|
+
verifyOutcome,
|
|
2025
|
+
scope: settleScope,
|
|
2026
|
+
rawResult: result.rawResult,
|
|
2027
|
+
body: body.data
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
if (result.response.status >= 400) {
|
|
2031
|
+
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
2032
|
+
}
|
|
2033
|
+
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2034
|
+
if (beforeErr) return beforeErr;
|
|
2035
|
+
return settleAndFinalize({
|
|
2036
|
+
ctx,
|
|
2037
|
+
strategy: incomingStrategy,
|
|
2038
|
+
verifyOutcome,
|
|
2039
|
+
scope: settleScope,
|
|
2040
|
+
rawResult: result.rawResult,
|
|
2041
|
+
body: body.data,
|
|
2042
|
+
onSettleError: async (error, failMessage) => {
|
|
2043
|
+
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2044
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2006
2045
|
level: "critical",
|
|
2007
|
-
message,
|
|
2046
|
+
message: `${incomingStrategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
|
|
2008
2047
|
route: routeEntry.key
|
|
2009
2048
|
});
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2049
|
+
}
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// src/pipeline/flows/siwx-only.ts
|
|
2054
|
+
import { NextResponse as NextResponse5 } from "next/server";
|
|
2055
|
+
|
|
2056
|
+
// src/auth/nonce.ts
|
|
2057
|
+
var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
|
|
2058
|
+
var MemoryNonceStore = class {
|
|
2059
|
+
seen = /* @__PURE__ */ new Map();
|
|
2060
|
+
async check(nonce) {
|
|
2061
|
+
this.evict();
|
|
2062
|
+
if (this.seen.has(nonce)) return false;
|
|
2063
|
+
this.seen.set(nonce, Date.now() + SIWX_CHALLENGE_EXPIRY_MS);
|
|
2064
|
+
return true;
|
|
2065
|
+
}
|
|
2066
|
+
evict() {
|
|
2067
|
+
const now = Date.now();
|
|
2068
|
+
for (const [n, exp] of this.seen) {
|
|
2069
|
+
if (exp < now) this.seen.delete(n);
|
|
2013
2070
|
}
|
|
2014
2071
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2072
|
+
};
|
|
2073
|
+
function detectRedisClientType(client) {
|
|
2074
|
+
if (!client || typeof client !== "object") {
|
|
2075
|
+
throw new Error(
|
|
2076
|
+
"createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
if ("options" in client && "status" in client) {
|
|
2080
|
+
return "ioredis";
|
|
2081
|
+
}
|
|
2082
|
+
const constructor = client.constructor?.name;
|
|
2083
|
+
if (constructor === "Redis" && "url" in client) {
|
|
2084
|
+
return "upstash";
|
|
2085
|
+
}
|
|
2086
|
+
if (typeof client.set === "function") {
|
|
2087
|
+
return "upstash";
|
|
2088
|
+
}
|
|
2089
|
+
throw new Error(
|
|
2090
|
+
"Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
|
|
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);
|
|
2097
|
+
return {
|
|
2098
|
+
async check(nonce) {
|
|
2099
|
+
const key = `${prefix}${nonce}`;
|
|
2100
|
+
if (clientType === "upstash") {
|
|
2101
|
+
const redis = client;
|
|
2102
|
+
const result = await redis.set(key, "1", { ex: ttlSeconds, nx: true });
|
|
2103
|
+
return result !== null;
|
|
2021
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");
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// src/protocols/mpp/siwx-mode.ts
|
|
2116
|
+
import { Credential as Credential2 } from "mppx";
|
|
2117
|
+
async function verifyMppSiwx(request, mppx) {
|
|
2118
|
+
const result = await mppx.charge({ amount: "0" })(request);
|
|
2119
|
+
if (result.status === 402) {
|
|
2120
|
+
return { valid: false, challenge: result.challenge };
|
|
2121
|
+
}
|
|
2122
|
+
const credential = Credential2.fromRequest(request);
|
|
2123
|
+
const rawSource = credential?.source ?? "";
|
|
2124
|
+
const wallet = walletFromDid(rawSource);
|
|
2125
|
+
return { valid: true, wallet, withReceipt: result.withReceipt };
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// src/pipeline/flows/siwx-only.ts
|
|
2129
|
+
async function runSiwxOnlyFlow(ctx) {
|
|
2130
|
+
const { request, routeEntry, deps } = ctx;
|
|
2131
|
+
if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get(HEADERS.SIWX)) {
|
|
2132
|
+
const earlyClone = request.clone();
|
|
2133
|
+
const earlyBody = await parseBody(earlyClone, routeEntry);
|
|
2134
|
+
if (earlyBody.ok) {
|
|
2135
|
+
const validateErr = await runValidate(ctx, earlyBody.data);
|
|
2136
|
+
if (validateErr) return validateErr;
|
|
2137
|
+
} else {
|
|
2138
|
+
firePluginResponse(ctx, earlyBody.response);
|
|
2139
|
+
return earlyBody.response;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const siwxHeader = request.headers.get(HEADERS.SIWX);
|
|
2143
|
+
const protocol = detectProtocol(request);
|
|
2144
|
+
if (!siwxHeader && protocol === "mpp" && deps.mppx) {
|
|
2145
|
+
let mppSiwxResult;
|
|
2146
|
+
try {
|
|
2147
|
+
mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
|
|
2022
2148
|
} catch (err) {
|
|
2023
|
-
|
|
2149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2150
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2024
2151
|
level: "critical",
|
|
2025
|
-
message: `MPP
|
|
2152
|
+
message: `MPP SIWX verification failed: ${message}`,
|
|
2153
|
+
route: routeEntry.key
|
|
2154
|
+
});
|
|
2155
|
+
return fail(ctx, 500, `MPP SIWX verification failed: ${message}`);
|
|
2156
|
+
}
|
|
2157
|
+
if (mppSiwxResult.valid) {
|
|
2158
|
+
ctx.pluginCtx.setVerifiedWallet(mppSiwxResult.wallet);
|
|
2159
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
2160
|
+
authMode: "siwx",
|
|
2161
|
+
wallet: mppSiwxResult.wallet,
|
|
2026
2162
|
route: routeEntry.key
|
|
2027
2163
|
});
|
|
2164
|
+
const authResponse = await runHandlerOnly(ctx, mppSiwxResult.wallet, void 0);
|
|
2165
|
+
if (authResponse.status < 400) {
|
|
2166
|
+
return mppSiwxResult.withReceipt(authResponse);
|
|
2167
|
+
}
|
|
2168
|
+
return authResponse;
|
|
2028
2169
|
}
|
|
2029
2170
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2171
|
+
if (!siwxHeader) {
|
|
2172
|
+
return buildSiwxChallenge(ctx);
|
|
2173
|
+
}
|
|
2174
|
+
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
2175
|
+
if (!siwx.valid) {
|
|
2176
|
+
const response = NextResponse5.json(
|
|
2177
|
+
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
2178
|
+
{ status: 402 }
|
|
2179
|
+
);
|
|
2180
|
+
firePluginResponse(ctx, response);
|
|
2181
|
+
return response;
|
|
2182
|
+
}
|
|
2183
|
+
const wallet = normalizeWalletAddress(siwx.wallet);
|
|
2184
|
+
ctx.pluginCtx.setVerifiedWallet(wallet);
|
|
2185
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
2186
|
+
authMode: "siwx",
|
|
2187
|
+
wallet,
|
|
2188
|
+
route: routeEntry.key
|
|
2042
2189
|
});
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2190
|
+
return runHandlerOnly(ctx, wallet, void 0);
|
|
2191
|
+
}
|
|
2192
|
+
async function buildSiwxChallenge(ctx) {
|
|
2193
|
+
const { request, routeEntry, deps } = ctx;
|
|
2194
|
+
const url = new URL(request.url);
|
|
2195
|
+
const nonce = crypto.randomUUID().replace(/-/g, "");
|
|
2196
|
+
const supportedChains = getSupportedChains(deps.x402Accepts, deps.network);
|
|
2197
|
+
const primaryChain = supportedChains[0];
|
|
2198
|
+
const siwxInfo = {
|
|
2199
|
+
domain: url.hostname,
|
|
2200
|
+
uri: request.url,
|
|
2201
|
+
version: "1",
|
|
2202
|
+
chainId: primaryChain.chainId,
|
|
2203
|
+
type: primaryChain.type,
|
|
2204
|
+
nonce,
|
|
2205
|
+
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2206
|
+
expirationTime: new Date(Date.now() + SIWX_CHALLENGE_EXPIRY_MS).toISOString(),
|
|
2207
|
+
statement: "Sign in to verify your wallet identity"
|
|
2208
|
+
};
|
|
2209
|
+
let siwxSchema;
|
|
2210
|
+
try {
|
|
2211
|
+
siwxSchema = await buildSIWXExtension();
|
|
2212
|
+
} catch {
|
|
2213
|
+
}
|
|
2214
|
+
const paymentRequired = {
|
|
2215
|
+
x402Version: 2,
|
|
2216
|
+
error: "SIWX authentication required",
|
|
2217
|
+
resource: {
|
|
2218
|
+
url: request.url,
|
|
2219
|
+
description: routeEntry.description ?? "SIWX-protected endpoint",
|
|
2220
|
+
mimeType: "application/json"
|
|
2221
|
+
},
|
|
2222
|
+
accepts: [],
|
|
2223
|
+
extensions: {
|
|
2224
|
+
"sign-in-with-x": {
|
|
2225
|
+
info: siwxInfo,
|
|
2226
|
+
// Required by MCP tools at the top level for chain detection.
|
|
2227
|
+
supportedChains,
|
|
2228
|
+
...siwxSchema ? { schema: siwxSchema } : {}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
let encoded;
|
|
2233
|
+
try {
|
|
2234
|
+
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
2235
|
+
encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2238
|
+
level: "warn",
|
|
2239
|
+
message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2240
|
+
route: routeEntry.key
|
|
2048
2241
|
});
|
|
2049
2242
|
}
|
|
2243
|
+
const response = new NextResponse5(JSON.stringify(paymentRequired), {
|
|
2244
|
+
status: 402,
|
|
2245
|
+
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
2246
|
+
});
|
|
2247
|
+
if (encoded) response.headers.set(HEADERS.X402_PAYMENT_REQUIRED, encoded);
|
|
2248
|
+
if (deps.mppx) {
|
|
2249
|
+
try {
|
|
2250
|
+
const mppChallenge = await deps.mppx.charge({ amount: "0" })(request);
|
|
2251
|
+
if (mppChallenge.status === 402) {
|
|
2252
|
+
const wwwAuth = mppChallenge.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
2253
|
+
if (wwwAuth) response.headers.set(HEADERS.WWW_AUTHENTICATE, wwwAuth);
|
|
2254
|
+
}
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
firePluginResponse(ctx, response);
|
|
2259
|
+
return response;
|
|
2050
2260
|
}
|
|
2051
|
-
function
|
|
2052
|
-
|
|
2053
|
-
if (critical !== void 0 && remaining <= critical) return "critical";
|
|
2054
|
-
if (warn !== void 0 && remaining <= warn) return "warn";
|
|
2055
|
-
return "healthy";
|
|
2261
|
+
function siwxSignatureType(network) {
|
|
2262
|
+
return network.startsWith("solana:") ? "ed25519" : "eip191";
|
|
2056
2263
|
}
|
|
2057
|
-
function
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
const overage = providerConfig.overage ?? "same-rate";
|
|
2066
|
-
const event = {
|
|
2067
|
-
provider: providerName,
|
|
2068
|
-
route: routeEntry.key,
|
|
2069
|
-
remaining: quota.remaining,
|
|
2070
|
-
limit: quota.limit,
|
|
2071
|
-
spend: quota.spend,
|
|
2072
|
-
level,
|
|
2073
|
-
overage,
|
|
2074
|
-
message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
|
|
2075
|
-
};
|
|
2076
|
-
firePluginHook(deps.plugin, "onProviderQuota", pluginCtx, event);
|
|
2077
|
-
} catch {
|
|
2264
|
+
function getSupportedChains(x402Accepts, fallbackNetwork) {
|
|
2265
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2266
|
+
const chains = [];
|
|
2267
|
+
for (const accept of x402Accepts) {
|
|
2268
|
+
if (accept.network && !seen.has(accept.network)) {
|
|
2269
|
+
seen.add(accept.network);
|
|
2270
|
+
chains.push({ chainId: accept.network, type: siwxSignatureType(accept.network) });
|
|
2271
|
+
}
|
|
2078
2272
|
}
|
|
2273
|
+
if (chains.length === 0) {
|
|
2274
|
+
chains.push({ chainId: fallbackNetwork, type: siwxSignatureType(fallbackNetwork) });
|
|
2275
|
+
}
|
|
2276
|
+
return chains;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/pipeline/flows/unprotected.ts
|
|
2280
|
+
async function runUnprotectedFlow(ctx) {
|
|
2281
|
+
return runHandlerOnly(ctx, null, void 0);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// src/orchestrate.ts
|
|
2285
|
+
function createRequestHandler(routeEntry, handler, deps) {
|
|
2286
|
+
return async (request) => {
|
|
2287
|
+
await deps.initPromise;
|
|
2288
|
+
const ctx = preflight(routeEntry, handler, deps, request);
|
|
2289
|
+
if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
|
|
2290
|
+
if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
|
|
2291
|
+
if (routeEntry.pricing) return runPaidFlow(ctx);
|
|
2292
|
+
if (routeEntry.apiKeyResolver) return runApiKeyOnlyFlow(ctx);
|
|
2293
|
+
return runUnprotectedFlow(ctx);
|
|
2294
|
+
};
|
|
2079
2295
|
}
|
|
2080
2296
|
|
|
2081
2297
|
// src/validate-examples.ts
|
|
@@ -2550,7 +2766,7 @@ function createRedisEntitlementStore(client, options) {
|
|
|
2550
2766
|
}
|
|
2551
2767
|
|
|
2552
2768
|
// src/discovery/well-known.ts
|
|
2553
|
-
import { NextResponse as
|
|
2769
|
+
import { NextResponse as NextResponse6 } from "next/server";
|
|
2554
2770
|
|
|
2555
2771
|
// src/discovery/utils/guidance.ts
|
|
2556
2772
|
async function resolveGuidance(discovery) {
|
|
@@ -2594,7 +2810,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2594
2810
|
if (instructions) {
|
|
2595
2811
|
body.instructions = instructions;
|
|
2596
2812
|
}
|
|
2597
|
-
return
|
|
2813
|
+
return NextResponse6.json(body, {
|
|
2598
2814
|
headers: {
|
|
2599
2815
|
"Access-Control-Allow-Origin": "*",
|
|
2600
2816
|
"Access-Control-Allow-Methods": "GET",
|
|
@@ -2612,13 +2828,13 @@ function toDiscoveryResource(method, url, mode) {
|
|
|
2612
2828
|
|
|
2613
2829
|
// src/discovery/openapi.ts
|
|
2614
2830
|
init_constants();
|
|
2615
|
-
import { NextResponse as
|
|
2831
|
+
import { NextResponse as NextResponse7 } from "next/server";
|
|
2616
2832
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
2617
2833
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
2618
2834
|
let cached = null;
|
|
2619
2835
|
let validated = false;
|
|
2620
2836
|
return async (_request) => {
|
|
2621
|
-
if (cached) return
|
|
2837
|
+
if (cached) return NextResponse7.json(cached);
|
|
2622
2838
|
if (!validated && pricesKeys) {
|
|
2623
2839
|
registry.validate(pricesKeys);
|
|
2624
2840
|
validated = true;
|
|
@@ -2643,14 +2859,14 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2643
2859
|
securitySchemes.siwx = {
|
|
2644
2860
|
type: "apiKey",
|
|
2645
2861
|
in: "header",
|
|
2646
|
-
name:
|
|
2862
|
+
name: HEADERS.SIWX
|
|
2647
2863
|
};
|
|
2648
2864
|
}
|
|
2649
2865
|
if (requiresApiKeyScheme) {
|
|
2650
2866
|
securitySchemes.apiKey = {
|
|
2651
2867
|
type: "apiKey",
|
|
2652
2868
|
in: "header",
|
|
2653
|
-
name:
|
|
2869
|
+
name: HEADERS.API_KEY
|
|
2654
2870
|
};
|
|
2655
2871
|
}
|
|
2656
2872
|
const discoveryMetadata = {};
|
|
@@ -2681,7 +2897,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2681
2897
|
paths
|
|
2682
2898
|
};
|
|
2683
2899
|
cached = createDocument(openApiDocument);
|
|
2684
|
-
return
|
|
2900
|
+
return NextResponse7.json(cached);
|
|
2685
2901
|
};
|
|
2686
2902
|
}
|
|
2687
2903
|
function deriveTag(routeKey) {
|
|
@@ -2804,11 +3020,11 @@ function buildPricingInfo(entry) {
|
|
|
2804
3020
|
}
|
|
2805
3021
|
|
|
2806
3022
|
// src/discovery/llms-txt.ts
|
|
2807
|
-
import { NextResponse as
|
|
3023
|
+
import { NextResponse as NextResponse8 } from "next/server";
|
|
2808
3024
|
function createLlmsTxtHandler(discovery) {
|
|
2809
3025
|
return async (_request) => {
|
|
2810
3026
|
const guidance = await resolveGuidance(discovery) ?? "";
|
|
2811
|
-
return new
|
|
3027
|
+
return new NextResponse8(guidance, {
|
|
2812
3028
|
headers: {
|
|
2813
3029
|
"Content-Type": "text/plain; charset=utf-8",
|
|
2814
3030
|
"Access-Control-Allow-Origin": "*"
|
|
@@ -2818,14 +3034,14 @@ function createLlmsTxtHandler(discovery) {
|
|
|
2818
3034
|
}
|
|
2819
3035
|
|
|
2820
3036
|
// src/index.ts
|
|
2821
|
-
|
|
3037
|
+
init_accepts();
|
|
2822
3038
|
init_constants();
|
|
2823
3039
|
|
|
2824
3040
|
// src/config.ts
|
|
2825
3041
|
init_constants();
|
|
2826
3042
|
init_evm();
|
|
2827
3043
|
init_solana();
|
|
2828
|
-
|
|
3044
|
+
init_accepts();
|
|
2829
3045
|
var RouterConfigError = class extends Error {
|
|
2830
3046
|
issues;
|
|
2831
3047
|
constructor(issues) {
|