@agentcash/router 1.5.0 → 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/README.md +80 -17
- package/dist/index.cjs +1811 -1336
- package/dist/index.d.cts +111 -44
- package/dist/index.d.ts +111 -44
- package/dist/index.js +1811 -1336
- package/package.json +3 -3
- 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,105 +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) {
|
|
589
|
-
try {
|
|
590
|
-
const result = await handler(ctx);
|
|
591
|
-
if (result instanceof Response) return result;
|
|
592
|
-
return NextResponse.json(result);
|
|
593
|
-
} catch (error) {
|
|
594
|
-
const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
|
|
595
|
-
const message = error instanceof Error ? error.message : "Internal error";
|
|
596
|
-
return NextResponse.json({ success: false, error: message }, { status });
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
545
|
// src/body.ts
|
|
601
546
|
async function bufferBody(request) {
|
|
602
547
|
const text = await request.text();
|
|
@@ -621,296 +566,329 @@ function validateBody(parsed, schema) {
|
|
|
621
566
|
};
|
|
622
567
|
}
|
|
623
568
|
|
|
624
|
-
// src/
|
|
625
|
-
async function
|
|
626
|
-
if (
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
if (defaultTier && tiers[defaultTier]) {
|
|
638
|
-
return tiers[defaultTier].price;
|
|
639
|
-
}
|
|
640
|
-
if (!tierKey) {
|
|
641
|
-
throw Object.assign(new Error(`Missing required field '${field}' for tier pricing`), {
|
|
642
|
-
status: 400
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
throw Object.assign(
|
|
646
|
-
new Error(
|
|
647
|
-
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
648
|
-
),
|
|
649
|
-
{ status: 400 }
|
|
650
|
-
);
|
|
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
|
+
};
|
|
651
582
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
for (const tier of Object.values(tiers)) {
|
|
660
|
-
if (parseFloat(tier.price) > parseFloat(max)) {
|
|
661
|
-
max = tier.price;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return max;
|
|
583
|
+
|
|
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;
|
|
665
590
|
}
|
|
666
591
|
|
|
667
|
-
// src/
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
592
|
+
// src/pipeline/context/errors.ts
|
|
593
|
+
function errorStatus(error, fallback) {
|
|
594
|
+
const status = error?.status;
|
|
595
|
+
return typeof status === "number" ? status : fallback;
|
|
596
|
+
}
|
|
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
|
+
}
|
|
672
604
|
|
|
673
|
-
// src/
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
extensions
|
|
694
|
-
);
|
|
695
|
-
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
696
|
-
return { encoded, requirements };
|
|
697
|
-
}
|
|
698
|
-
async function verifyX402Payment(opts) {
|
|
699
|
-
const { server, request, price, accepts } = opts;
|
|
700
|
-
const payload = await readPaymentPayload(request);
|
|
701
|
-
if (!payload) return null;
|
|
702
|
-
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
703
|
-
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
704
|
-
if (!matching) {
|
|
705
|
-
return invalidPaymentVerification();
|
|
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
|
+
});
|
|
706
625
|
}
|
|
707
|
-
|
|
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;
|
|
708
638
|
try {
|
|
709
|
-
|
|
639
|
+
await ctx.routeEntry.validateFn(body);
|
|
640
|
+
return null;
|
|
710
641
|
} catch (err) {
|
|
711
|
-
|
|
712
|
-
if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
|
|
713
|
-
throw err;
|
|
642
|
+
return fail(ctx, errorStatus(err, 400), errorMessage(err, "Validation failed"), body);
|
|
714
643
|
}
|
|
715
|
-
if (!verify.isValid) return invalidPaymentVerification();
|
|
716
|
-
return {
|
|
717
|
-
valid: true,
|
|
718
|
-
payer: verify.payer,
|
|
719
|
-
payload,
|
|
720
|
-
requirements: matching
|
|
721
|
-
};
|
|
722
644
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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";
|
|
727
655
|
}
|
|
728
|
-
|
|
729
|
-
|
|
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 });
|
|
730
669
|
}
|
|
731
|
-
const stableMatch = requirements.find(
|
|
732
|
-
(requirement) => matchesStableFields(requirement, payload.accepted)
|
|
733
|
-
);
|
|
734
|
-
return stableMatch ? payload.accepted : null;
|
|
735
|
-
}
|
|
736
|
-
function matchesStableFields(requirement, accepted) {
|
|
737
|
-
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;
|
|
738
|
-
}
|
|
739
|
-
async function buildExpectedRequirements(server, request, price, accepts) {
|
|
740
|
-
const exactRequirements = await buildExactRequirements(server, request, price, accepts);
|
|
741
|
-
const customRequirements = buildCustomRequirements(price, accepts);
|
|
742
|
-
return [...exactRequirements, ...customRequirements];
|
|
743
670
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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;
|
|
762
704
|
}
|
|
763
|
-
console.warn(
|
|
764
|
-
`[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
|
|
765
|
-
);
|
|
766
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 {
|
|
767
732
|
}
|
|
768
|
-
if (requirements.length > 0) {
|
|
769
|
-
return requirements;
|
|
770
|
-
}
|
|
771
|
-
throw failures[0] ?? new Error("Failed to build x402 exact requirements");
|
|
772
733
|
}
|
|
773
|
-
function
|
|
774
|
-
|
|
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";
|
|
775
739
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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;
|
|
780
746
|
}
|
|
781
|
-
|
|
782
|
-
|
|
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);
|
|
783
759
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
|
793
788
|
);
|
|
794
789
|
}
|
|
795
|
-
return accepted;
|
|
796
790
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
})
|
|
813
|
-
);
|
|
814
|
-
const enriched = [...requirements];
|
|
815
|
-
results.filter((r) => r.success).forEach(({ group, accepted }) => {
|
|
816
|
-
accepted.forEach((req, offset) => {
|
|
817
|
-
const index = group.items[offset]?.index;
|
|
818
|
-
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
|
|
819
805
|
});
|
|
820
|
-
});
|
|
821
|
-
const failedIndices = new Set(
|
|
822
|
-
results.filter((r) => !r.success).flatMap(({ group }) => group.items.map(({ index }) => index))
|
|
823
|
-
);
|
|
824
|
-
const remaining = enriched.filter((_, i) => !failedIndices.has(i));
|
|
825
|
-
if (remaining.length === 0) {
|
|
826
|
-
throw new Error(
|
|
827
|
-
"All facilitator enrichments failed; no payment requirements remain for challenge"
|
|
828
|
-
);
|
|
829
806
|
}
|
|
830
|
-
return remaining;
|
|
831
807
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
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
|
|
847
822
|
});
|
|
848
|
-
|
|
849
|
-
|
|
823
|
+
await runSettlementError(ctx, scope, error, "afterSettle");
|
|
824
|
+
}
|
|
850
825
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
+
});
|
|
857
841
|
}
|
|
858
|
-
return facilitator;
|
|
859
842
|
}
|
|
860
|
-
|
|
861
|
-
|
|
843
|
+
|
|
844
|
+
// src/pipeline/context/grant-entitlement.ts
|
|
845
|
+
async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
846
|
+
if (!ctx.routeEntry.siwxEnabled) return;
|
|
847
|
+
try {
|
|
848
|
+
await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
851
|
+
level: "warn",
|
|
852
|
+
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
853
|
+
route: ctx.routeEntry.key
|
|
854
|
+
});
|
|
855
|
+
}
|
|
862
856
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
function invalidPaymentVerification() {
|
|
894
|
-
return { valid: false, payload: null, requirements: null, payer: null };
|
|
895
|
-
}
|
|
896
|
-
function decimalToAtomicUnits(amount, decimals) {
|
|
897
|
-
const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
|
|
898
|
-
if (!match?.groups) {
|
|
899
|
-
throw new Error(`Invalid decimal amount '${amount}'`);
|
|
900
|
-
}
|
|
901
|
-
const whole = match.groups.whole;
|
|
902
|
-
const fraction = match.groups.fraction ?? "";
|
|
903
|
-
if (fraction.length > decimals) {
|
|
904
|
-
throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
|
|
905
|
-
}
|
|
906
|
-
const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
|
|
907
|
-
return normalized === "" ? "0" : normalized;
|
|
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);
|
|
908
887
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
return { encoded, result };
|
|
888
|
+
|
|
889
|
+
// src/auth/normalize-wallet.ts
|
|
890
|
+
function normalizeWalletAddress(address) {
|
|
891
|
+
return /^0x/i.test(address) ? address.toLowerCase() : address;
|
|
914
892
|
}
|
|
915
893
|
|
|
916
894
|
// src/auth/siwx.ts
|
|
@@ -934,7 +912,7 @@ function categorizeValidationError(error) {
|
|
|
934
912
|
}
|
|
935
913
|
async function verifySIWX(request, _routeEntry, nonceStore) {
|
|
936
914
|
const { parseSIWxHeader, validateSIWxMessage, verifySIWxSignature } = await import("@x402/extensions/sign-in-with-x");
|
|
937
|
-
const header = request.headers.get(
|
|
915
|
+
const header = request.headers.get(HEADERS.SIWX);
|
|
938
916
|
if (!header) {
|
|
939
917
|
return { valid: false, wallet: null, code: "siwx_missing_header" };
|
|
940
918
|
}
|
|
@@ -963,25 +941,52 @@ async function buildSIWXExtension() {
|
|
|
963
941
|
return declareSIWxExtension();
|
|
964
942
|
}
|
|
965
943
|
|
|
966
|
-
// src/
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const
|
|
971
|
-
if (
|
|
972
|
-
|
|
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
|
+
}
|
|
973
982
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
const didParts = rawSource.split(":");
|
|
977
|
-
const lastPart = didParts[didParts.length - 1];
|
|
978
|
-
const wallet = normalizeWalletAddress(isAddress(lastPart) ? getAddress(lastPart) : rawSource);
|
|
979
|
-
return { valid: true, wallet, withReceipt: result.withReceipt };
|
|
983
|
+
if (errors.length === 0) return null;
|
|
984
|
+
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
980
985
|
}
|
|
981
986
|
|
|
982
987
|
// src/auth/api-key.ts
|
|
983
988
|
async function verifyApiKey(request, resolver) {
|
|
984
|
-
const apiKey = request.headers.get(
|
|
989
|
+
const apiKey = request.headers.get(HEADERS.API_KEY) ?? extractBearerToken(request.headers.get(HEADERS.AUTHORIZATION));
|
|
985
990
|
if (!apiKey) {
|
|
986
991
|
return { valid: false, account: null };
|
|
987
992
|
}
|
|
@@ -993,939 +998,1311 @@ async function verifyApiKey(request, resolver) {
|
|
|
993
998
|
}
|
|
994
999
|
function extractBearerToken(header) {
|
|
995
1000
|
if (!header) return null;
|
|
996
|
-
if (header.startsWith(
|
|
1001
|
+
if (header.startsWith(AUTH_SCHEME.BEARER)) return header.slice(AUTH_SCHEME.BEARER.length);
|
|
997
1002
|
return null;
|
|
998
1003
|
}
|
|
999
1004
|
|
|
1000
|
-
// src/
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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);
|
|
1012
1019
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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;
|
|
1020
1041
|
}
|
|
1021
1042
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1043
|
+
challengeQuote(body) {
|
|
1044
|
+
if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
|
|
1045
|
+
return this.quote(body);
|
|
1024
1046
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
body: parsedBody,
|
|
1031
|
-
query: parseQuery(request, routeEntry),
|
|
1032
|
-
request,
|
|
1033
|
-
requestId: meta.requestId,
|
|
1034
|
-
route: routeEntry.key,
|
|
1035
|
-
wallet,
|
|
1036
|
-
payment,
|
|
1037
|
-
account,
|
|
1038
|
-
alert(level, message, alertMeta) {
|
|
1039
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1040
|
-
level,
|
|
1041
|
-
message,
|
|
1042
|
-
route: routeEntry.key,
|
|
1043
|
-
meta: alertMeta
|
|
1044
|
-
});
|
|
1045
|
-
},
|
|
1046
|
-
setVerifiedWallet: (addr) => pluginCtx.setVerifiedWallet(addr)
|
|
1047
|
+
describe() {
|
|
1048
|
+
return {
|
|
1049
|
+
mode: "dynamic",
|
|
1050
|
+
min: this.opts.minPrice ?? "0",
|
|
1051
|
+
max: this.opts.maxPrice ?? "0"
|
|
1047
1052
|
};
|
|
1048
|
-
let rawResult;
|
|
1049
|
-
const response = await safeCallHandler(async (c) => {
|
|
1050
|
-
rawResult = await handler(c);
|
|
1051
|
-
return rawResult;
|
|
1052
|
-
}, ctx);
|
|
1053
|
-
return { response, rawResult };
|
|
1054
1053
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
return async (request) => {
|
|
1065
|
-
await deps.initPromise;
|
|
1066
|
-
const meta = buildMeta(request, routeEntry);
|
|
1067
|
-
const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
|
|
1068
|
-
async function handleAuth(wallet, account2) {
|
|
1069
|
-
const body2 = await parseBody(request, routeEntry);
|
|
1070
|
-
if (!body2.ok) {
|
|
1071
|
-
firePluginResponse(deps, pluginCtx, meta, body2.response);
|
|
1072
|
-
return body2.response;
|
|
1073
|
-
}
|
|
1074
|
-
if (routeEntry.validateFn) {
|
|
1075
|
-
try {
|
|
1076
|
-
await routeEntry.validateFn(body2.data);
|
|
1077
|
-
} catch (err) {
|
|
1078
|
-
const status = err.status ?? 400;
|
|
1079
|
-
const message = err instanceof Error ? err.message : "Validation failed";
|
|
1080
|
-
return fail(status, message, meta, pluginCtx, body2.data);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
const { response, rawResult } = await invoke(
|
|
1084
|
-
request,
|
|
1085
|
-
meta,
|
|
1086
|
-
pluginCtx,
|
|
1087
|
-
wallet,
|
|
1088
|
-
account2,
|
|
1089
|
-
body2.data,
|
|
1090
|
-
null
|
|
1091
|
-
);
|
|
1092
|
-
finalize(response, rawResult, meta, pluginCtx, body2.data);
|
|
1093
|
-
return response;
|
|
1094
|
-
}
|
|
1095
|
-
if (routeEntry.authMode === "unprotected") {
|
|
1096
|
-
return handleAuth(null, void 0);
|
|
1097
|
-
}
|
|
1098
|
-
let account;
|
|
1099
|
-
if (routeEntry.authMode === "apiKey" || routeEntry.apiKeyResolver) {
|
|
1100
|
-
if (!routeEntry.apiKeyResolver) {
|
|
1101
|
-
return fail(401, "API key resolver not configured", meta, pluginCtx);
|
|
1102
|
-
}
|
|
1103
|
-
const keyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
|
|
1104
|
-
if (!keyResult.valid) {
|
|
1105
|
-
return fail(401, "Invalid or missing API key", meta, pluginCtx);
|
|
1106
|
-
}
|
|
1107
|
-
account = keyResult.account;
|
|
1108
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1109
|
-
authMode: "apiKey",
|
|
1110
|
-
wallet: null,
|
|
1111
|
-
route: routeEntry.key,
|
|
1112
|
-
account
|
|
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
|
|
1113
1063
|
});
|
|
1114
|
-
|
|
1115
|
-
return handleAuth(null, account);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
const protocol = detectProtocol(request);
|
|
1119
|
-
let earlyBodyData;
|
|
1120
|
-
const pricingNeedsBody = routeEntry.pricing != null && typeof routeEntry.pricing !== "string";
|
|
1121
|
-
const needsEarlyParse = !protocol && routeEntry.bodySchema && (pricingNeedsBody || routeEntry.validateFn);
|
|
1122
|
-
if (needsEarlyParse) {
|
|
1123
|
-
const requestForPricing = request.clone();
|
|
1124
|
-
const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
|
|
1125
|
-
if (earlyBodyResult.ok) {
|
|
1126
|
-
earlyBodyData = earlyBodyResult.data;
|
|
1127
|
-
if (routeEntry.validateFn) {
|
|
1128
|
-
try {
|
|
1129
|
-
await routeEntry.validateFn(earlyBodyData);
|
|
1130
|
-
} catch (err) {
|
|
1131
|
-
const status = err.status ?? 400;
|
|
1132
|
-
const message = err instanceof Error ? err.message : "Validation failed";
|
|
1133
|
-
return fail(status, message, meta, pluginCtx, earlyBodyData);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
} else {
|
|
1137
|
-
firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
|
|
1138
|
-
return earlyBodyResult.response;
|
|
1139
|
-
}
|
|
1064
|
+
return this.opts.maxPrice;
|
|
1140
1065
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
domain: url.hostname,
|
|
1190
|
-
uri: request.url,
|
|
1191
|
-
version: "1",
|
|
1192
|
-
chainId: primaryChain.chainId,
|
|
1193
|
-
type: primaryChain.type,
|
|
1194
|
-
nonce,
|
|
1195
|
-
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1196
|
-
expirationTime: new Date(Date.now() + SIWX_CHALLENGE_EXPIRY_MS).toISOString(),
|
|
1197
|
-
statement: "Sign in to verify your wallet identity"
|
|
1198
|
-
};
|
|
1199
|
-
let siwxSchema;
|
|
1200
|
-
try {
|
|
1201
|
-
siwxSchema = await buildSIWXExtension();
|
|
1202
|
-
} catch {
|
|
1203
|
-
}
|
|
1204
|
-
const paymentRequired = {
|
|
1205
|
-
x402Version: 2,
|
|
1206
|
-
error: "SIWX authentication required",
|
|
1207
|
-
resource: {
|
|
1208
|
-
url: request.url,
|
|
1209
|
-
description: routeEntry.description ?? "SIWX-protected endpoint",
|
|
1210
|
-
mimeType: "application/json"
|
|
1211
|
-
},
|
|
1212
|
-
accepts: [],
|
|
1213
|
-
extensions: {
|
|
1214
|
-
"sign-in-with-x": {
|
|
1215
|
-
info: siwxInfo,
|
|
1216
|
-
// supportedChains at top level required by MCP tools for chain detection
|
|
1217
|
-
supportedChains,
|
|
1218
|
-
...siwxSchema ? { schema: siwxSchema } : {}
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
};
|
|
1222
|
-
let encoded;
|
|
1223
|
-
try {
|
|
1224
|
-
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
1225
|
-
encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1226
|
-
} catch (err) {
|
|
1227
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1228
|
-
level: "warn",
|
|
1229
|
-
message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1230
|
-
route: routeEntry.key
|
|
1231
|
-
});
|
|
1232
|
-
}
|
|
1233
|
-
const response = new NextResponse2(JSON.stringify(paymentRequired), {
|
|
1234
|
-
status: 402,
|
|
1235
|
-
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
1236
|
-
});
|
|
1237
|
-
if (encoded) response.headers.set("PAYMENT-REQUIRED", encoded);
|
|
1238
|
-
if (deps.mppx) {
|
|
1239
|
-
try {
|
|
1240
|
-
const mppChallenge = await deps.mppx.charge({ amount: "0" })(request);
|
|
1241
|
-
if (mppChallenge.status === 402) {
|
|
1242
|
-
const wwwAuth = mppChallenge.challenge.headers.get("WWW-Authenticate");
|
|
1243
|
-
if (wwwAuth) response.headers.set("WWW-Authenticate", wwwAuth);
|
|
1244
|
-
}
|
|
1245
|
-
} catch {
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
firePluginResponse(deps, pluginCtx, meta, response);
|
|
1249
|
-
return response;
|
|
1250
|
-
}
|
|
1251
|
-
if (siwxHeader) {
|
|
1252
|
-
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
1253
|
-
if (!siwx.valid) {
|
|
1254
|
-
if (routeEntry.authMode === "siwx") {
|
|
1255
|
-
const response = NextResponse2.json(
|
|
1256
|
-
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
1257
|
-
{ status: 402 }
|
|
1258
|
-
);
|
|
1259
|
-
firePluginResponse(deps, pluginCtx, meta, response);
|
|
1260
|
-
return response;
|
|
1261
|
-
}
|
|
1262
|
-
} else {
|
|
1263
|
-
const wallet = normalizeWalletAddress(siwx.wallet);
|
|
1264
|
-
pluginCtx.setVerifiedWallet(wallet);
|
|
1265
|
-
if (routeEntry.authMode === "siwx") {
|
|
1266
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1267
|
-
authMode: "siwx",
|
|
1268
|
-
wallet,
|
|
1269
|
-
route: routeEntry.key
|
|
1270
|
-
});
|
|
1271
|
-
return handleAuth(wallet, void 0);
|
|
1272
|
-
}
|
|
1273
|
-
if (routeEntry.siwxEnabled && routeEntry.pricing) {
|
|
1274
|
-
const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
|
|
1275
|
-
if (entitled) {
|
|
1276
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1277
|
-
authMode: "siwx",
|
|
1278
|
-
wallet,
|
|
1279
|
-
route: routeEntry.key
|
|
1280
|
-
});
|
|
1281
|
-
return handleAuth(wallet, account);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
if (!protocol || protocol === "siwx") {
|
|
1288
|
-
if (routeEntry.pricing) {
|
|
1289
|
-
const initErrors = routeEntry.protocols.map((p) => {
|
|
1290
|
-
if (p === "x402" && deps.x402InitError) return `x402: ${deps.x402InitError}`;
|
|
1291
|
-
if (p === "mpp" && deps.mppInitError) return `mpp: ${deps.mppInitError}`;
|
|
1292
|
-
return null;
|
|
1293
|
-
}).filter(Boolean);
|
|
1294
|
-
if (initErrors.length > 0) {
|
|
1295
|
-
return fail(
|
|
1296
|
-
500,
|
|
1297
|
-
`Payment protocol initialization failed. ${initErrors.join("; ")}`,
|
|
1298
|
-
meta,
|
|
1299
|
-
pluginCtx
|
|
1300
|
-
);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
1304
|
-
}
|
|
1305
|
-
const body = await parseBody(request, routeEntry);
|
|
1306
|
-
if (!body.ok) {
|
|
1307
|
-
firePluginResponse(deps, pluginCtx, meta, body.response);
|
|
1308
|
-
return body.response;
|
|
1309
|
-
}
|
|
1310
|
-
if (routeEntry.validateFn) {
|
|
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);
|
|
1074
|
+
}
|
|
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) {
|
|
1311
1114
|
try {
|
|
1312
|
-
|
|
1313
|
-
} catch
|
|
1314
|
-
const status = err.status ?? 400;
|
|
1315
|
-
const message = err instanceof Error ? err.message : "Validation failed";
|
|
1316
|
-
return fail(status, message, meta, pluginCtx, body.data);
|
|
1115
|
+
return this.quote(body);
|
|
1116
|
+
} catch {
|
|
1317
1117
|
}
|
|
1318
1118
|
}
|
|
1319
|
-
|
|
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 });
|
|
1142
|
+
}
|
|
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)}`);
|
|
1169
|
+
}
|
|
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 };
|
|
1181
|
+
}
|
|
1182
|
+
function walletFromDid(rawSource) {
|
|
1183
|
+
const parts = rawSource.split(":");
|
|
1184
|
+
const last = parts[parts.length - 1];
|
|
1185
|
+
return normalizeWalletAddress(isAddress(last) ? getAddress(last) : rawSource);
|
|
1186
|
+
}
|
|
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 "";
|
|
1200
|
+
}
|
|
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 "";
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
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"
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
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
|
+
};
|
|
1262
|
+
}
|
|
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
|
+
};
|
|
1275
|
+
}
|
|
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}`);
|
|
1286
|
+
return {
|
|
1287
|
+
ok: false,
|
|
1288
|
+
error: settlementError,
|
|
1289
|
+
failMessage: `MPP payment failed: ${detail}`,
|
|
1290
|
+
failStatus: 500
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
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 {};
|
|
1320
1385
|
try {
|
|
1321
|
-
|
|
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
|
+
}
|
|
1322
1391
|
} catch (err) {
|
|
1323
|
-
return fail(
|
|
1324
|
-
err.status ?? 500,
|
|
1325
|
-
err instanceof Error ? err.message : "Price resolution failed",
|
|
1326
|
-
meta,
|
|
1327
|
-
pluginCtx,
|
|
1328
|
-
body.data
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
if (!routeEntry.protocols.includes(protocol)) {
|
|
1332
|
-
const accepted = routeEntry.protocols.join(", ") || "none";
|
|
1333
1392
|
console.warn(
|
|
1334
|
-
`[router]
|
|
1335
|
-
);
|
|
1336
|
-
return fail(
|
|
1337
|
-
400,
|
|
1338
|
-
`This route does not accept ${protocol} payments. Accepted protocols: ${accepted}`,
|
|
1339
|
-
meta,
|
|
1340
|
-
pluginCtx,
|
|
1341
|
-
body.data
|
|
1393
|
+
`[router] MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1342
1394
|
);
|
|
1395
|
+
throw err;
|
|
1343
1396
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
...matchedRecipient ? { recipient: matchedRecipient } : {}
|
|
1376
|
-
};
|
|
1377
|
-
pluginCtx.setVerifiedWallet(wallet);
|
|
1378
|
-
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1379
|
-
protocol: "x402",
|
|
1380
|
-
payer: wallet,
|
|
1381
|
-
amount: price,
|
|
1382
|
-
network: matchedNetwork
|
|
1383
|
-
});
|
|
1384
|
-
const { response, rawResult } = await invoke(
|
|
1385
|
-
request,
|
|
1386
|
-
meta,
|
|
1387
|
-
pluginCtx,
|
|
1388
|
-
wallet,
|
|
1389
|
-
account,
|
|
1390
|
-
body.data,
|
|
1391
|
-
payment
|
|
1397
|
+
return {};
|
|
1398
|
+
}
|
|
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) {
|
|
1425
|
+
try {
|
|
1426
|
+
requirements.push(
|
|
1427
|
+
...await server.buildPaymentRequirementsFromOptions(options, { request })
|
|
1392
1428
|
);
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
verifyRequirements
|
|
1399
|
-
);
|
|
1400
|
-
if (!settle.result?.success) {
|
|
1401
|
-
const reason = settle.result?.errorReason || "x402 settlement returned success=false";
|
|
1402
|
-
const error = new Error(reason);
|
|
1403
|
-
error.errorReason = reason;
|
|
1404
|
-
throw error;
|
|
1405
|
-
}
|
|
1406
|
-
if (routeEntry.siwxEnabled) {
|
|
1407
|
-
try {
|
|
1408
|
-
await deps.entitlementStore.grant(routeEntry.key, wallet);
|
|
1409
|
-
} catch (error) {
|
|
1410
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1411
|
-
level: "warn",
|
|
1412
|
-
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1413
|
-
route: routeEntry.key
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
response.headers.set("PAYMENT-RESPONSE", settle.encoded);
|
|
1418
|
-
response.headers.set("Cache-Control", "private");
|
|
1419
|
-
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1420
|
-
protocol: "x402",
|
|
1421
|
-
payer: verify.payer,
|
|
1422
|
-
transaction: String(settle.result?.transaction ?? ""),
|
|
1423
|
-
network: matchedNetwork
|
|
1424
|
-
});
|
|
1425
|
-
} catch (err) {
|
|
1426
|
-
const errObj = err;
|
|
1427
|
-
console.error("Settlement failed", {
|
|
1428
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1429
|
-
route: routeEntry.key,
|
|
1430
|
-
network: matchedNetwork,
|
|
1431
|
-
errorReason: errObj.errorReason,
|
|
1432
|
-
facilitatorStatus: errObj.response?.status,
|
|
1433
|
-
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
1434
|
-
});
|
|
1435
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1436
|
-
level: "critical",
|
|
1437
|
-
message: `Settlement failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1438
|
-
route: routeEntry.key
|
|
1439
|
-
});
|
|
1440
|
-
return fail(500, "Settlement failed", meta, pluginCtx, body.data);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
finalize(response, rawResult, meta, pluginCtx, body.data);
|
|
1444
|
-
return response;
|
|
1445
|
-
}
|
|
1446
|
-
if (protocol === "mpp") {
|
|
1447
|
-
if (!deps.mppx) {
|
|
1448
|
-
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";
|
|
1449
|
-
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
1450
|
-
return fail(500, reason, meta, pluginCtx, body.data);
|
|
1451
|
-
}
|
|
1452
|
-
const mppCredential = Credential2.fromRequest(request);
|
|
1453
|
-
const rawSource = mppCredential?.source ?? "";
|
|
1454
|
-
const didParts = rawSource.split(":");
|
|
1455
|
-
const lastPart = didParts[didParts.length - 1];
|
|
1456
|
-
const wallet = normalizeWalletAddress(isAddress2(lastPart) ? getAddress2(lastPart) : rawSource);
|
|
1457
|
-
const payloadType = mppCredential?.payload?.type;
|
|
1458
|
-
if (payloadType === "transaction" && deps.tempoClient) {
|
|
1459
|
-
try {
|
|
1460
|
-
const serializedTx = mppCredential.payload.signature;
|
|
1461
|
-
const transaction = TempoTransaction.deserialize(serializedTx);
|
|
1462
|
-
await viemCall(deps.tempoClient, {
|
|
1463
|
-
...transaction,
|
|
1464
|
-
account: transaction.from,
|
|
1465
|
-
calls: transaction.calls ?? []
|
|
1466
|
-
});
|
|
1467
|
-
} catch (err) {
|
|
1468
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1469
|
-
console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
|
|
1470
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1471
|
-
level: "warn",
|
|
1472
|
-
message: `MPP simulation failed: ${message}`,
|
|
1473
|
-
route: routeEntry.key
|
|
1474
|
-
});
|
|
1475
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1476
|
-
}
|
|
1477
|
-
pluginCtx.setVerifiedWallet(wallet);
|
|
1478
|
-
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1479
|
-
protocol: "mpp",
|
|
1480
|
-
payer: wallet,
|
|
1481
|
-
amount: price,
|
|
1482
|
-
network: "tempo:4217"
|
|
1483
|
-
});
|
|
1484
|
-
const { response: response2, rawResult: rawResult2 } = await invoke(
|
|
1485
|
-
request,
|
|
1486
|
-
meta,
|
|
1487
|
-
pluginCtx,
|
|
1488
|
-
wallet,
|
|
1489
|
-
account,
|
|
1490
|
-
body.data,
|
|
1491
|
-
{
|
|
1492
|
-
protocol: "mpp",
|
|
1493
|
-
status: "verified",
|
|
1494
|
-
payer: wallet,
|
|
1495
|
-
amount: price,
|
|
1496
|
-
network: "tempo:4217",
|
|
1497
|
-
recipient: deps.payeeAddress
|
|
1498
|
-
}
|
|
1499
|
-
);
|
|
1500
|
-
if (response2.status < 400) {
|
|
1501
|
-
let mppResult2;
|
|
1502
|
-
try {
|
|
1503
|
-
mppResult2 = await deps.mppx.charge({ amount: price })(request);
|
|
1504
|
-
} catch (err) {
|
|
1505
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1506
|
-
console.error(
|
|
1507
|
-
`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`
|
|
1508
|
-
);
|
|
1509
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1510
|
-
level: "critical",
|
|
1511
|
-
message: `MPP broadcast failed after handler: ${message}`,
|
|
1512
|
-
route: routeEntry.key
|
|
1513
|
-
});
|
|
1514
|
-
return fail(
|
|
1515
|
-
500,
|
|
1516
|
-
`MPP payment processing failed: ${message}`,
|
|
1517
|
-
meta,
|
|
1518
|
-
pluginCtx,
|
|
1519
|
-
body.data
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
if (mppResult2.status === 402) {
|
|
1523
|
-
let rejectReason = "";
|
|
1524
|
-
try {
|
|
1525
|
-
const problemBody = await mppResult2.challenge.clone().text();
|
|
1526
|
-
if (problemBody) {
|
|
1527
|
-
const problem = JSON.parse(problemBody);
|
|
1528
|
-
rejectReason = problem.detail || problem.title || "";
|
|
1529
|
-
}
|
|
1530
|
-
} catch {
|
|
1531
|
-
}
|
|
1532
|
-
const detail = rejectReason || "transaction reverted on-chain after handler execution";
|
|
1533
|
-
console.error(
|
|
1534
|
-
`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`
|
|
1535
|
-
);
|
|
1536
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1537
|
-
level: "critical",
|
|
1538
|
-
message: `MPP payment failed after handler: ${detail}`,
|
|
1539
|
-
route: routeEntry.key
|
|
1540
|
-
});
|
|
1541
|
-
return fail(500, `MPP payment failed: ${detail}`, meta, pluginCtx, body.data);
|
|
1542
|
-
}
|
|
1543
|
-
const receiptResponse = mppResult2.withReceipt(response2);
|
|
1544
|
-
receiptResponse.headers.set("Cache-Control", "private");
|
|
1545
|
-
let txHash2 = "";
|
|
1546
|
-
const receiptHeader2 = receiptResponse.headers.get("Payment-Receipt");
|
|
1547
|
-
if (receiptHeader2) {
|
|
1548
|
-
try {
|
|
1549
|
-
txHash2 = Receipt.deserialize(receiptHeader2).reference;
|
|
1550
|
-
} catch {
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
if (routeEntry.siwxEnabled) {
|
|
1554
|
-
try {
|
|
1555
|
-
await deps.entitlementStore.grant(routeEntry.key, wallet);
|
|
1556
|
-
} catch (error) {
|
|
1557
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1558
|
-
level: "warn",
|
|
1559
|
-
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1560
|
-
route: routeEntry.key
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1565
|
-
protocol: "mpp",
|
|
1566
|
-
payer: wallet,
|
|
1567
|
-
transaction: txHash2,
|
|
1568
|
-
network: "tempo:4217"
|
|
1569
|
-
});
|
|
1570
|
-
finalize(receiptResponse, rawResult2, meta, pluginCtx, body.data);
|
|
1571
|
-
return receiptResponse;
|
|
1572
|
-
}
|
|
1573
|
-
finalize(response2, rawResult2, meta, pluginCtx, body.data);
|
|
1574
|
-
return response2;
|
|
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;
|
|
1575
1434
|
}
|
|
1576
|
-
|
|
1435
|
+
console.warn(
|
|
1436
|
+
`[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
if (requirements.length > 0) {
|
|
1441
|
+
return requirements;
|
|
1442
|
+
}
|
|
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
|
+
);
|
|
1453
|
+
}
|
|
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) => {
|
|
1577
1526
|
try {
|
|
1578
|
-
|
|
1527
|
+
return { success: true, group, accepted: await enrichGroup(group, resource) };
|
|
1579
1528
|
} catch (err) {
|
|
1580
|
-
const
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
});
|
|
1587
|
-
return fail(500, `MPP payment processing failed: ${message}`, meta, pluginCtx, body.data);
|
|
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}`
|
|
1533
|
+
);
|
|
1534
|
+
return { success: false, group };
|
|
1588
1535
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
|
1607
1706
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
|
1611
1717
|
);
|
|
1612
|
-
if (
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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;
|
|
1617
1723
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1724
|
+
response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
|
|
1725
|
+
response.headers.set("Cache-Control", "private");
|
|
1726
|
+
const transaction = String(settle.result?.transaction ?? "");
|
|
1727
|
+
const settledPayment = {
|
|
1728
|
+
...payment,
|
|
1729
|
+
status: "settled",
|
|
1730
|
+
...transaction ? { transaction } : {}
|
|
1731
|
+
};
|
|
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
|
|
1624
1742
|
});
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1743
|
+
return { ok: false, error: err, failMessage: "Settlement failed" };
|
|
1744
|
+
}
|
|
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;
|
|
1771
|
+
}
|
|
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;
|
|
1790
|
+
}
|
|
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;
|
|
1803
|
+
}
|
|
1804
|
+
function getAllowedStrategies(allowed) {
|
|
1805
|
+
return allowed.map((name) => STRATEGIES[name]);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// src/pipeline/challenge.ts
|
|
1809
|
+
import { NextResponse as NextResponse4 } from "next/server";
|
|
1810
|
+
async function build402(ctx, pricing, body) {
|
|
1811
|
+
let challengePrice;
|
|
1812
|
+
try {
|
|
1813
|
+
challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
|
|
1814
|
+
} catch (err) {
|
|
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;
|
|
1822
|
+
}
|
|
1823
|
+
const extensions = await buildChallengeExtensions(ctx);
|
|
1824
|
+
const response = new NextResponse4(null, {
|
|
1825
|
+
status: 402,
|
|
1826
|
+
headers: {
|
|
1827
|
+
"Content-Type": "application/json",
|
|
1828
|
+
"Cache-Control": "no-store"
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
|
|
1832
|
+
try {
|
|
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);
|
|
1654
1844
|
}
|
|
1655
|
-
const receiptResponse = mppResult.withReceipt(response);
|
|
1656
|
-
receiptResponse.headers.set("Cache-Control", "private");
|
|
1657
|
-
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1658
|
-
protocol: "mpp",
|
|
1659
|
-
payer: wallet,
|
|
1660
|
-
transaction: txHash,
|
|
1661
|
-
network: "tempo:4217"
|
|
1662
|
-
});
|
|
1663
|
-
finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
|
|
1664
|
-
return receiptResponse;
|
|
1665
1845
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
+
}
|
|
1668
1861
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1862
|
+
}
|
|
1863
|
+
firePluginResponse(ctx, response);
|
|
1864
|
+
return response;
|
|
1671
1865
|
}
|
|
1672
|
-
async function
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
)
|
|
1866
|
+
async function buildChallengeExtensions(ctx) {
|
|
1867
|
+
const { routeEntry } = ctx;
|
|
1868
|
+
let extensions;
|
|
1869
|
+
try {
|
|
1870
|
+
const { z } = await import("zod");
|
|
1871
|
+
const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
|
|
1872
|
+
const toJSON = (schema) => z.toJSONSchema(schema, {
|
|
1873
|
+
target: "draft-2020-12",
|
|
1874
|
+
unrepresentable: "any"
|
|
1875
|
+
});
|
|
1876
|
+
const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
|
|
1877
|
+
const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
|
|
1878
|
+
if (inputSchema) {
|
|
1879
|
+
const config = {
|
|
1880
|
+
method: routeEntry.method,
|
|
1881
|
+
bodyType: routeEntry.bodySchema ? "json" : void 0,
|
|
1882
|
+
inputSchema
|
|
1883
|
+
};
|
|
1884
|
+
if (routeEntry.inputExample !== void 0) {
|
|
1885
|
+
config.input = routeEntry.inputExample;
|
|
1886
|
+
}
|
|
1887
|
+
if (outputSchema && routeEntry.outputExample !== void 0) {
|
|
1888
|
+
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
1889
|
+
}
|
|
1890
|
+
extensions = declareDiscoveryExtension(config);
|
|
1891
|
+
}
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
1894
|
+
level: "warn",
|
|
1895
|
+
message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1896
|
+
route: routeEntry.key
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
if (routeEntry.siwxEnabled) {
|
|
1900
|
+
try {
|
|
1901
|
+
const siwxExtension = await buildSIWXExtension();
|
|
1902
|
+
if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
|
|
1903
|
+
extensions = {
|
|
1904
|
+
...extensions ?? {},
|
|
1905
|
+
...siwxExtension
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
} catch {
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
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
|
+
});
|
|
1683
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, {
|
|
2045
|
+
level: "critical",
|
|
2046
|
+
message: `${incomingStrategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
|
|
2047
|
+
route: routeEntry.key
|
|
2048
|
+
});
|
|
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);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
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
|
+
);
|
|
1684
2092
|
}
|
|
1685
|
-
function
|
|
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);
|
|
1686
2097
|
return {
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
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;
|
|
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
|
+
}
|
|
1698
2112
|
};
|
|
1699
2113
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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 };
|
|
1705
2126
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
route: routeEntry.key,
|
|
1717
|
-
meta: { calculated: price, maxPrice: routeEntry.maxPrice, body: bodyData }
|
|
1718
|
-
});
|
|
1719
|
-
price = routeEntry.maxPrice;
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
return { price };
|
|
1723
|
-
} catch (err) {
|
|
1724
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1725
|
-
level: "error",
|
|
1726
|
-
message: `Pricing function failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1727
|
-
route: routeEntry.key,
|
|
1728
|
-
meta: { error: err instanceof Error ? err.stack : String(err), body: bodyData }
|
|
1729
|
-
});
|
|
1730
|
-
if (routeEntry.maxPrice) {
|
|
1731
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1732
|
-
level: "warn",
|
|
1733
|
-
message: `Using maxPrice ${routeEntry.maxPrice} as fallback after pricing error`,
|
|
1734
|
-
route: routeEntry.key
|
|
1735
|
-
});
|
|
1736
|
-
return { price: routeEntry.maxPrice };
|
|
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;
|
|
1737
2137
|
} else {
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
{ status: 500 }
|
|
1741
|
-
);
|
|
1742
|
-
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
1743
|
-
return { error: errorResponse };
|
|
2138
|
+
firePluginResponse(ctx, earlyBody.response);
|
|
2139
|
+
return earlyBody.response;
|
|
1744
2140
|
}
|
|
1745
2141
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
headers: {
|
|
1751
|
-
"Content-Type": "application/json",
|
|
1752
|
-
"Cache-Control": "no-store"
|
|
1753
|
-
}
|
|
1754
|
-
});
|
|
1755
|
-
let challengePrice;
|
|
1756
|
-
if (bodyData !== void 0 && typeof routeEntry.pricing !== "string" && routeEntry.pricing != null) {
|
|
1757
|
-
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
|
1758
|
-
if ("error" in result) return result.error;
|
|
1759
|
-
challengePrice = result.price;
|
|
1760
|
-
} else if (routeEntry.maxPrice) {
|
|
1761
|
-
challengePrice = routeEntry.maxPrice;
|
|
1762
|
-
} else if (routeEntry.pricing) {
|
|
2142
|
+
const siwxHeader = request.headers.get(HEADERS.SIWX);
|
|
2143
|
+
const protocol = detectProtocol(request);
|
|
2144
|
+
if (!siwxHeader && protocol === "mpp" && deps.mppx) {
|
|
2145
|
+
let mppSiwxResult;
|
|
1763
2146
|
try {
|
|
1764
|
-
|
|
1765
|
-
} catch {
|
|
1766
|
-
|
|
2147
|
+
mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2150
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2151
|
+
level: "critical",
|
|
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,
|
|
2162
|
+
route: routeEntry.key
|
|
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;
|
|
1767
2169
|
}
|
|
1768
|
-
} else {
|
|
1769
|
-
challengePrice = "0";
|
|
1770
2170
|
}
|
|
1771
|
-
|
|
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
|
|
2189
|
+
});
|
|
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;
|
|
1772
2210
|
try {
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
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 } : {}
|
|
1792
2229
|
}
|
|
1793
|
-
extensions = declareDiscoveryExtension(config);
|
|
1794
2230
|
}
|
|
2231
|
+
};
|
|
2232
|
+
let encoded;
|
|
2233
|
+
try {
|
|
2234
|
+
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
2235
|
+
encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1795
2236
|
} catch (err) {
|
|
1796
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
2237
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
1797
2238
|
level: "warn",
|
|
1798
|
-
message: `
|
|
2239
|
+
message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1799
2240
|
route: routeEntry.key
|
|
1800
2241
|
});
|
|
1801
2242
|
}
|
|
1802
|
-
|
|
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) {
|
|
1803
2249
|
try {
|
|
1804
|
-
const
|
|
1805
|
-
if (
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
...siwxExtension
|
|
1809
|
-
};
|
|
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);
|
|
1810
2254
|
}
|
|
1811
2255
|
} catch {
|
|
1812
2256
|
}
|
|
1813
2257
|
}
|
|
1814
|
-
|
|
1815
|
-
try {
|
|
1816
|
-
const accepts = await resolveX402Accepts(
|
|
1817
|
-
request,
|
|
1818
|
-
routeEntry,
|
|
1819
|
-
deps.x402Accepts,
|
|
1820
|
-
deps.payeeAddress,
|
|
1821
|
-
bodyData
|
|
1822
|
-
);
|
|
1823
|
-
const { encoded } = await buildX402Challenge({
|
|
1824
|
-
server: deps.x402Server,
|
|
1825
|
-
routeEntry,
|
|
1826
|
-
request,
|
|
1827
|
-
price: challengePrice,
|
|
1828
|
-
accepts,
|
|
1829
|
-
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
1830
|
-
extensions
|
|
1831
|
-
});
|
|
1832
|
-
response.headers.set("PAYMENT-REQUIRED", encoded);
|
|
1833
|
-
} catch (err) {
|
|
1834
|
-
const message = `x402 challenge build failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1835
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1836
|
-
level: "critical",
|
|
1837
|
-
message,
|
|
1838
|
-
route: routeEntry.key
|
|
1839
|
-
});
|
|
1840
|
-
const errorResponse = NextResponse2.json({ success: false, error: message }, { status: 500 });
|
|
1841
|
-
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
1842
|
-
return errorResponse;
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
if (routeEntry.protocols.includes("mpp") && deps.mppx) {
|
|
1846
|
-
try {
|
|
1847
|
-
const result = await deps.mppx.charge({ amount: challengePrice })(request);
|
|
1848
|
-
if (result.status === 402) {
|
|
1849
|
-
const wwwAuth = result.challenge.headers.get("WWW-Authenticate");
|
|
1850
|
-
if (wwwAuth) response.headers.set("WWW-Authenticate", wwwAuth);
|
|
1851
|
-
}
|
|
1852
|
-
} catch (err) {
|
|
1853
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1854
|
-
level: "critical",
|
|
1855
|
-
message: `MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1856
|
-
route: routeEntry.key
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
firePluginResponse(deps, pluginCtx, meta, response);
|
|
2258
|
+
firePluginResponse(ctx, response);
|
|
1861
2259
|
return response;
|
|
1862
2260
|
}
|
|
1863
|
-
function
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
firePluginHook(deps.plugin, "onError", pluginCtx, {
|
|
1875
|
-
status: response.status,
|
|
1876
|
-
message: response.statusText || `HTTP ${response.status}`,
|
|
1877
|
-
settled: false
|
|
1878
|
-
});
|
|
2261
|
+
function siwxSignatureType(network) {
|
|
2262
|
+
return network.startsWith("solana:") ? "ed25519" : "eip191";
|
|
2263
|
+
}
|
|
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
|
+
}
|
|
1879
2272
|
}
|
|
2273
|
+
if (chains.length === 0) {
|
|
2274
|
+
chains.push({ chainId: fallbackNetwork, type: siwxSignatureType(fallbackNetwork) });
|
|
2275
|
+
}
|
|
2276
|
+
return chains;
|
|
1880
2277
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
return "healthy";
|
|
2278
|
+
|
|
2279
|
+
// src/pipeline/flows/unprotected.ts
|
|
2280
|
+
async function runUnprotectedFlow(ctx) {
|
|
2281
|
+
return runHandlerOnly(ctx, null, void 0);
|
|
1886
2282
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
const
|
|
1893
|
-
if (
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
remaining: quota.remaining,
|
|
1900
|
-
limit: quota.limit,
|
|
1901
|
-
spend: quota.spend,
|
|
1902
|
-
level,
|
|
1903
|
-
overage,
|
|
1904
|
-
message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
|
|
1905
|
-
};
|
|
1906
|
-
firePluginHook(deps.plugin, "onProviderQuota", pluginCtx, event);
|
|
1907
|
-
} catch {
|
|
1908
|
-
}
|
|
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
|
+
};
|
|
1909
2295
|
}
|
|
1910
2296
|
|
|
1911
2297
|
// src/validate-examples.ts
|
|
1912
2298
|
function validateExamples(key, bodySchema, querySchema, outputSchema, inputExample, hasInputExample, outputExample, hasOutputExample) {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
);
|
|
1917
|
-
}
|
|
1918
|
-
if (querySchema && !hasInputExample) {
|
|
1919
|
-
throw new Error(
|
|
1920
|
-
`route '${key}': .query() requires a matching .inputExample() \u2014 the bazaar discovery extension needs a conforming sample query to advertise.`
|
|
1921
|
-
);
|
|
2299
|
+
const inputSchema = bodySchema ?? querySchema;
|
|
2300
|
+
if (hasInputExample && !inputSchema) {
|
|
2301
|
+
throw new Error(`route '${key}': .inputExample() requires .body() or .query()`);
|
|
1922
2302
|
}
|
|
1923
|
-
if (
|
|
1924
|
-
throw new Error(
|
|
1925
|
-
`route '${key}': .output() requires a matching .outputExample() \u2014 the bazaar discovery extension needs a conforming sample response to advertise.`
|
|
1926
|
-
);
|
|
2303
|
+
if (hasOutputExample && !outputSchema) {
|
|
2304
|
+
throw new Error(`route '${key}': .outputExample() requires .output()`);
|
|
1927
2305
|
}
|
|
1928
|
-
const inputSchema = bodySchema ?? querySchema;
|
|
1929
2306
|
if (inputSchema && hasInputExample) {
|
|
1930
2307
|
const result = inputSchema.safeParse(inputExample);
|
|
1931
2308
|
if (!result.success) {
|
|
@@ -1999,6 +2376,8 @@ var RouteBuilder = class {
|
|
|
1999
2376
|
/** @internal */
|
|
2000
2377
|
_validateFn;
|
|
2001
2378
|
/** @internal */
|
|
2379
|
+
_settlement;
|
|
2380
|
+
/** @internal */
|
|
2002
2381
|
_mppInfo;
|
|
2003
2382
|
constructor(key, registry, deps) {
|
|
2004
2383
|
this._key = key;
|
|
@@ -2012,11 +2391,21 @@ var RouteBuilder = class {
|
|
|
2012
2391
|
return next;
|
|
2013
2392
|
}
|
|
2014
2393
|
paid(pricing, options) {
|
|
2394
|
+
if (this._authMode === "unprotected") {
|
|
2395
|
+
throw new Error(
|
|
2396
|
+
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
if (this._pricing !== void 0) {
|
|
2400
|
+
throw new Error(
|
|
2401
|
+
`route '${this._key}': Cannot call .paid() more than once on the same route.`
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2015
2404
|
const next = this.fork();
|
|
2016
2405
|
next._authMode = "paid";
|
|
2017
2406
|
next._pricing = pricing;
|
|
2018
2407
|
if (options?.protocols) {
|
|
2019
|
-
next._protocols = options.protocols;
|
|
2408
|
+
next._protocols = [...options.protocols];
|
|
2020
2409
|
} else if (next._protocols.length === 0) {
|
|
2021
2410
|
next._protocols = ["x402"];
|
|
2022
2411
|
}
|
|
@@ -2081,6 +2470,16 @@ var RouteBuilder = class {
|
|
|
2081
2470
|
return next;
|
|
2082
2471
|
}
|
|
2083
2472
|
unprotected() {
|
|
2473
|
+
if (this._authMode && this._authMode !== "unprotected") {
|
|
2474
|
+
throw new Error(
|
|
2475
|
+
`route '${this._key}': Cannot combine .unprotected() and .${this._authMode}() on the same route.`
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
if (this._pricing) {
|
|
2479
|
+
throw new Error(
|
|
2480
|
+
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2084
2483
|
const next = this.fork();
|
|
2085
2484
|
next._authMode = "unprotected";
|
|
2086
2485
|
next._protocols = [];
|
|
@@ -2095,32 +2494,43 @@ var RouteBuilder = class {
|
|
|
2095
2494
|
next._providerConfig = config ?? {};
|
|
2096
2495
|
return next;
|
|
2097
2496
|
}
|
|
2098
|
-
|
|
2099
|
-
// Schema methods
|
|
2100
|
-
// -------------------------------------------------------------------------
|
|
2101
|
-
body(schema) {
|
|
2497
|
+
body(schema, example) {
|
|
2102
2498
|
const next = this.fork();
|
|
2103
2499
|
next._bodySchema = schema;
|
|
2500
|
+
if (example !== void 0) {
|
|
2501
|
+
next._inputExample = example;
|
|
2502
|
+
next._hasInputExample = true;
|
|
2503
|
+
}
|
|
2104
2504
|
return next;
|
|
2105
2505
|
}
|
|
2106
|
-
query(schema) {
|
|
2506
|
+
query(schema, example) {
|
|
2107
2507
|
const next = this.fork();
|
|
2108
2508
|
next._querySchema = schema;
|
|
2509
|
+
if (example !== void 0) {
|
|
2510
|
+
next._inputExample = example;
|
|
2511
|
+
next._hasInputExample = true;
|
|
2512
|
+
}
|
|
2109
2513
|
next._method = "GET";
|
|
2110
2514
|
return next;
|
|
2111
2515
|
}
|
|
2112
|
-
output(schema) {
|
|
2516
|
+
output(schema, example) {
|
|
2113
2517
|
const next = this.fork();
|
|
2114
2518
|
next._outputSchema = schema;
|
|
2519
|
+
if (example !== void 0) {
|
|
2520
|
+
next._outputExample = example;
|
|
2521
|
+
next._hasOutputExample = true;
|
|
2522
|
+
}
|
|
2115
2523
|
return next;
|
|
2116
2524
|
}
|
|
2117
2525
|
/**
|
|
2118
2526
|
* Provide a conforming example of the request input (body or query params).
|
|
2119
2527
|
*
|
|
2120
|
-
*
|
|
2121
|
-
*
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2528
|
+
* Optional. When provided, the example is validated against the request schema
|
|
2529
|
+
* at route registration and embedded in the bazaar discovery extension so
|
|
2530
|
+
* indexers can advertise a working sample call.
|
|
2531
|
+
*
|
|
2532
|
+
* For the common case, pass the example directly to `.body(schema, example)` or
|
|
2533
|
+
* `.query(schema, example)` instead.
|
|
2124
2534
|
*
|
|
2125
2535
|
* @example
|
|
2126
2536
|
* ```ts
|
|
@@ -2140,10 +2550,11 @@ var RouteBuilder = class {
|
|
|
2140
2550
|
/**
|
|
2141
2551
|
* Provide a conforming example of the response output.
|
|
2142
2552
|
*
|
|
2143
|
-
*
|
|
2144
|
-
*
|
|
2145
|
-
*
|
|
2146
|
-
*
|
|
2553
|
+
* Optional. When provided, the example is validated against the output schema
|
|
2554
|
+
* at route registration and embedded in the bazaar discovery extension so
|
|
2555
|
+
* indexers can advertise the response shape.
|
|
2556
|
+
*
|
|
2557
|
+
* For the common case, pass the example directly to `.output(schema, example)` instead.
|
|
2147
2558
|
*
|
|
2148
2559
|
* Accepts any JSON value (objects, arrays, or primitives) — top-level array
|
|
2149
2560
|
* or primitive responses (e.g. `z.array(...)`) are supported alongside the
|
|
@@ -2216,15 +2627,39 @@ var RouteBuilder = class {
|
|
|
2216
2627
|
return next;
|
|
2217
2628
|
}
|
|
2218
2629
|
// -------------------------------------------------------------------------
|
|
2630
|
+
// Settlement lifecycle
|
|
2631
|
+
// -------------------------------------------------------------------------
|
|
2632
|
+
/**
|
|
2633
|
+
* Add route-specific settlement hooks.
|
|
2634
|
+
*
|
|
2635
|
+
* `beforeSettle` runs after a successful handler response but before
|
|
2636
|
+
* router-controlled settlement/broadcast, so it can still prevent the charge
|
|
2637
|
+
* for x402 and MPP transaction-payload flows. `afterSettle` runs after
|
|
2638
|
+
* settlement and is intended for durable ledgers or app-owned refund queues.
|
|
2639
|
+
*/
|
|
2640
|
+
settlement(lifecycle) {
|
|
2641
|
+
const next = this.fork();
|
|
2642
|
+
next._settlement = lifecycle;
|
|
2643
|
+
return next;
|
|
2644
|
+
}
|
|
2645
|
+
// -------------------------------------------------------------------------
|
|
2219
2646
|
// Terminal method
|
|
2220
2647
|
// -------------------------------------------------------------------------
|
|
2221
2648
|
handler(fn) {
|
|
2222
2649
|
const handlerFn = fn;
|
|
2650
|
+
if (!this._authMode) {
|
|
2651
|
+
throw new Error(
|
|
2652
|
+
`route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2223
2655
|
if (this._validateFn && !this._bodySchema) {
|
|
2224
2656
|
throw new Error(
|
|
2225
2657
|
`route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
|
|
2226
2658
|
);
|
|
2227
2659
|
}
|
|
2660
|
+
if (this._settlement && !this._pricing) {
|
|
2661
|
+
throw new Error(`route '${this._key}': .settlement() requires a paid route`);
|
|
2662
|
+
}
|
|
2228
2663
|
validateExamples(
|
|
2229
2664
|
this._key,
|
|
2230
2665
|
this._bodySchema,
|
|
@@ -2256,6 +2691,7 @@ var RouteBuilder = class {
|
|
|
2256
2691
|
providerName: this._providerName,
|
|
2257
2692
|
providerConfig: this._providerConfig,
|
|
2258
2693
|
validateFn: this._validateFn,
|
|
2694
|
+
settlement: this._settlement,
|
|
2259
2695
|
mppInfo: this._mppInfo
|
|
2260
2696
|
};
|
|
2261
2697
|
this._registry.register(entry);
|
|
@@ -2330,7 +2766,7 @@ function createRedisEntitlementStore(client, options) {
|
|
|
2330
2766
|
}
|
|
2331
2767
|
|
|
2332
2768
|
// src/discovery/well-known.ts
|
|
2333
|
-
import { NextResponse as
|
|
2769
|
+
import { NextResponse as NextResponse6 } from "next/server";
|
|
2334
2770
|
|
|
2335
2771
|
// src/discovery/utils/guidance.ts
|
|
2336
2772
|
async function resolveGuidance(discovery) {
|
|
@@ -2374,7 +2810,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2374
2810
|
if (instructions) {
|
|
2375
2811
|
body.instructions = instructions;
|
|
2376
2812
|
}
|
|
2377
|
-
return
|
|
2813
|
+
return NextResponse6.json(body, {
|
|
2378
2814
|
headers: {
|
|
2379
2815
|
"Access-Control-Allow-Origin": "*",
|
|
2380
2816
|
"Access-Control-Allow-Methods": "GET",
|
|
@@ -2391,13 +2827,14 @@ function toDiscoveryResource(method, url, mode) {
|
|
|
2391
2827
|
}
|
|
2392
2828
|
|
|
2393
2829
|
// src/discovery/openapi.ts
|
|
2394
|
-
|
|
2830
|
+
init_constants();
|
|
2831
|
+
import { NextResponse as NextResponse7 } from "next/server";
|
|
2395
2832
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
2396
2833
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
2397
2834
|
let cached = null;
|
|
2398
2835
|
let validated = false;
|
|
2399
2836
|
return async (_request) => {
|
|
2400
|
-
if (cached) return
|
|
2837
|
+
if (cached) return NextResponse7.json(cached);
|
|
2401
2838
|
if (!validated && pricesKeys) {
|
|
2402
2839
|
registry.validate(pricesKeys);
|
|
2403
2840
|
validated = true;
|
|
@@ -2422,14 +2859,14 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2422
2859
|
securitySchemes.siwx = {
|
|
2423
2860
|
type: "apiKey",
|
|
2424
2861
|
in: "header",
|
|
2425
|
-
name:
|
|
2862
|
+
name: HEADERS.SIWX
|
|
2426
2863
|
};
|
|
2427
2864
|
}
|
|
2428
2865
|
if (requiresApiKeyScheme) {
|
|
2429
2866
|
securitySchemes.apiKey = {
|
|
2430
2867
|
type: "apiKey",
|
|
2431
2868
|
in: "header",
|
|
2432
|
-
name:
|
|
2869
|
+
name: HEADERS.API_KEY
|
|
2433
2870
|
};
|
|
2434
2871
|
}
|
|
2435
2872
|
const discoveryMetadata = {};
|
|
@@ -2460,7 +2897,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2460
2897
|
paths
|
|
2461
2898
|
};
|
|
2462
2899
|
cached = createDocument(openApiDocument);
|
|
2463
|
-
return
|
|
2900
|
+
return NextResponse7.json(cached);
|
|
2464
2901
|
};
|
|
2465
2902
|
}
|
|
2466
2903
|
function deriveTag(routeKey) {
|
|
@@ -2533,7 +2970,7 @@ function toProtocolObject(protocol, mppInfo) {
|
|
|
2533
2970
|
mpp: {
|
|
2534
2971
|
method: mppInfo?.method ?? "tempo",
|
|
2535
2972
|
intent: mppInfo?.intent ?? "charge",
|
|
2536
|
-
currency: mppInfo?.currency ??
|
|
2973
|
+
currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
|
|
2537
2974
|
}
|
|
2538
2975
|
};
|
|
2539
2976
|
}
|
|
@@ -2583,11 +3020,11 @@ function buildPricingInfo(entry) {
|
|
|
2583
3020
|
}
|
|
2584
3021
|
|
|
2585
3022
|
// src/discovery/llms-txt.ts
|
|
2586
|
-
import { NextResponse as
|
|
3023
|
+
import { NextResponse as NextResponse8 } from "next/server";
|
|
2587
3024
|
function createLlmsTxtHandler(discovery) {
|
|
2588
3025
|
return async (_request) => {
|
|
2589
3026
|
const guidance = await resolveGuidance(discovery) ?? "";
|
|
2590
|
-
return new
|
|
3027
|
+
return new NextResponse8(guidance, {
|
|
2591
3028
|
headers: {
|
|
2592
3029
|
"Content-Type": "text/plain; charset=utf-8",
|
|
2593
3030
|
"Access-Control-Allow-Origin": "*"
|
|
@@ -2597,14 +3034,14 @@ function createLlmsTxtHandler(discovery) {
|
|
|
2597
3034
|
}
|
|
2598
3035
|
|
|
2599
3036
|
// src/index.ts
|
|
2600
|
-
|
|
3037
|
+
init_accepts();
|
|
2601
3038
|
init_constants();
|
|
2602
3039
|
|
|
2603
3040
|
// src/config.ts
|
|
2604
3041
|
init_constants();
|
|
2605
3042
|
init_evm();
|
|
2606
3043
|
init_solana();
|
|
2607
|
-
|
|
3044
|
+
init_accepts();
|
|
2608
3045
|
var RouterConfigError = class extends Error {
|
|
2609
3046
|
issues;
|
|
2610
3047
|
constructor(issues) {
|
|
@@ -2648,6 +3085,8 @@ function mppFromEnv(env, options = {}) {
|
|
|
2648
3085
|
const secretKey = env.MPP_SECRET_KEY;
|
|
2649
3086
|
const currency = env.MPP_CURRENCY;
|
|
2650
3087
|
const rpcUrl = env.TEMPO_RPC_URL;
|
|
3088
|
+
const feePayerKey = options.feePayerKey ?? env.MPP_FEE_PAYER_KEY;
|
|
3089
|
+
const feePayerKeySource = options.feePayerKey !== void 0 ? "feePayerKey" : "MPP_FEE_PAYER_KEY";
|
|
2651
3090
|
const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
|
|
2652
3091
|
if (!hasAnyMppEnv) return void 0;
|
|
2653
3092
|
const missing = [
|
|
@@ -2658,12 +3097,21 @@ function mppFromEnv(env, options = {}) {
|
|
|
2658
3097
|
if (missing.length > 0) {
|
|
2659
3098
|
throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
|
|
2660
3099
|
}
|
|
3100
|
+
if (!isEvmAddress(currency)) {
|
|
3101
|
+
throw new Error("MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address");
|
|
3102
|
+
}
|
|
3103
|
+
if (options.recipient && !isEvmAddress(options.recipient)) {
|
|
3104
|
+
throw new Error("MPP recipient must be a 0x-prefixed EVM address");
|
|
3105
|
+
}
|
|
3106
|
+
if (feePayerKey && !isEvmPrivateKey(feePayerKey)) {
|
|
3107
|
+
throw new Error(`${feePayerKeySource} must be a 0x-prefixed 32-byte EVM private key`);
|
|
3108
|
+
}
|
|
2661
3109
|
return {
|
|
2662
3110
|
secretKey,
|
|
2663
3111
|
currency,
|
|
2664
3112
|
rpcUrl,
|
|
2665
3113
|
...options.recipient ? { recipient: options.recipient } : {},
|
|
2666
|
-
...
|
|
3114
|
+
...feePayerKey ? { feePayerKey } : {},
|
|
2667
3115
|
...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
|
|
2668
3116
|
};
|
|
2669
3117
|
}
|
|
@@ -2801,13 +3249,26 @@ function validateMppConfig(config, env) {
|
|
|
2801
3249
|
protocol: "mpp",
|
|
2802
3250
|
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
2803
3251
|
});
|
|
3252
|
+
} else if (!isEvmAddress(mpp.currency)) {
|
|
3253
|
+
issues.push({
|
|
3254
|
+
code: "invalid_mpp_currency",
|
|
3255
|
+
protocol: "mpp",
|
|
3256
|
+
message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
|
|
3257
|
+
});
|
|
2804
3258
|
}
|
|
2805
|
-
|
|
3259
|
+
const mppRecipient = mpp.recipient ?? config.payeeAddress;
|
|
3260
|
+
if (!mppRecipient) {
|
|
2806
3261
|
issues.push({
|
|
2807
3262
|
code: "missing_mpp_recipient",
|
|
2808
3263
|
protocol: "mpp",
|
|
2809
3264
|
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
2810
3265
|
});
|
|
3266
|
+
} else if (!isEvmAddress(mppRecipient)) {
|
|
3267
|
+
issues.push({
|
|
3268
|
+
code: "invalid_mpp_recipient",
|
|
3269
|
+
protocol: "mpp",
|
|
3270
|
+
message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
3271
|
+
});
|
|
2811
3272
|
}
|
|
2812
3273
|
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
2813
3274
|
if (placeholder) {
|
|
@@ -2824,6 +3285,13 @@ function validateMppConfig(config, env) {
|
|
|
2824
3285
|
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
2825
3286
|
});
|
|
2826
3287
|
}
|
|
3288
|
+
if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
|
|
3289
|
+
issues.push({
|
|
3290
|
+
code: "invalid_mpp_fee_payer_key",
|
|
3291
|
+
protocol: "mpp",
|
|
3292
|
+
message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
2827
3295
|
if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
|
|
2828
3296
|
issues.push({
|
|
2829
3297
|
code: "missing_mpp_default_store_env",
|
|
@@ -2841,8 +3309,14 @@ function usesDefaultEvmFacilitator(config) {
|
|
|
2841
3309
|
function isSupportedX402Network(network) {
|
|
2842
3310
|
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2843
3311
|
}
|
|
3312
|
+
function isEvmAddress(value) {
|
|
3313
|
+
return /^0x[a-fA-F0-9]{40}$/.test(value);
|
|
3314
|
+
}
|
|
3315
|
+
function isEvmPrivateKey(value) {
|
|
3316
|
+
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
3317
|
+
}
|
|
2844
3318
|
function findPlaceholderPayee(values) {
|
|
2845
|
-
return values.find((value) => value
|
|
3319
|
+
return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
|
|
2846
3320
|
}
|
|
2847
3321
|
|
|
2848
3322
|
// src/index.ts
|
|
@@ -2891,6 +3365,7 @@ function createRouter(config) {
|
|
|
2891
3365
|
nonceStore,
|
|
2892
3366
|
entitlementStore,
|
|
2893
3367
|
payeeAddress: config.payeeAddress ?? "",
|
|
3368
|
+
mppRecipient: config.mpp?.recipient ?? config.payeeAddress,
|
|
2894
3369
|
network,
|
|
2895
3370
|
x402FacilitatorsByNetwork: void 0,
|
|
2896
3371
|
x402Accepts,
|