@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.cjs
CHANGED
|
@@ -30,7 +30,65 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
-
// src/
|
|
33
|
+
// src/constants.ts
|
|
34
|
+
var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
|
|
35
|
+
var init_constants = __esm({
|
|
36
|
+
"src/constants.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
BASE_NETWORK = "eip155:8453";
|
|
39
|
+
SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
40
|
+
TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
41
|
+
ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// src/protocols/x402/accepts.ts
|
|
46
|
+
async function resolvePayToValue(payTo, request, fallback, body) {
|
|
47
|
+
if (!payTo) return fallback;
|
|
48
|
+
if (typeof payTo === "string") return payTo;
|
|
49
|
+
return payTo(request, body);
|
|
50
|
+
}
|
|
51
|
+
function getConfiguredX402Accepts(config) {
|
|
52
|
+
if (config.x402?.accepts?.length) {
|
|
53
|
+
return [...config.x402.accepts];
|
|
54
|
+
}
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
scheme: "exact",
|
|
58
|
+
network: config.network ?? BASE_NETWORK,
|
|
59
|
+
payTo: config.payeeAddress
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
function getConfiguredX402Networks(config) {
|
|
64
|
+
return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
|
|
65
|
+
}
|
|
66
|
+
async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
|
|
67
|
+
return Promise.all(
|
|
68
|
+
accepts.map(async (accept) => ({
|
|
69
|
+
network: accept.network,
|
|
70
|
+
scheme: accept.scheme ?? "exact",
|
|
71
|
+
payTo: await resolvePayToValue(
|
|
72
|
+
routeEntry.payTo ?? accept.payTo,
|
|
73
|
+
request,
|
|
74
|
+
fallbackPayTo,
|
|
75
|
+
body
|
|
76
|
+
),
|
|
77
|
+
...accept.asset ? { asset: accept.asset } : {},
|
|
78
|
+
...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
|
|
79
|
+
...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
|
|
80
|
+
...accept.extra ? { extra: accept.extra } : {}
|
|
81
|
+
}))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
var init_accepts = __esm({
|
|
85
|
+
"src/protocols/x402/accepts.ts"() {
|
|
86
|
+
"use strict";
|
|
87
|
+
init_constants();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// src/protocols/x402/evm.ts
|
|
34
92
|
function isEvmNetwork(network) {
|
|
35
93
|
return network.startsWith("eip155:");
|
|
36
94
|
}
|
|
@@ -48,12 +106,12 @@ function buildEvmExactOptions(accepts, price) {
|
|
|
48
106
|
}));
|
|
49
107
|
}
|
|
50
108
|
var init_evm = __esm({
|
|
51
|
-
"src/protocols/evm.ts"() {
|
|
109
|
+
"src/protocols/x402/evm.ts"() {
|
|
52
110
|
"use strict";
|
|
53
111
|
}
|
|
54
112
|
});
|
|
55
113
|
|
|
56
|
-
// src/protocols/solana.ts
|
|
114
|
+
// src/protocols/x402/solana.ts
|
|
57
115
|
function isSolanaNetwork(network) {
|
|
58
116
|
return network.startsWith("solana:");
|
|
59
117
|
}
|
|
@@ -103,13 +161,13 @@ function isSolanaRequirement(requirement) {
|
|
|
103
161
|
return isSolanaNetwork(requirement.network);
|
|
104
162
|
}
|
|
105
163
|
var init_solana = __esm({
|
|
106
|
-
"src/protocols/solana.ts"() {
|
|
164
|
+
"src/protocols/x402/solana.ts"() {
|
|
107
165
|
"use strict";
|
|
108
|
-
|
|
166
|
+
init_facilitators();
|
|
109
167
|
}
|
|
110
168
|
});
|
|
111
169
|
|
|
112
|
-
// src/x402
|
|
170
|
+
// src/protocols/x402/facilitators.ts
|
|
113
171
|
function getResolvedX402Facilitator(config, network, defaultEvmFacilitator) {
|
|
114
172
|
const family = getNetworkFamily(network);
|
|
115
173
|
if (!family) return null;
|
|
@@ -179,8 +237,8 @@ function sameFacilitatorConfig(a, b) {
|
|
|
179
237
|
return a.url === b.url && a.createAuthHeaders === b.createAuthHeaders && a.createAcceptsHeaders === b.createAcceptsHeaders;
|
|
180
238
|
}
|
|
181
239
|
var DEFAULT_SOLANA_FACILITATOR_URL;
|
|
182
|
-
var
|
|
183
|
-
"src/x402
|
|
240
|
+
var init_facilitators = __esm({
|
|
241
|
+
"src/protocols/x402/facilitators.ts"() {
|
|
184
242
|
"use strict";
|
|
185
243
|
init_evm();
|
|
186
244
|
init_solana();
|
|
@@ -188,64 +246,6 @@ var init_x402_facilitators = __esm({
|
|
|
188
246
|
}
|
|
189
247
|
});
|
|
190
248
|
|
|
191
|
-
// src/constants.ts
|
|
192
|
-
var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
|
|
193
|
-
var init_constants = __esm({
|
|
194
|
-
"src/constants.ts"() {
|
|
195
|
-
"use strict";
|
|
196
|
-
BASE_NETWORK = "eip155:8453";
|
|
197
|
-
SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
198
|
-
TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
199
|
-
ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// src/x402-config.ts
|
|
204
|
-
async function resolvePayToValue(payTo, request, fallback, body) {
|
|
205
|
-
if (!payTo) return fallback;
|
|
206
|
-
if (typeof payTo === "string") return payTo;
|
|
207
|
-
return payTo(request, body);
|
|
208
|
-
}
|
|
209
|
-
function getConfiguredX402Accepts(config) {
|
|
210
|
-
if (config.x402?.accepts?.length) {
|
|
211
|
-
return [...config.x402.accepts];
|
|
212
|
-
}
|
|
213
|
-
return [
|
|
214
|
-
{
|
|
215
|
-
scheme: "exact",
|
|
216
|
-
network: config.network ?? BASE_NETWORK,
|
|
217
|
-
payTo: config.payeeAddress
|
|
218
|
-
}
|
|
219
|
-
];
|
|
220
|
-
}
|
|
221
|
-
function getConfiguredX402Networks(config) {
|
|
222
|
-
return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
|
|
223
|
-
}
|
|
224
|
-
async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
|
|
225
|
-
return Promise.all(
|
|
226
|
-
accepts.map(async (accept) => ({
|
|
227
|
-
network: accept.network,
|
|
228
|
-
scheme: accept.scheme ?? "exact",
|
|
229
|
-
payTo: await resolvePayToValue(
|
|
230
|
-
routeEntry.payTo ?? accept.payTo,
|
|
231
|
-
request,
|
|
232
|
-
fallbackPayTo,
|
|
233
|
-
body
|
|
234
|
-
),
|
|
235
|
-
...accept.asset ? { asset: accept.asset } : {},
|
|
236
|
-
...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
|
|
237
|
-
...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
|
|
238
|
-
...accept.extra ? { extra: accept.extra } : {}
|
|
239
|
-
}))
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
var init_x402_config = __esm({
|
|
243
|
-
"src/x402-config.ts"() {
|
|
244
|
-
"use strict";
|
|
245
|
-
init_constants();
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
249
|
// src/server.ts
|
|
250
250
|
var server_exports = {};
|
|
251
251
|
__export(server_exports, {
|
|
@@ -330,8 +330,8 @@ var init_server = __esm({
|
|
|
330
330
|
"use strict";
|
|
331
331
|
init_evm();
|
|
332
332
|
init_solana();
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
init_facilitators();
|
|
334
|
+
init_accepts();
|
|
335
335
|
}
|
|
336
336
|
});
|
|
337
337
|
|
|
@@ -465,13 +465,32 @@ var RouteRegistry = class {
|
|
|
465
465
|
}
|
|
466
466
|
};
|
|
467
467
|
|
|
468
|
-
// src/
|
|
469
|
-
var
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
468
|
+
// src/headers.ts
|
|
469
|
+
var HEADERS = {
|
|
470
|
+
// ---- Standard HTTP ----
|
|
471
|
+
AUTHORIZATION: "Authorization",
|
|
472
|
+
WWW_AUTHENTICATE: "WWW-Authenticate",
|
|
473
|
+
// ---- Auth ----
|
|
474
|
+
API_KEY: "X-API-Key",
|
|
475
|
+
// ---- Request meta (used by plugin/observability) ----
|
|
476
|
+
WALLET_ADDRESS: "X-Wallet-Address",
|
|
477
|
+
CLIENT_ID: "X-Client-ID",
|
|
478
|
+
SESSION_ID: "X-Session-ID",
|
|
479
|
+
// ---- SIWX ----
|
|
480
|
+
SIWX: "SIGN-IN-WITH-X",
|
|
481
|
+
// ---- x402 (payment) ----
|
|
482
|
+
X402_PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
|
|
483
|
+
/** Legacy x402 payment header — accepted alongside PAYMENT-SIGNATURE. */
|
|
484
|
+
X402_PAYMENT_LEGACY: "X-PAYMENT",
|
|
485
|
+
X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
|
|
486
|
+
X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
|
|
487
|
+
// ---- MPP (payment) ----
|
|
488
|
+
MPP_PAYMENT_RECEIPT: "Payment-Receipt"
|
|
489
|
+
};
|
|
490
|
+
var AUTH_SCHEME = {
|
|
491
|
+
BEARER: "Bearer ",
|
|
492
|
+
MPP_PAYMENT: "Payment "
|
|
493
|
+
};
|
|
475
494
|
|
|
476
495
|
// src/plugin.ts
|
|
477
496
|
function createDefaultContext(meta) {
|
|
@@ -548,105 +567,31 @@ function consolePlugin() {
|
|
|
548
567
|
};
|
|
549
568
|
}
|
|
550
569
|
|
|
551
|
-
// src/
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
this.evict();
|
|
557
|
-
if (this.seen.has(nonce)) return false;
|
|
558
|
-
this.seen.set(nonce, Date.now() + SIWX_CHALLENGE_EXPIRY_MS);
|
|
559
|
-
return true;
|
|
560
|
-
}
|
|
561
|
-
evict() {
|
|
562
|
-
const now = Date.now();
|
|
563
|
-
for (const [n, exp] of this.seen) {
|
|
564
|
-
if (exp < now) this.seen.delete(n);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
};
|
|
568
|
-
function detectRedisClientType(client) {
|
|
569
|
-
if (!client || typeof client !== "object") {
|
|
570
|
-
throw new Error(
|
|
571
|
-
"createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
if ("options" in client && "status" in client) {
|
|
575
|
-
return "ioredis";
|
|
576
|
-
}
|
|
577
|
-
const constructor = client.constructor?.name;
|
|
578
|
-
if (constructor === "Redis" && "url" in client) {
|
|
579
|
-
return "upstash";
|
|
580
|
-
}
|
|
581
|
-
if (typeof client.set === "function") {
|
|
582
|
-
return "upstash";
|
|
583
|
-
}
|
|
584
|
-
throw new Error(
|
|
585
|
-
"Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
|
|
586
|
-
);
|
|
570
|
+
// src/pipeline/context/preflight.ts
|
|
571
|
+
function preflight(routeEntry, handler, deps, request) {
|
|
572
|
+
const meta = buildMeta(request, routeEntry);
|
|
573
|
+
const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
|
|
574
|
+
return { routeEntry, handler, deps, request, meta, pluginCtx };
|
|
587
575
|
}
|
|
588
|
-
function
|
|
589
|
-
const prefix = opts?.prefix ?? "siwx:nonce:";
|
|
590
|
-
const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
591
|
-
const clientType = detectRedisClientType(client);
|
|
576
|
+
function buildMeta(request, routeEntry) {
|
|
592
577
|
return {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
throw new Error("Unknown Redis client type");
|
|
606
|
-
}
|
|
578
|
+
requestId: crypto.randomUUID(),
|
|
579
|
+
method: request.method,
|
|
580
|
+
route: routeEntry.key,
|
|
581
|
+
origin: request.headers.get("origin") ?? new URL(request.url).origin,
|
|
582
|
+
referer: request.headers.get("referer"),
|
|
583
|
+
walletAddress: request.headers.get(HEADERS.WALLET_ADDRESS),
|
|
584
|
+
clientId: request.headers.get(HEADERS.CLIENT_ID),
|
|
585
|
+
sessionId: request.headers.get(HEADERS.SESSION_ID),
|
|
586
|
+
contentType: request.headers.get("content-type"),
|
|
587
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
588
|
+
startTime: Date.now()
|
|
607
589
|
};
|
|
608
590
|
}
|
|
609
591
|
|
|
610
|
-
// src/
|
|
611
|
-
function detectProtocol(request) {
|
|
612
|
-
if (request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT")) {
|
|
613
|
-
return "x402";
|
|
614
|
-
}
|
|
615
|
-
const auth = request.headers.get("Authorization");
|
|
616
|
-
if (auth && auth.startsWith("Payment ")) {
|
|
617
|
-
return "mpp";
|
|
618
|
-
}
|
|
619
|
-
if (request.headers.get("SIGN-IN-WITH-X")) {
|
|
620
|
-
return "siwx";
|
|
621
|
-
}
|
|
622
|
-
return null;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// src/handler.ts
|
|
592
|
+
// src/pipeline/context/parse-body.ts
|
|
626
593
|
var import_server = require("next/server");
|
|
627
594
|
|
|
628
|
-
// src/types.ts
|
|
629
|
-
var HttpError = class extends Error {
|
|
630
|
-
constructor(message, status) {
|
|
631
|
-
super(message);
|
|
632
|
-
this.status = status;
|
|
633
|
-
this.name = "HttpError";
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
// src/handler.ts
|
|
638
|
-
async function safeCallHandler(handler, ctx) {
|
|
639
|
-
try {
|
|
640
|
-
const result = await handler(ctx);
|
|
641
|
-
if (result instanceof Response) return result;
|
|
642
|
-
return import_server.NextResponse.json(result);
|
|
643
|
-
} catch (error) {
|
|
644
|
-
const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
|
|
645
|
-
const message = error instanceof Error ? error.message : "Internal error";
|
|
646
|
-
return import_server.NextResponse.json({ success: false, error: message }, { status });
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
595
|
// src/body.ts
|
|
651
596
|
async function bufferBody(request) {
|
|
652
597
|
const text = await request.text();
|
|
@@ -671,296 +616,329 @@ function validateBody(parsed, schema) {
|
|
|
671
616
|
};
|
|
672
617
|
}
|
|
673
618
|
|
|
674
|
-
// src/
|
|
675
|
-
async function
|
|
676
|
-
if (
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if (
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
if (defaultTier && tiers[defaultTier]) {
|
|
688
|
-
return tiers[defaultTier].price;
|
|
689
|
-
}
|
|
690
|
-
if (!tierKey) {
|
|
691
|
-
throw Object.assign(new Error(`Missing required field '${field}' for tier pricing`), {
|
|
692
|
-
status: 400
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
throw Object.assign(
|
|
696
|
-
new Error(
|
|
697
|
-
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
698
|
-
),
|
|
699
|
-
{ status: 400 }
|
|
700
|
-
);
|
|
619
|
+
// src/pipeline/context/parse-body.ts
|
|
620
|
+
async function parseBody(request, routeEntry) {
|
|
621
|
+
if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
|
|
622
|
+
const raw = await bufferBody(request);
|
|
623
|
+
const result = validateBody(raw, routeEntry.bodySchema);
|
|
624
|
+
if (result.success) return { ok: true, data: result.data };
|
|
625
|
+
return {
|
|
626
|
+
ok: false,
|
|
627
|
+
response: import_server.NextResponse.json(
|
|
628
|
+
{ success: false, error: result.error, issues: result.issues },
|
|
629
|
+
{ status: 400 }
|
|
630
|
+
)
|
|
631
|
+
};
|
|
701
632
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
for (const tier of Object.values(tiers)) {
|
|
710
|
-
if (parseFloat(tier.price) > parseFloat(max)) {
|
|
711
|
-
max = tier.price;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
return max;
|
|
633
|
+
|
|
634
|
+
// src/pipeline/context/parse-query.ts
|
|
635
|
+
function parseQuery(request, routeEntry) {
|
|
636
|
+
if (!routeEntry.querySchema) return void 0;
|
|
637
|
+
const params = Object.fromEntries(request.nextUrl.searchParams.entries());
|
|
638
|
+
const result = routeEntry.querySchema.safeParse(params);
|
|
639
|
+
return result.success ? result.data : params;
|
|
715
640
|
}
|
|
716
641
|
|
|
717
|
-
// src/
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
642
|
+
// src/pipeline/context/errors.ts
|
|
643
|
+
function errorStatus(error, fallback) {
|
|
644
|
+
const status = error?.status;
|
|
645
|
+
return typeof status === "number" ? status : fallback;
|
|
646
|
+
}
|
|
647
|
+
function errorMessage(error, fallback) {
|
|
648
|
+
return error instanceof Error ? error.message : fallback;
|
|
649
|
+
}
|
|
650
|
+
function handlerFailureError(response) {
|
|
651
|
+
const message = response.statusText || `Handler returned HTTP ${response.status}`;
|
|
652
|
+
return Object.assign(new Error(message), { status: response.status });
|
|
653
|
+
}
|
|
722
654
|
|
|
723
|
-
// src/
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
extensions
|
|
744
|
-
);
|
|
745
|
-
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
746
|
-
return { encoded, requirements };
|
|
747
|
-
}
|
|
748
|
-
async function verifyX402Payment(opts) {
|
|
749
|
-
const { server, request, price, accepts } = opts;
|
|
750
|
-
const payload = await readPaymentPayload(request);
|
|
751
|
-
if (!payload) return null;
|
|
752
|
-
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
753
|
-
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
754
|
-
if (!matching) {
|
|
755
|
-
return invalidPaymentVerification();
|
|
655
|
+
// src/pipeline/context/fail.ts
|
|
656
|
+
var import_server2 = require("next/server");
|
|
657
|
+
|
|
658
|
+
// src/pipeline/context/fire-plugin-response.ts
|
|
659
|
+
function firePluginResponse(ctx, response, requestBody, responseBody) {
|
|
660
|
+
firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
|
|
661
|
+
statusCode: response.status,
|
|
662
|
+
statusText: response.statusText,
|
|
663
|
+
duration: Date.now() - ctx.meta.startTime,
|
|
664
|
+
contentType: response.headers.get("content-type"),
|
|
665
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
666
|
+
requestBody,
|
|
667
|
+
responseBody
|
|
668
|
+
});
|
|
669
|
+
if (response.status >= 400 && response.status !== 402) {
|
|
670
|
+
firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
|
|
671
|
+
status: response.status,
|
|
672
|
+
message: response.statusText || `HTTP ${response.status}`,
|
|
673
|
+
settled: false
|
|
674
|
+
});
|
|
756
675
|
}
|
|
757
|
-
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/pipeline/context/fail.ts
|
|
679
|
+
function fail(ctx, status, message, requestBody) {
|
|
680
|
+
const response = import_server2.NextResponse.json({ success: false, error: message }, { status });
|
|
681
|
+
firePluginResponse(ctx, response, requestBody);
|
|
682
|
+
return response;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/pipeline/context/run-validate.ts
|
|
686
|
+
async function runValidate(ctx, body) {
|
|
687
|
+
if (!ctx.routeEntry.validateFn) return null;
|
|
758
688
|
try {
|
|
759
|
-
|
|
689
|
+
await ctx.routeEntry.validateFn(body);
|
|
690
|
+
return null;
|
|
760
691
|
} catch (err) {
|
|
761
|
-
|
|
762
|
-
if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
|
|
763
|
-
throw err;
|
|
692
|
+
return fail(ctx, errorStatus(err, 400), errorMessage(err, "Validation failed"), body);
|
|
764
693
|
}
|
|
765
|
-
if (!verify.isValid) return invalidPaymentVerification();
|
|
766
|
-
return {
|
|
767
|
-
valid: true,
|
|
768
|
-
payer: verify.payer,
|
|
769
|
-
payload,
|
|
770
|
-
requirements: matching
|
|
771
|
-
};
|
|
772
694
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
695
|
+
|
|
696
|
+
// src/handler.ts
|
|
697
|
+
var import_server3 = require("next/server");
|
|
698
|
+
|
|
699
|
+
// src/types.ts
|
|
700
|
+
var HttpError = class extends Error {
|
|
701
|
+
constructor(message, status) {
|
|
702
|
+
super(message);
|
|
703
|
+
this.status = status;
|
|
704
|
+
this.name = "HttpError";
|
|
777
705
|
}
|
|
778
|
-
|
|
779
|
-
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// src/handler.ts
|
|
709
|
+
async function safeCallHandler(handler, ctx, options = {}) {
|
|
710
|
+
try {
|
|
711
|
+
const result = await handler(ctx);
|
|
712
|
+
if (result instanceof Response) return result;
|
|
713
|
+
return import_server3.NextResponse.json(result);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
options.onError?.(error);
|
|
716
|
+
const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
|
|
717
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
718
|
+
return import_server3.NextResponse.json({ success: false, error: message }, { status });
|
|
780
719
|
}
|
|
781
|
-
const stableMatch = requirements.find(
|
|
782
|
-
(requirement) => matchesStableFields(requirement, payload.accepted)
|
|
783
|
-
);
|
|
784
|
-
return stableMatch ? payload.accepted : null;
|
|
785
|
-
}
|
|
786
|
-
function matchesStableFields(requirement, accepted) {
|
|
787
|
-
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;
|
|
788
|
-
}
|
|
789
|
-
async function buildExpectedRequirements(server, request, price, accepts) {
|
|
790
|
-
const exactRequirements = await buildExactRequirements(server, request, price, accepts);
|
|
791
|
-
const customRequirements = buildCustomRequirements(price, accepts);
|
|
792
|
-
return [...exactRequirements, ...customRequirements];
|
|
793
720
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
721
|
+
|
|
722
|
+
// src/pipeline/context/invoke.ts
|
|
723
|
+
async function invoke(ctx, wallet, account, body, payment) {
|
|
724
|
+
const handlerCtx = {
|
|
725
|
+
body,
|
|
726
|
+
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
727
|
+
request: ctx.request,
|
|
728
|
+
requestId: ctx.meta.requestId,
|
|
729
|
+
route: ctx.routeEntry.key,
|
|
730
|
+
wallet,
|
|
731
|
+
payment,
|
|
732
|
+
account,
|
|
733
|
+
alert(level, message, alertMeta) {
|
|
734
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
735
|
+
level,
|
|
736
|
+
message,
|
|
737
|
+
route: ctx.routeEntry.key,
|
|
738
|
+
meta: alertMeta
|
|
739
|
+
});
|
|
740
|
+
},
|
|
741
|
+
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
742
|
+
};
|
|
743
|
+
let rawResult;
|
|
744
|
+
let handlerError;
|
|
745
|
+
const response = await safeCallHandler(
|
|
746
|
+
async (c) => {
|
|
747
|
+
rawResult = await ctx.handler(c);
|
|
748
|
+
return rawResult;
|
|
749
|
+
},
|
|
750
|
+
handlerCtx,
|
|
751
|
+
{
|
|
752
|
+
onError(error) {
|
|
753
|
+
handlerError = error;
|
|
812
754
|
}
|
|
813
|
-
console.warn(
|
|
814
|
-
`[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
|
|
815
|
-
);
|
|
816
755
|
}
|
|
756
|
+
);
|
|
757
|
+
return { response, rawResult, handlerError };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/pipeline/context/fire-provider-quota.ts
|
|
761
|
+
function fireProviderQuota(ctx, response, handlerResult) {
|
|
762
|
+
const { providerName, providerConfig } = ctx.routeEntry;
|
|
763
|
+
if (!providerName || !providerConfig?.extractQuota) return;
|
|
764
|
+
if (response.status >= 400) return;
|
|
765
|
+
try {
|
|
766
|
+
const quota = providerConfig.extractQuota(handlerResult, response.headers);
|
|
767
|
+
if (!quota) return;
|
|
768
|
+
const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
|
|
769
|
+
const overage = providerConfig.overage ?? "same-rate";
|
|
770
|
+
const event = {
|
|
771
|
+
provider: providerName,
|
|
772
|
+
route: ctx.routeEntry.key,
|
|
773
|
+
remaining: quota.remaining,
|
|
774
|
+
limit: quota.limit,
|
|
775
|
+
spend: quota.spend,
|
|
776
|
+
level,
|
|
777
|
+
overage,
|
|
778
|
+
message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
|
|
779
|
+
};
|
|
780
|
+
firePluginHook(ctx.deps.plugin, "onProviderQuota", ctx.pluginCtx, event);
|
|
781
|
+
} catch {
|
|
817
782
|
}
|
|
818
|
-
if (requirements.length > 0) {
|
|
819
|
-
return requirements;
|
|
820
|
-
}
|
|
821
|
-
throw failures[0] ?? new Error("Failed to build x402 exact requirements");
|
|
822
783
|
}
|
|
823
|
-
function
|
|
824
|
-
|
|
784
|
+
function computeQuotaLevel(remaining, warn, critical) {
|
|
785
|
+
if (remaining === null) return "healthy";
|
|
786
|
+
if (critical !== void 0 && remaining <= critical) return "critical";
|
|
787
|
+
if (warn !== void 0 && remaining <= warn) return "warn";
|
|
788
|
+
return "healthy";
|
|
825
789
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
790
|
+
|
|
791
|
+
// src/pipeline/context/finalize.ts
|
|
792
|
+
function finalize(ctx, response, rawResult, requestBody) {
|
|
793
|
+
fireProviderQuota(ctx, response, rawResult);
|
|
794
|
+
firePluginResponse(ctx, response, requestBody, rawResult);
|
|
795
|
+
return response;
|
|
830
796
|
}
|
|
831
|
-
|
|
832
|
-
|
|
797
|
+
|
|
798
|
+
// src/pipeline/context/run-handler-only.ts
|
|
799
|
+
async function runHandlerOnly(ctx, wallet, account) {
|
|
800
|
+
const body = await parseBody(ctx.request, ctx.routeEntry);
|
|
801
|
+
if (!body.ok) {
|
|
802
|
+
firePluginResponse(ctx, body.response);
|
|
803
|
+
return body.response;
|
|
804
|
+
}
|
|
805
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
806
|
+
if (validateErr) return validateErr;
|
|
807
|
+
const result = await invoke(ctx, wallet, account, body.data, null);
|
|
808
|
+
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
833
809
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
810
|
+
|
|
811
|
+
// src/pipeline/context/settlement-context.ts
|
|
812
|
+
function settlementContext(ctx, scope) {
|
|
813
|
+
return {
|
|
814
|
+
route: ctx.routeEntry.key,
|
|
815
|
+
request: ctx.request,
|
|
816
|
+
body: scope.body,
|
|
817
|
+
wallet: scope.wallet,
|
|
818
|
+
account: scope.account,
|
|
819
|
+
payment: scope.payment,
|
|
820
|
+
response: scope.response,
|
|
821
|
+
result: scope.rawResult
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/pipeline/context/run-before-settle.ts
|
|
826
|
+
async function runBeforeSettle(ctx, scope) {
|
|
827
|
+
const hook = ctx.routeEntry.settlement?.beforeSettle;
|
|
828
|
+
if (!hook) return null;
|
|
829
|
+
try {
|
|
830
|
+
await hook(settlementContext(ctx, scope));
|
|
831
|
+
return null;
|
|
832
|
+
} catch (error) {
|
|
833
|
+
return fail(
|
|
834
|
+
ctx,
|
|
835
|
+
errorStatus(error, 500),
|
|
836
|
+
errorMessage(error, "Pre-settlement validation failed"),
|
|
837
|
+
scope.body
|
|
843
838
|
);
|
|
844
839
|
}
|
|
845
|
-
return accepted;
|
|
846
840
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
862
|
-
})
|
|
863
|
-
);
|
|
864
|
-
const enriched = [...requirements];
|
|
865
|
-
results.filter((r) => r.success).forEach(({ group, accepted }) => {
|
|
866
|
-
accepted.forEach((req, offset) => {
|
|
867
|
-
const index = group.items[offset]?.index;
|
|
868
|
-
if (index !== void 0) enriched[index] = req;
|
|
841
|
+
|
|
842
|
+
// src/pipeline/context/run-settlement-error.ts
|
|
843
|
+
async function runSettlementError(ctx, scope, error, phase) {
|
|
844
|
+
const hook = ctx.routeEntry.settlement?.onSettlementError;
|
|
845
|
+
if (!hook) return;
|
|
846
|
+
try {
|
|
847
|
+
await hook({ ...settlementContext(ctx, scope), error, phase });
|
|
848
|
+
} catch (hookError) {
|
|
849
|
+
const message = errorMessage(hookError, "Settlement error hook failed");
|
|
850
|
+
console.error(`[router] ${ctx.routeEntry.key}: onSettlementError failed: ${message}`);
|
|
851
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
852
|
+
level: "error",
|
|
853
|
+
message: `Settlement error hook failed: ${message}`,
|
|
854
|
+
route: ctx.routeEntry.key
|
|
869
855
|
});
|
|
870
|
-
});
|
|
871
|
-
const failedIndices = new Set(
|
|
872
|
-
results.filter((r) => !r.success).flatMap(({ group }) => group.items.map(({ index }) => index))
|
|
873
|
-
);
|
|
874
|
-
const remaining = enriched.filter((_, i) => !failedIndices.has(i));
|
|
875
|
-
if (remaining.length === 0) {
|
|
876
|
-
throw new Error(
|
|
877
|
-
"All facilitator enrichments failed; no payment requirements remain for challenge"
|
|
878
|
-
);
|
|
879
856
|
}
|
|
880
|
-
return remaining;
|
|
881
857
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
items: [{ index, requirement }]
|
|
858
|
+
|
|
859
|
+
// src/pipeline/context/run-after-settle.ts
|
|
860
|
+
async function runAfterSettle(ctx, scope) {
|
|
861
|
+
const hook = ctx.routeEntry.settlement?.afterSettle;
|
|
862
|
+
if (!hook) return;
|
|
863
|
+
try {
|
|
864
|
+
await hook(settlementContext(ctx, scope));
|
|
865
|
+
} catch (error) {
|
|
866
|
+
const message = errorMessage(error, "Post-settlement hook failed");
|
|
867
|
+
console.error(`[router] ${ctx.routeEntry.key}: afterSettle failed: ${message}`);
|
|
868
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
869
|
+
level: "error",
|
|
870
|
+
message: `Post-settlement hook failed: ${message}`,
|
|
871
|
+
route: ctx.routeEntry.key
|
|
897
872
|
});
|
|
898
|
-
|
|
899
|
-
|
|
873
|
+
await runSettlementError(ctx, scope, error, "afterSettle");
|
|
874
|
+
}
|
|
900
875
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
876
|
+
|
|
877
|
+
// src/pipeline/context/run-settled-handler-error.ts
|
|
878
|
+
async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
|
|
879
|
+
const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
|
|
880
|
+
if (!hook) return;
|
|
881
|
+
try {
|
|
882
|
+
await hook({ ...settlementContext(ctx, scope), error });
|
|
883
|
+
} catch (hookError) {
|
|
884
|
+
const message = errorMessage(hookError, "Settled handler error hook failed");
|
|
885
|
+
console.error(`[router] ${ctx.routeEntry.key}: onSettledHandlerError failed: ${message}`);
|
|
886
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
887
|
+
level: "error",
|
|
888
|
+
message: `Settled handler error hook failed: ${message}`,
|
|
889
|
+
route: ctx.routeEntry.key
|
|
890
|
+
});
|
|
907
891
|
}
|
|
908
|
-
return facilitator;
|
|
909
892
|
}
|
|
910
|
-
|
|
911
|
-
|
|
893
|
+
|
|
894
|
+
// src/pipeline/context/grant-entitlement.ts
|
|
895
|
+
async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
896
|
+
if (!ctx.routeEntry.siwxEnabled) return;
|
|
897
|
+
try {
|
|
898
|
+
await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
901
|
+
level: "warn",
|
|
902
|
+
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
903
|
+
route: ctx.routeEntry.key
|
|
904
|
+
});
|
|
905
|
+
}
|
|
912
906
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
function invalidPaymentVerification() {
|
|
944
|
-
return { valid: false, payload: null, requirements: null, payer: null };
|
|
945
|
-
}
|
|
946
|
-
function decimalToAtomicUnits(amount, decimals) {
|
|
947
|
-
const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
|
|
948
|
-
if (!match?.groups) {
|
|
949
|
-
throw new Error(`Invalid decimal amount '${amount}'`);
|
|
950
|
-
}
|
|
951
|
-
const whole = match.groups.whole;
|
|
952
|
-
const fraction = match.groups.fraction ?? "";
|
|
953
|
-
if (fraction.length > decimals) {
|
|
954
|
-
throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
|
|
955
|
-
}
|
|
956
|
-
const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
|
|
957
|
-
return normalized === "" ? "0" : normalized;
|
|
907
|
+
|
|
908
|
+
// src/pipeline/context/settle-and-finalize.ts
|
|
909
|
+
async function settleAndFinalize(args) {
|
|
910
|
+
const { ctx, strategy, verifyOutcome, scope, rawResult, body, onSettleError } = args;
|
|
911
|
+
const { request, routeEntry, deps } = ctx;
|
|
912
|
+
const settle = await strategy.settle({
|
|
913
|
+
request,
|
|
914
|
+
response: scope.response,
|
|
915
|
+
payment: verifyOutcome.payment,
|
|
916
|
+
token: verifyOutcome.token,
|
|
917
|
+
routeEntry,
|
|
918
|
+
deps
|
|
919
|
+
});
|
|
920
|
+
if (!settle.ok) {
|
|
921
|
+
if (onSettleError) await onSettleError(settle.error, settle.failMessage);
|
|
922
|
+
return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
|
|
923
|
+
}
|
|
924
|
+
await grantEntitlementIfSiwx(ctx, verifyOutcome.wallet);
|
|
925
|
+
firePluginHook(deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
|
|
926
|
+
protocol: strategy.protocol,
|
|
927
|
+
payer: verifyOutcome.wallet,
|
|
928
|
+
transaction: settle.settledPayment.transaction ?? "",
|
|
929
|
+
network: settle.settledPayment.network
|
|
930
|
+
});
|
|
931
|
+
await runAfterSettle(ctx, {
|
|
932
|
+
...scope,
|
|
933
|
+
payment: settle.settledPayment,
|
|
934
|
+
response: settle.response
|
|
935
|
+
});
|
|
936
|
+
return finalize(ctx, settle.response, rawResult, body);
|
|
958
937
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
return { encoded, result };
|
|
938
|
+
|
|
939
|
+
// src/auth/normalize-wallet.ts
|
|
940
|
+
function normalizeWalletAddress(address) {
|
|
941
|
+
return /^0x/i.test(address) ? address.toLowerCase() : address;
|
|
964
942
|
}
|
|
965
943
|
|
|
966
944
|
// src/auth/siwx.ts
|
|
@@ -984,7 +962,7 @@ function categorizeValidationError(error) {
|
|
|
984
962
|
}
|
|
985
963
|
async function verifySIWX(request, _routeEntry, nonceStore) {
|
|
986
964
|
const { parseSIWxHeader, validateSIWxMessage, verifySIWxSignature } = await import("@x402/extensions/sign-in-with-x");
|
|
987
|
-
const header = request.headers.get(
|
|
965
|
+
const header = request.headers.get(HEADERS.SIWX);
|
|
988
966
|
if (!header) {
|
|
989
967
|
return { valid: false, wallet: null, code: "siwx_missing_header" };
|
|
990
968
|
}
|
|
@@ -1013,25 +991,52 @@ async function buildSIWXExtension() {
|
|
|
1013
991
|
return declareSIWxExtension();
|
|
1014
992
|
}
|
|
1015
993
|
|
|
1016
|
-
// src/
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
const
|
|
1021
|
-
if (
|
|
1022
|
-
|
|
994
|
+
// src/pipeline/context/try-siwx-fast-path.ts
|
|
995
|
+
async function trySiwxFastPath(ctx, account) {
|
|
996
|
+
const { request, routeEntry, deps } = ctx;
|
|
997
|
+
if (!routeEntry.siwxEnabled) return null;
|
|
998
|
+
const siwxHeader = request.headers.get(HEADERS.SIWX);
|
|
999
|
+
if (!siwxHeader) return null;
|
|
1000
|
+
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
1001
|
+
if (!siwx.valid) return null;
|
|
1002
|
+
const wallet = normalizeWalletAddress(siwx.wallet);
|
|
1003
|
+
ctx.pluginCtx.setVerifiedWallet(wallet);
|
|
1004
|
+
const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
|
|
1005
|
+
if (!entitled) return null;
|
|
1006
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
1007
|
+
authMode: "siwx",
|
|
1008
|
+
wallet,
|
|
1009
|
+
route: routeEntry.key
|
|
1010
|
+
});
|
|
1011
|
+
return runHandlerOnly(ctx, wallet, account);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/pipeline/context/should-parse-body-early.ts
|
|
1015
|
+
function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
|
|
1016
|
+
if (incomingStrategy) return false;
|
|
1017
|
+
if (!routeEntry.bodySchema) return false;
|
|
1018
|
+
return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/pipeline/context/protocol-init-error.ts
|
|
1022
|
+
function protocolInitError(routeEntry, deps) {
|
|
1023
|
+
if (!routeEntry.pricing) return null;
|
|
1024
|
+
const errors = [];
|
|
1025
|
+
for (const protocol of routeEntry.protocols) {
|
|
1026
|
+
if (protocol === "x402" && deps.x402InitError) {
|
|
1027
|
+
errors.push(`x402: ${deps.x402InitError}`);
|
|
1028
|
+
}
|
|
1029
|
+
if (protocol === "mpp" && deps.mppInitError) {
|
|
1030
|
+
errors.push(`mpp: ${deps.mppInitError}`);
|
|
1031
|
+
}
|
|
1023
1032
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
const didParts = rawSource.split(":");
|
|
1027
|
-
const lastPart = didParts[didParts.length - 1];
|
|
1028
|
-
const wallet = normalizeWalletAddress((0, import_viem.isAddress)(lastPart) ? (0, import_viem.getAddress)(lastPart) : rawSource);
|
|
1029
|
-
return { valid: true, wallet, withReceipt: result.withReceipt };
|
|
1033
|
+
if (errors.length === 0) return null;
|
|
1034
|
+
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
1030
1035
|
}
|
|
1031
1036
|
|
|
1032
1037
|
// src/auth/api-key.ts
|
|
1033
1038
|
async function verifyApiKey(request, resolver) {
|
|
1034
|
-
const apiKey = request.headers.get(
|
|
1039
|
+
const apiKey = request.headers.get(HEADERS.API_KEY) ?? extractBearerToken(request.headers.get(HEADERS.AUTHORIZATION));
|
|
1035
1040
|
if (!apiKey) {
|
|
1036
1041
|
return { valid: false, account: null };
|
|
1037
1042
|
}
|
|
@@ -1043,939 +1048,1311 @@ async function verifyApiKey(request, resolver) {
|
|
|
1043
1048
|
}
|
|
1044
1049
|
function extractBearerToken(header) {
|
|
1045
1050
|
if (!header) return null;
|
|
1046
|
-
if (header.startsWith(
|
|
1051
|
+
if (header.startsWith(AUTH_SCHEME.BEARER)) return header.slice(AUTH_SCHEME.BEARER.length);
|
|
1047
1052
|
return null;
|
|
1048
1053
|
}
|
|
1049
1054
|
|
|
1050
|
-
// src/
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1055
|
+
// src/pipeline/flows/api-key-only.ts
|
|
1056
|
+
async function runApiKeyOnlyFlow(ctx) {
|
|
1057
|
+
if (!ctx.routeEntry.apiKeyResolver) {
|
|
1058
|
+
return fail(ctx, 401, "API key resolver not configured");
|
|
1059
|
+
}
|
|
1060
|
+
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
1061
|
+
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
1062
|
+
firePluginHook(ctx.deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
1063
|
+
authMode: "apiKey",
|
|
1064
|
+
wallet: null,
|
|
1065
|
+
route: ctx.routeEntry.key,
|
|
1066
|
+
account: result.account
|
|
1067
|
+
});
|
|
1068
|
+
return runHandlerOnly(ctx, null, result.account);
|
|
1062
1069
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
+
|
|
1071
|
+
// src/pricing/dynamic.ts
|
|
1072
|
+
var DynamicPricing = class {
|
|
1073
|
+
constructor(opts) {
|
|
1074
|
+
this.opts = opts;
|
|
1075
|
+
}
|
|
1076
|
+
needsBody = true;
|
|
1077
|
+
async quote(body) {
|
|
1078
|
+
try {
|
|
1079
|
+
const raw = await this.opts.fn(body);
|
|
1080
|
+
return this.cap(raw, body);
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
1083
|
+
error: err instanceof Error ? err.stack : String(err),
|
|
1084
|
+
body
|
|
1085
|
+
});
|
|
1086
|
+
if (this.opts.maxPrice) {
|
|
1087
|
+
this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
|
|
1088
|
+
return this.opts.maxPrice;
|
|
1089
|
+
}
|
|
1090
|
+
throw err;
|
|
1070
1091
|
}
|
|
1071
1092
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1093
|
+
challengeQuote(body) {
|
|
1094
|
+
if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
|
|
1095
|
+
return this.quote(body);
|
|
1074
1096
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
body: parsedBody,
|
|
1081
|
-
query: parseQuery(request, routeEntry),
|
|
1082
|
-
request,
|
|
1083
|
-
requestId: meta.requestId,
|
|
1084
|
-
route: routeEntry.key,
|
|
1085
|
-
wallet,
|
|
1086
|
-
payment,
|
|
1087
|
-
account,
|
|
1088
|
-
alert(level, message, alertMeta) {
|
|
1089
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1090
|
-
level,
|
|
1091
|
-
message,
|
|
1092
|
-
route: routeEntry.key,
|
|
1093
|
-
meta: alertMeta
|
|
1094
|
-
});
|
|
1095
|
-
},
|
|
1096
|
-
setVerifiedWallet: (addr) => pluginCtx.setVerifiedWallet(addr)
|
|
1097
|
+
describe() {
|
|
1098
|
+
return {
|
|
1099
|
+
mode: "dynamic",
|
|
1100
|
+
min: this.opts.minPrice ?? "0",
|
|
1101
|
+
max: this.opts.maxPrice ?? "0"
|
|
1097
1102
|
};
|
|
1098
|
-
let rawResult;
|
|
1099
|
-
const response = await safeCallHandler(async (c) => {
|
|
1100
|
-
rawResult = await handler(c);
|
|
1101
|
-
return rawResult;
|
|
1102
|
-
}, ctx);
|
|
1103
|
-
return { response, rawResult };
|
|
1104
|
-
}
|
|
1105
|
-
function finalize(response, rawResult, meta, pluginCtx, requestBody) {
|
|
1106
|
-
fireProviderQuota(routeEntry, response, rawResult, deps, pluginCtx);
|
|
1107
|
-
firePluginResponse(deps, pluginCtx, meta, response, requestBody, rawResult);
|
|
1108
|
-
}
|
|
1109
|
-
function fail(status, message, meta, pluginCtx, requestBody) {
|
|
1110
|
-
const response = import_server2.NextResponse.json({ success: false, error: message }, { status });
|
|
1111
|
-
firePluginResponse(deps, pluginCtx, meta, response, requestBody);
|
|
1112
|
-
return response;
|
|
1113
1103
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
}
|
|
1124
|
-
if (routeEntry.validateFn) {
|
|
1125
|
-
try {
|
|
1126
|
-
await routeEntry.validateFn(body2.data);
|
|
1127
|
-
} catch (err) {
|
|
1128
|
-
const status = err.status ?? 400;
|
|
1129
|
-
const message = err instanceof Error ? err.message : "Validation failed";
|
|
1130
|
-
return fail(status, message, meta, pluginCtx, body2.data);
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
const { response, rawResult } = await invoke(
|
|
1134
|
-
request,
|
|
1135
|
-
meta,
|
|
1136
|
-
pluginCtx,
|
|
1137
|
-
wallet,
|
|
1138
|
-
account2,
|
|
1139
|
-
body2.data,
|
|
1140
|
-
null
|
|
1141
|
-
);
|
|
1142
|
-
finalize(response, rawResult, meta, pluginCtx, body2.data);
|
|
1143
|
-
return response;
|
|
1144
|
-
}
|
|
1145
|
-
if (routeEntry.authMode === "unprotected") {
|
|
1146
|
-
return handleAuth(null, void 0);
|
|
1147
|
-
}
|
|
1148
|
-
let account;
|
|
1149
|
-
if (routeEntry.authMode === "apiKey" || routeEntry.apiKeyResolver) {
|
|
1150
|
-
if (!routeEntry.apiKeyResolver) {
|
|
1151
|
-
return fail(401, "API key resolver not configured", meta, pluginCtx);
|
|
1152
|
-
}
|
|
1153
|
-
const keyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
|
|
1154
|
-
if (!keyResult.valid) {
|
|
1155
|
-
return fail(401, "Invalid or missing API key", meta, pluginCtx);
|
|
1156
|
-
}
|
|
1157
|
-
account = keyResult.account;
|
|
1158
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1159
|
-
authMode: "apiKey",
|
|
1160
|
-
wallet: null,
|
|
1161
|
-
route: routeEntry.key,
|
|
1162
|
-
account
|
|
1104
|
+
cap(raw, body) {
|
|
1105
|
+
if (!this.opts.maxPrice) return raw;
|
|
1106
|
+
const n = parseFloat(raw);
|
|
1107
|
+
const max = parseFloat(this.opts.maxPrice);
|
|
1108
|
+
if (!Number.isFinite(n) || n > max) {
|
|
1109
|
+
this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
|
|
1110
|
+
calculated: raw,
|
|
1111
|
+
maxPrice: this.opts.maxPrice,
|
|
1112
|
+
body
|
|
1163
1113
|
});
|
|
1164
|
-
|
|
1165
|
-
return handleAuth(null, account);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
const protocol = detectProtocol(request);
|
|
1169
|
-
let earlyBodyData;
|
|
1170
|
-
const pricingNeedsBody = routeEntry.pricing != null && typeof routeEntry.pricing !== "string";
|
|
1171
|
-
const needsEarlyParse = !protocol && routeEntry.bodySchema && (pricingNeedsBody || routeEntry.validateFn);
|
|
1172
|
-
if (needsEarlyParse) {
|
|
1173
|
-
const requestForPricing = request.clone();
|
|
1174
|
-
const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
|
|
1175
|
-
if (earlyBodyResult.ok) {
|
|
1176
|
-
earlyBodyData = earlyBodyResult.data;
|
|
1177
|
-
if (routeEntry.validateFn) {
|
|
1178
|
-
try {
|
|
1179
|
-
await routeEntry.validateFn(earlyBodyData);
|
|
1180
|
-
} catch (err) {
|
|
1181
|
-
const status = err.status ?? 400;
|
|
1182
|
-
const message = err instanceof Error ? err.message : "Validation failed";
|
|
1183
|
-
return fail(status, message, meta, pluginCtx, earlyBodyData);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
} else {
|
|
1187
|
-
firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
|
|
1188
|
-
return earlyBodyResult.response;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
if (routeEntry.authMode === "siwx" || routeEntry.siwxEnabled) {
|
|
1192
|
-
if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get("SIGN-IN-WITH-X")) {
|
|
1193
|
-
const requestForValidation = request.clone();
|
|
1194
|
-
const earlyBodyResult = await parseBody(requestForValidation, routeEntry);
|
|
1195
|
-
if (earlyBodyResult.ok) {
|
|
1196
|
-
try {
|
|
1197
|
-
await routeEntry.validateFn(earlyBodyResult.data);
|
|
1198
|
-
} catch (err) {
|
|
1199
|
-
const status = err.status ?? 400;
|
|
1200
|
-
const message = err instanceof Error ? err.message : "Validation failed";
|
|
1201
|
-
return fail(status, message, meta, pluginCtx, earlyBodyResult.data);
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
const siwxHeader = request.headers.get("SIGN-IN-WITH-X");
|
|
1206
|
-
if (!siwxHeader && protocol === "mpp" && routeEntry.authMode === "siwx" && deps.mppx) {
|
|
1207
|
-
let mppSiwxResult;
|
|
1208
|
-
try {
|
|
1209
|
-
mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
|
|
1210
|
-
} catch (err) {
|
|
1211
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1212
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1213
|
-
level: "critical",
|
|
1214
|
-
message: `MPP SIWX verification failed: ${message}`,
|
|
1215
|
-
route: routeEntry.key
|
|
1216
|
-
});
|
|
1217
|
-
return fail(500, `MPP SIWX verification failed: ${message}`, meta, pluginCtx);
|
|
1218
|
-
}
|
|
1219
|
-
if (mppSiwxResult.valid) {
|
|
1220
|
-
pluginCtx.setVerifiedWallet(mppSiwxResult.wallet);
|
|
1221
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1222
|
-
authMode: "siwx",
|
|
1223
|
-
wallet: mppSiwxResult.wallet,
|
|
1224
|
-
route: routeEntry.key
|
|
1225
|
-
});
|
|
1226
|
-
const authResponse = await handleAuth(mppSiwxResult.wallet, void 0);
|
|
1227
|
-
if (authResponse.status < 400) {
|
|
1228
|
-
return mppSiwxResult.withReceipt(authResponse);
|
|
1229
|
-
}
|
|
1230
|
-
return authResponse;
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
if (!siwxHeader && routeEntry.authMode === "siwx") {
|
|
1234
|
-
const url = new URL(request.url);
|
|
1235
|
-
const nonce = crypto.randomUUID().replace(/-/g, "");
|
|
1236
|
-
const supportedChains = getSupportedChains(deps.x402Accepts, deps.network);
|
|
1237
|
-
const primaryChain = supportedChains[0];
|
|
1238
|
-
const siwxInfo = {
|
|
1239
|
-
domain: url.hostname,
|
|
1240
|
-
uri: request.url,
|
|
1241
|
-
version: "1",
|
|
1242
|
-
chainId: primaryChain.chainId,
|
|
1243
|
-
type: primaryChain.type,
|
|
1244
|
-
nonce,
|
|
1245
|
-
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1246
|
-
expirationTime: new Date(Date.now() + SIWX_CHALLENGE_EXPIRY_MS).toISOString(),
|
|
1247
|
-
statement: "Sign in to verify your wallet identity"
|
|
1248
|
-
};
|
|
1249
|
-
let siwxSchema;
|
|
1250
|
-
try {
|
|
1251
|
-
siwxSchema = await buildSIWXExtension();
|
|
1252
|
-
} catch {
|
|
1253
|
-
}
|
|
1254
|
-
const paymentRequired = {
|
|
1255
|
-
x402Version: 2,
|
|
1256
|
-
error: "SIWX authentication required",
|
|
1257
|
-
resource: {
|
|
1258
|
-
url: request.url,
|
|
1259
|
-
description: routeEntry.description ?? "SIWX-protected endpoint",
|
|
1260
|
-
mimeType: "application/json"
|
|
1261
|
-
},
|
|
1262
|
-
accepts: [],
|
|
1263
|
-
extensions: {
|
|
1264
|
-
"sign-in-with-x": {
|
|
1265
|
-
info: siwxInfo,
|
|
1266
|
-
// supportedChains at top level required by MCP tools for chain detection
|
|
1267
|
-
supportedChains,
|
|
1268
|
-
...siwxSchema ? { schema: siwxSchema } : {}
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
};
|
|
1272
|
-
let encoded;
|
|
1273
|
-
try {
|
|
1274
|
-
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
1275
|
-
encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1276
|
-
} catch (err) {
|
|
1277
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1278
|
-
level: "warn",
|
|
1279
|
-
message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1280
|
-
route: routeEntry.key
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
const response = new import_server2.NextResponse(JSON.stringify(paymentRequired), {
|
|
1284
|
-
status: 402,
|
|
1285
|
-
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
1286
|
-
});
|
|
1287
|
-
if (encoded) response.headers.set("PAYMENT-REQUIRED", encoded);
|
|
1288
|
-
if (deps.mppx) {
|
|
1289
|
-
try {
|
|
1290
|
-
const mppChallenge = await deps.mppx.charge({ amount: "0" })(request);
|
|
1291
|
-
if (mppChallenge.status === 402) {
|
|
1292
|
-
const wwwAuth = mppChallenge.challenge.headers.get("WWW-Authenticate");
|
|
1293
|
-
if (wwwAuth) response.headers.set("WWW-Authenticate", wwwAuth);
|
|
1294
|
-
}
|
|
1295
|
-
} catch {
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
firePluginResponse(deps, pluginCtx, meta, response);
|
|
1299
|
-
return response;
|
|
1300
|
-
}
|
|
1301
|
-
if (siwxHeader) {
|
|
1302
|
-
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
1303
|
-
if (!siwx.valid) {
|
|
1304
|
-
if (routeEntry.authMode === "siwx") {
|
|
1305
|
-
const response = import_server2.NextResponse.json(
|
|
1306
|
-
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
1307
|
-
{ status: 402 }
|
|
1308
|
-
);
|
|
1309
|
-
firePluginResponse(deps, pluginCtx, meta, response);
|
|
1310
|
-
return response;
|
|
1311
|
-
}
|
|
1312
|
-
} else {
|
|
1313
|
-
const wallet = normalizeWalletAddress(siwx.wallet);
|
|
1314
|
-
pluginCtx.setVerifiedWallet(wallet);
|
|
1315
|
-
if (routeEntry.authMode === "siwx") {
|
|
1316
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1317
|
-
authMode: "siwx",
|
|
1318
|
-
wallet,
|
|
1319
|
-
route: routeEntry.key
|
|
1320
|
-
});
|
|
1321
|
-
return handleAuth(wallet, void 0);
|
|
1322
|
-
}
|
|
1323
|
-
if (routeEntry.siwxEnabled && routeEntry.pricing) {
|
|
1324
|
-
const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
|
|
1325
|
-
if (entitled) {
|
|
1326
|
-
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
1327
|
-
authMode: "siwx",
|
|
1328
|
-
wallet,
|
|
1329
|
-
route: routeEntry.key
|
|
1330
|
-
});
|
|
1331
|
-
return handleAuth(wallet, account);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
if (!protocol || protocol === "siwx") {
|
|
1338
|
-
if (routeEntry.pricing) {
|
|
1339
|
-
const initErrors = routeEntry.protocols.map((p) => {
|
|
1340
|
-
if (p === "x402" && deps.x402InitError) return `x402: ${deps.x402InitError}`;
|
|
1341
|
-
if (p === "mpp" && deps.mppInitError) return `mpp: ${deps.mppInitError}`;
|
|
1342
|
-
return null;
|
|
1343
|
-
}).filter(Boolean);
|
|
1344
|
-
if (initErrors.length > 0) {
|
|
1345
|
-
return fail(
|
|
1346
|
-
500,
|
|
1347
|
-
`Payment protocol initialization failed. ${initErrors.join("; ")}`,
|
|
1348
|
-
meta,
|
|
1349
|
-
pluginCtx
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
1354
|
-
}
|
|
1355
|
-
const body = await parseBody(request, routeEntry);
|
|
1356
|
-
if (!body.ok) {
|
|
1357
|
-
firePluginResponse(deps, pluginCtx, meta, body.response);
|
|
1358
|
-
return body.response;
|
|
1114
|
+
return this.opts.maxPrice;
|
|
1359
1115
|
}
|
|
1360
|
-
|
|
1116
|
+
return raw;
|
|
1117
|
+
}
|
|
1118
|
+
alert(level, message, meta) {
|
|
1119
|
+
this.opts.alert?.(level, message, meta);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
function msg(err) {
|
|
1123
|
+
return err instanceof Error ? err.message : String(err);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/pricing/fixed.ts
|
|
1127
|
+
var FixedPricing = class {
|
|
1128
|
+
constructor(price) {
|
|
1129
|
+
this.price = price;
|
|
1130
|
+
}
|
|
1131
|
+
needsBody = false;
|
|
1132
|
+
quote() {
|
|
1133
|
+
return Promise.resolve(this.price);
|
|
1134
|
+
}
|
|
1135
|
+
challengeQuote() {
|
|
1136
|
+
return Promise.resolve(this.price);
|
|
1137
|
+
}
|
|
1138
|
+
describe() {
|
|
1139
|
+
return { mode: "fixed", amount: this.price };
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
// src/pricing/tiered.ts
|
|
1144
|
+
var TieredPricing = class {
|
|
1145
|
+
constructor(opts) {
|
|
1146
|
+
this.opts = opts;
|
|
1147
|
+
}
|
|
1148
|
+
needsBody = true;
|
|
1149
|
+
async quote(body) {
|
|
1150
|
+
const { field, tiers, default: defaultTier } = this.opts;
|
|
1151
|
+
const tierKey = body != null ? String(body[field] ?? "") : "";
|
|
1152
|
+
if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
|
|
1153
|
+
if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
|
|
1154
|
+
if (!tierKey) {
|
|
1155
|
+
throw httpError(400, `Missing required field '${field}' for tier pricing`);
|
|
1156
|
+
}
|
|
1157
|
+
throw httpError(
|
|
1158
|
+
400,
|
|
1159
|
+
`Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
challengeQuote(body) {
|
|
1163
|
+
if (body !== void 0) {
|
|
1361
1164
|
try {
|
|
1362
|
-
|
|
1363
|
-
} catch
|
|
1364
|
-
const status = err.status ?? 400;
|
|
1365
|
-
const message = err instanceof Error ? err.message : "Validation failed";
|
|
1366
|
-
return fail(status, message, meta, pluginCtx, body.data);
|
|
1165
|
+
return this.quote(body);
|
|
1166
|
+
} catch {
|
|
1367
1167
|
}
|
|
1368
1168
|
}
|
|
1369
|
-
|
|
1169
|
+
return Promise.resolve(this.maxTierPrice());
|
|
1170
|
+
}
|
|
1171
|
+
describe() {
|
|
1172
|
+
return {
|
|
1173
|
+
mode: "tiered",
|
|
1174
|
+
tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
|
|
1175
|
+
key,
|
|
1176
|
+
price: tier.price,
|
|
1177
|
+
...tier.label !== void 0 ? { label: tier.label } : {}
|
|
1178
|
+
})),
|
|
1179
|
+
...this.opts.default !== void 0 ? { default: this.opts.default } : {}
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
maxTierPrice() {
|
|
1183
|
+
let max = "0";
|
|
1184
|
+
for (const tier of Object.values(this.opts.tiers)) {
|
|
1185
|
+
if (parseFloat(tier.price) > parseFloat(max)) max = tier.price;
|
|
1186
|
+
}
|
|
1187
|
+
return max;
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
function httpError(status, message) {
|
|
1191
|
+
return Object.assign(new Error(message), { status });
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/pricing/index.ts
|
|
1195
|
+
function selectPricing(raw, deps = {}) {
|
|
1196
|
+
if (raw == null) return null;
|
|
1197
|
+
if (typeof raw === "string") {
|
|
1198
|
+
return new FixedPricing(raw);
|
|
1199
|
+
}
|
|
1200
|
+
if (typeof raw === "function") {
|
|
1201
|
+
return new DynamicPricing({
|
|
1202
|
+
fn: raw,
|
|
1203
|
+
maxPrice: deps.maxPrice,
|
|
1204
|
+
minPrice: deps.minPrice,
|
|
1205
|
+
route: deps.route,
|
|
1206
|
+
alert: deps.alert
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
if (typeof raw === "object" && "tiers" in raw) {
|
|
1210
|
+
return new TieredPricing({
|
|
1211
|
+
field: raw.field,
|
|
1212
|
+
tiers: raw.tiers,
|
|
1213
|
+
default: raw.default,
|
|
1214
|
+
maxPrice: deps.maxPrice,
|
|
1215
|
+
minPrice: deps.minPrice
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/protocols/mpp/credential.ts
|
|
1222
|
+
var import_mppx = require("mppx");
|
|
1223
|
+
var import_viem = require("viem");
|
|
1224
|
+
function readMppCredential(request) {
|
|
1225
|
+
const credential = import_mppx.Credential.fromRequest(request);
|
|
1226
|
+
if (!credential) return null;
|
|
1227
|
+
const wallet = walletFromDid(credential.source ?? "");
|
|
1228
|
+
const rawType = credential.payload?.type;
|
|
1229
|
+
const payloadType = rawType === "transaction" ? "transaction" : rawType === "hash" ? "hash" : "unknown";
|
|
1230
|
+
return { credential, wallet, payloadType };
|
|
1231
|
+
}
|
|
1232
|
+
function walletFromDid(rawSource) {
|
|
1233
|
+
const parts = rawSource.split(":");
|
|
1234
|
+
const last = parts[parts.length - 1];
|
|
1235
|
+
return normalizeWalletAddress((0, import_viem.isAddress)(last) ? (0, import_viem.getAddress)(last) : rawSource);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/protocols/mpp/transaction-mode.ts
|
|
1239
|
+
var import_tempo = require("viem/tempo");
|
|
1240
|
+
var import_actions = require("viem/actions");
|
|
1241
|
+
|
|
1242
|
+
// src/protocols/mpp/receipt.ts
|
|
1243
|
+
var import_mppx2 = require("mppx");
|
|
1244
|
+
function extractTxHash(receiptHeader) {
|
|
1245
|
+
if (!receiptHeader) return "";
|
|
1246
|
+
try {
|
|
1247
|
+
return import_mppx2.Receipt.deserialize(receiptHeader).reference;
|
|
1248
|
+
} catch {
|
|
1249
|
+
return "";
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
async function readChallengeReason(challenge) {
|
|
1253
|
+
try {
|
|
1254
|
+
const text = await challenge.clone().text();
|
|
1255
|
+
if (!text) return "";
|
|
1256
|
+
const problem = JSON.parse(text);
|
|
1257
|
+
return problem.detail || problem.title || "";
|
|
1258
|
+
} catch {
|
|
1259
|
+
return "";
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/protocols/mpp/transaction-mode.ts
|
|
1264
|
+
async function verifyTxMode(args, info) {
|
|
1265
|
+
const { deps, price, routeEntry } = args;
|
|
1266
|
+
if (!deps.tempoClient) {
|
|
1267
|
+
return {
|
|
1268
|
+
ok: false,
|
|
1269
|
+
kind: "config",
|
|
1270
|
+
message: "tempoClient not configured for MPP transaction-payload mode"
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
try {
|
|
1274
|
+
const serializedTx = info.credential.payload.signature;
|
|
1275
|
+
const transaction = import_tempo.Transaction.deserialize(serializedTx);
|
|
1276
|
+
await (0, import_actions.call)(deps.tempoClient, {
|
|
1277
|
+
...transaction,
|
|
1278
|
+
account: transaction.from,
|
|
1279
|
+
calls: transaction.calls ?? []
|
|
1280
|
+
});
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1283
|
+
console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
|
|
1284
|
+
return { ok: false, kind: "invalid" };
|
|
1285
|
+
}
|
|
1286
|
+
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1287
|
+
const payment = {
|
|
1288
|
+
protocol: "mpp",
|
|
1289
|
+
status: "verified",
|
|
1290
|
+
payer: info.wallet,
|
|
1291
|
+
amount: price,
|
|
1292
|
+
network: "tempo:4217",
|
|
1293
|
+
...mppRecipient ? { recipient: mppRecipient } : {}
|
|
1294
|
+
};
|
|
1295
|
+
return {
|
|
1296
|
+
ok: true,
|
|
1297
|
+
wallet: info.wallet,
|
|
1298
|
+
payment,
|
|
1299
|
+
token: { mode: "transaction", credential: info.credential },
|
|
1300
|
+
alreadySettled: false
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
async function settleTxMode(args) {
|
|
1304
|
+
const { request, response, payment, deps, routeEntry } = args;
|
|
1305
|
+
if (!deps.mppx) {
|
|
1306
|
+
return {
|
|
1307
|
+
ok: false,
|
|
1308
|
+
error: new Error("mppx unavailable"),
|
|
1309
|
+
failMessage: "MPP not initialized",
|
|
1310
|
+
failStatus: 500
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
let result;
|
|
1314
|
+
try {
|
|
1315
|
+
result = await deps.mppx.charge({ amount: payment.amount })(request);
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1318
|
+
console.error(`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`);
|
|
1319
|
+
return {
|
|
1320
|
+
ok: false,
|
|
1321
|
+
error: err,
|
|
1322
|
+
failMessage: `MPP payment processing failed: ${message}`,
|
|
1323
|
+
failStatus: 500
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
if (result.status === 402) {
|
|
1327
|
+
const reason = await readChallengeReason(result.challenge);
|
|
1328
|
+
const detail = reason || "transaction reverted on-chain after handler execution";
|
|
1329
|
+
const settlementError = Object.assign(new Error(detail), {
|
|
1330
|
+
status: 402,
|
|
1331
|
+
detail,
|
|
1332
|
+
mppResult: result,
|
|
1333
|
+
challenge: result.challenge
|
|
1334
|
+
});
|
|
1335
|
+
console.error(`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`);
|
|
1336
|
+
return {
|
|
1337
|
+
ok: false,
|
|
1338
|
+
error: settlementError,
|
|
1339
|
+
failMessage: `MPP payment failed: ${detail}`,
|
|
1340
|
+
failStatus: 500
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
const receiptResponse = result.withReceipt(response);
|
|
1344
|
+
receiptResponse.headers.set("Cache-Control", "private");
|
|
1345
|
+
const receiptHeader = receiptResponse.headers.get(HEADERS.MPP_PAYMENT_RECEIPT) ?? void 0;
|
|
1346
|
+
const txHash = extractTxHash(receiptHeader);
|
|
1347
|
+
const settledPayment = {
|
|
1348
|
+
...payment,
|
|
1349
|
+
status: "settled",
|
|
1350
|
+
...txHash ? { transaction: txHash } : {},
|
|
1351
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1352
|
+
};
|
|
1353
|
+
return { ok: true, response: receiptResponse, settledPayment };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/protocols/mpp/hash-mode.ts
|
|
1357
|
+
async function verifyHashMode(args, info) {
|
|
1358
|
+
const { deps, price, routeEntry, request } = args;
|
|
1359
|
+
if (!deps.mppx) {
|
|
1360
|
+
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";
|
|
1361
|
+
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
1362
|
+
return { ok: false, kind: "config", message: reason };
|
|
1363
|
+
}
|
|
1364
|
+
let chargeResult;
|
|
1365
|
+
try {
|
|
1366
|
+
chargeResult = await deps.mppx.charge({ amount: price })(request);
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1369
|
+
console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
|
|
1370
|
+
return { ok: false, kind: "config", message: `MPP payment processing failed: ${message}` };
|
|
1371
|
+
}
|
|
1372
|
+
if (chargeResult.status === 402) {
|
|
1373
|
+
const reason = await readChallengeReason(chargeResult.challenge);
|
|
1374
|
+
const detail = reason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
|
|
1375
|
+
console.warn(`[router] ${routeEntry.key}: MPP credential rejected \u2014 ${detail}`);
|
|
1376
|
+
return { ok: false, kind: "invalid" };
|
|
1377
|
+
}
|
|
1378
|
+
const receiptHeader = chargeResult.withReceipt(new Response()).headers.get(
|
|
1379
|
+
HEADERS.MPP_PAYMENT_RECEIPT
|
|
1380
|
+
);
|
|
1381
|
+
const txHash = extractTxHash(receiptHeader);
|
|
1382
|
+
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1383
|
+
const payment = {
|
|
1384
|
+
protocol: "mpp",
|
|
1385
|
+
status: "settled",
|
|
1386
|
+
payer: info.wallet,
|
|
1387
|
+
amount: price,
|
|
1388
|
+
network: "tempo:4217",
|
|
1389
|
+
...mppRecipient ? { recipient: mppRecipient } : {},
|
|
1390
|
+
...txHash ? { transaction: txHash } : {},
|
|
1391
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1392
|
+
};
|
|
1393
|
+
return {
|
|
1394
|
+
ok: true,
|
|
1395
|
+
wallet: info.wallet,
|
|
1396
|
+
payment,
|
|
1397
|
+
token: { mode: "hash", charge: chargeResult },
|
|
1398
|
+
alreadySettled: true
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function settleHashMode(args) {
|
|
1402
|
+
const { response, payment, token } = args;
|
|
1403
|
+
const hashToken = token;
|
|
1404
|
+
const receiptResponse = hashToken.charge.withReceipt(response);
|
|
1405
|
+
receiptResponse.headers.set("Cache-Control", "private");
|
|
1406
|
+
return {
|
|
1407
|
+
ok: true,
|
|
1408
|
+
response: receiptResponse,
|
|
1409
|
+
settledPayment: payment
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/protocols/mpp/strategy.ts
|
|
1414
|
+
var mppStrategy = {
|
|
1415
|
+
protocol: "mpp",
|
|
1416
|
+
detects(request) {
|
|
1417
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
1418
|
+
return Boolean(auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT));
|
|
1419
|
+
},
|
|
1420
|
+
async verify(args) {
|
|
1421
|
+
const info = readMppCredential(args.request);
|
|
1422
|
+
if (!info) return { ok: false, kind: "invalid" };
|
|
1423
|
+
if (info.payloadType === "transaction" && args.deps.tempoClient) {
|
|
1424
|
+
return verifyTxMode(args, info);
|
|
1425
|
+
}
|
|
1426
|
+
return verifyHashMode(args, info);
|
|
1427
|
+
},
|
|
1428
|
+
async settle(args) {
|
|
1429
|
+
const token = args.token;
|
|
1430
|
+
if (token.mode === "transaction") return settleTxMode(args);
|
|
1431
|
+
return settleHashMode(args);
|
|
1432
|
+
},
|
|
1433
|
+
async buildChallenge(args) {
|
|
1434
|
+
if (!args.deps.mppx) return {};
|
|
1370
1435
|
try {
|
|
1371
|
-
|
|
1436
|
+
const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
|
|
1437
|
+
if (result.status === 402) {
|
|
1438
|
+
const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
1439
|
+
if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
|
|
1440
|
+
}
|
|
1372
1441
|
} catch (err) {
|
|
1373
|
-
return fail(
|
|
1374
|
-
err.status ?? 500,
|
|
1375
|
-
err instanceof Error ? err.message : "Price resolution failed",
|
|
1376
|
-
meta,
|
|
1377
|
-
pluginCtx,
|
|
1378
|
-
body.data
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
if (!routeEntry.protocols.includes(protocol)) {
|
|
1382
|
-
const accepted = routeEntry.protocols.join(", ") || "none";
|
|
1383
1442
|
console.warn(
|
|
1384
|
-
`[router]
|
|
1385
|
-
);
|
|
1386
|
-
return fail(
|
|
1387
|
-
400,
|
|
1388
|
-
`This route does not accept ${protocol} payments. Accepted protocols: ${accepted}`,
|
|
1389
|
-
meta,
|
|
1390
|
-
pluginCtx,
|
|
1391
|
-
body.data
|
|
1443
|
+
`[router] MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1392
1444
|
);
|
|
1445
|
+
throw err;
|
|
1393
1446
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
...matchedRecipient ? { recipient: matchedRecipient } : {}
|
|
1426
|
-
};
|
|
1427
|
-
pluginCtx.setVerifiedWallet(wallet);
|
|
1428
|
-
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1429
|
-
protocol: "x402",
|
|
1430
|
-
payer: wallet,
|
|
1431
|
-
amount: price,
|
|
1432
|
-
network: matchedNetwork
|
|
1433
|
-
});
|
|
1434
|
-
const { response, rawResult } = await invoke(
|
|
1435
|
-
request,
|
|
1436
|
-
meta,
|
|
1437
|
-
pluginCtx,
|
|
1438
|
-
wallet,
|
|
1439
|
-
account,
|
|
1440
|
-
body.data,
|
|
1441
|
-
payment
|
|
1447
|
+
return {};
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
// src/protocols/x402/strategy.ts
|
|
1452
|
+
init_accepts();
|
|
1453
|
+
|
|
1454
|
+
// src/protocols/x402/challenge.ts
|
|
1455
|
+
init_facilitators();
|
|
1456
|
+
init_solana();
|
|
1457
|
+
|
|
1458
|
+
// src/protocols/x402/requirements.ts
|
|
1459
|
+
init_evm();
|
|
1460
|
+
init_solana();
|
|
1461
|
+
async function buildExpectedRequirements(server, request, price, accepts) {
|
|
1462
|
+
const exactRequirements = await buildExactRequirements(server, request, price, accepts);
|
|
1463
|
+
const customRequirements = buildCustomRequirements(price, accepts);
|
|
1464
|
+
return [...exactRequirements, ...customRequirements];
|
|
1465
|
+
}
|
|
1466
|
+
async function buildExactRequirements(server, request, price, accepts) {
|
|
1467
|
+
const exactGroups = [
|
|
1468
|
+
buildEvmExactOptions(accepts, price),
|
|
1469
|
+
buildSolanaExactOptions(accepts, price)
|
|
1470
|
+
].filter((options) => options.length > 0);
|
|
1471
|
+
if (exactGroups.length === 0) return [];
|
|
1472
|
+
const requirements = [];
|
|
1473
|
+
const failures = [];
|
|
1474
|
+
for (const options of exactGroups) {
|
|
1475
|
+
try {
|
|
1476
|
+
requirements.push(
|
|
1477
|
+
...await server.buildPaymentRequirementsFromOptions(options, { request })
|
|
1442
1478
|
);
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
verifyRequirements
|
|
1449
|
-
);
|
|
1450
|
-
if (!settle.result?.success) {
|
|
1451
|
-
const reason = settle.result?.errorReason || "x402 settlement returned success=false";
|
|
1452
|
-
const error = new Error(reason);
|
|
1453
|
-
error.errorReason = reason;
|
|
1454
|
-
throw error;
|
|
1455
|
-
}
|
|
1456
|
-
if (routeEntry.siwxEnabled) {
|
|
1457
|
-
try {
|
|
1458
|
-
await deps.entitlementStore.grant(routeEntry.key, wallet);
|
|
1459
|
-
} catch (error) {
|
|
1460
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1461
|
-
level: "warn",
|
|
1462
|
-
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1463
|
-
route: routeEntry.key
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
response.headers.set("PAYMENT-RESPONSE", settle.encoded);
|
|
1468
|
-
response.headers.set("Cache-Control", "private");
|
|
1469
|
-
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1470
|
-
protocol: "x402",
|
|
1471
|
-
payer: verify.payer,
|
|
1472
|
-
transaction: String(settle.result?.transaction ?? ""),
|
|
1473
|
-
network: matchedNetwork
|
|
1474
|
-
});
|
|
1475
|
-
} catch (err) {
|
|
1476
|
-
const errObj = err;
|
|
1477
|
-
console.error("Settlement failed", {
|
|
1478
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1479
|
-
route: routeEntry.key,
|
|
1480
|
-
network: matchedNetwork,
|
|
1481
|
-
errorReason: errObj.errorReason,
|
|
1482
|
-
facilitatorStatus: errObj.response?.status,
|
|
1483
|
-
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
1484
|
-
});
|
|
1485
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1486
|
-
level: "critical",
|
|
1487
|
-
message: `Settlement failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1488
|
-
route: routeEntry.key
|
|
1489
|
-
});
|
|
1490
|
-
return fail(500, "Settlement failed", meta, pluginCtx, body.data);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
finalize(response, rawResult, meta, pluginCtx, body.data);
|
|
1494
|
-
return response;
|
|
1495
|
-
}
|
|
1496
|
-
if (protocol === "mpp") {
|
|
1497
|
-
if (!deps.mppx) {
|
|
1498
|
-
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";
|
|
1499
|
-
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
1500
|
-
return fail(500, reason, meta, pluginCtx, body.data);
|
|
1501
|
-
}
|
|
1502
|
-
const mppCredential = import_mppx2.Credential.fromRequest(request);
|
|
1503
|
-
const rawSource = mppCredential?.source ?? "";
|
|
1504
|
-
const didParts = rawSource.split(":");
|
|
1505
|
-
const lastPart = didParts[didParts.length - 1];
|
|
1506
|
-
const wallet = normalizeWalletAddress((0, import_viem2.isAddress)(lastPart) ? (0, import_viem2.getAddress)(lastPart) : rawSource);
|
|
1507
|
-
const payloadType = mppCredential?.payload?.type;
|
|
1508
|
-
if (payloadType === "transaction" && deps.tempoClient) {
|
|
1509
|
-
try {
|
|
1510
|
-
const serializedTx = mppCredential.payload.signature;
|
|
1511
|
-
const transaction = import_tempo.Transaction.deserialize(serializedTx);
|
|
1512
|
-
await (0, import_actions.call)(deps.tempoClient, {
|
|
1513
|
-
...transaction,
|
|
1514
|
-
account: transaction.from,
|
|
1515
|
-
calls: transaction.calls ?? []
|
|
1516
|
-
});
|
|
1517
|
-
} catch (err) {
|
|
1518
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1519
|
-
console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
|
|
1520
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1521
|
-
level: "warn",
|
|
1522
|
-
message: `MPP simulation failed: ${message}`,
|
|
1523
|
-
route: routeEntry.key
|
|
1524
|
-
});
|
|
1525
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1526
|
-
}
|
|
1527
|
-
pluginCtx.setVerifiedWallet(wallet);
|
|
1528
|
-
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1529
|
-
protocol: "mpp",
|
|
1530
|
-
payer: wallet,
|
|
1531
|
-
amount: price,
|
|
1532
|
-
network: "tempo:4217"
|
|
1533
|
-
});
|
|
1534
|
-
const { response: response2, rawResult: rawResult2 } = await invoke(
|
|
1535
|
-
request,
|
|
1536
|
-
meta,
|
|
1537
|
-
pluginCtx,
|
|
1538
|
-
wallet,
|
|
1539
|
-
account,
|
|
1540
|
-
body.data,
|
|
1541
|
-
{
|
|
1542
|
-
protocol: "mpp",
|
|
1543
|
-
status: "verified",
|
|
1544
|
-
payer: wallet,
|
|
1545
|
-
amount: price,
|
|
1546
|
-
network: "tempo:4217",
|
|
1547
|
-
recipient: deps.payeeAddress
|
|
1548
|
-
}
|
|
1549
|
-
);
|
|
1550
|
-
if (response2.status < 400) {
|
|
1551
|
-
let mppResult2;
|
|
1552
|
-
try {
|
|
1553
|
-
mppResult2 = await deps.mppx.charge({ amount: price })(request);
|
|
1554
|
-
} catch (err) {
|
|
1555
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1556
|
-
console.error(
|
|
1557
|
-
`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`
|
|
1558
|
-
);
|
|
1559
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1560
|
-
level: "critical",
|
|
1561
|
-
message: `MPP broadcast failed after handler: ${message}`,
|
|
1562
|
-
route: routeEntry.key
|
|
1563
|
-
});
|
|
1564
|
-
return fail(
|
|
1565
|
-
500,
|
|
1566
|
-
`MPP payment processing failed: ${message}`,
|
|
1567
|
-
meta,
|
|
1568
|
-
pluginCtx,
|
|
1569
|
-
body.data
|
|
1570
|
-
);
|
|
1571
|
-
}
|
|
1572
|
-
if (mppResult2.status === 402) {
|
|
1573
|
-
let rejectReason = "";
|
|
1574
|
-
try {
|
|
1575
|
-
const problemBody = await mppResult2.challenge.clone().text();
|
|
1576
|
-
if (problemBody) {
|
|
1577
|
-
const problem = JSON.parse(problemBody);
|
|
1578
|
-
rejectReason = problem.detail || problem.title || "";
|
|
1579
|
-
}
|
|
1580
|
-
} catch {
|
|
1581
|
-
}
|
|
1582
|
-
const detail = rejectReason || "transaction reverted on-chain after handler execution";
|
|
1583
|
-
console.error(
|
|
1584
|
-
`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`
|
|
1585
|
-
);
|
|
1586
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1587
|
-
level: "critical",
|
|
1588
|
-
message: `MPP payment failed after handler: ${detail}`,
|
|
1589
|
-
route: routeEntry.key
|
|
1590
|
-
});
|
|
1591
|
-
return fail(500, `MPP payment failed: ${detail}`, meta, pluginCtx, body.data);
|
|
1592
|
-
}
|
|
1593
|
-
const receiptResponse = mppResult2.withReceipt(response2);
|
|
1594
|
-
receiptResponse.headers.set("Cache-Control", "private");
|
|
1595
|
-
let txHash2 = "";
|
|
1596
|
-
const receiptHeader2 = receiptResponse.headers.get("Payment-Receipt");
|
|
1597
|
-
if (receiptHeader2) {
|
|
1598
|
-
try {
|
|
1599
|
-
txHash2 = import_mppx2.Receipt.deserialize(receiptHeader2).reference;
|
|
1600
|
-
} catch {
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
if (routeEntry.siwxEnabled) {
|
|
1604
|
-
try {
|
|
1605
|
-
await deps.entitlementStore.grant(routeEntry.key, wallet);
|
|
1606
|
-
} catch (error) {
|
|
1607
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1608
|
-
level: "warn",
|
|
1609
|
-
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1610
|
-
route: routeEntry.key
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1615
|
-
protocol: "mpp",
|
|
1616
|
-
payer: wallet,
|
|
1617
|
-
transaction: txHash2,
|
|
1618
|
-
network: "tempo:4217"
|
|
1619
|
-
});
|
|
1620
|
-
finalize(receiptResponse, rawResult2, meta, pluginCtx, body.data);
|
|
1621
|
-
return receiptResponse;
|
|
1622
|
-
}
|
|
1623
|
-
finalize(response2, rawResult2, meta, pluginCtx, body.data);
|
|
1624
|
-
return response2;
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1481
|
+
failures.push(err);
|
|
1482
|
+
if (exactGroups.length === 1) {
|
|
1483
|
+
throw err;
|
|
1625
1484
|
}
|
|
1626
|
-
|
|
1485
|
+
console.warn(
|
|
1486
|
+
`[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (requirements.length > 0) {
|
|
1491
|
+
return requirements;
|
|
1492
|
+
}
|
|
1493
|
+
throw failures[0] ?? new Error("Failed to build x402 exact requirements");
|
|
1494
|
+
}
|
|
1495
|
+
function buildCustomRequirements(price, accepts) {
|
|
1496
|
+
return accepts.filter((accept) => accept.scheme !== "exact").map((accept) => buildCustomRequirement(price, accept));
|
|
1497
|
+
}
|
|
1498
|
+
function buildCustomRequirement(price, accept) {
|
|
1499
|
+
if (!accept.asset) {
|
|
1500
|
+
throw new Error(
|
|
1501
|
+
`Custom x402 accept '${accept.scheme}' on '${accept.network}' is missing asset`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
scheme: accept.scheme,
|
|
1506
|
+
network: accept.network,
|
|
1507
|
+
amount: decimalToAtomicUnits(price, accept.decimals ?? 6),
|
|
1508
|
+
asset: accept.asset,
|
|
1509
|
+
payTo: accept.payTo,
|
|
1510
|
+
maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
|
|
1511
|
+
extra: accept.extra ?? {}
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
function decimalToAtomicUnits(amount, decimals) {
|
|
1515
|
+
const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
|
|
1516
|
+
if (!match?.groups) {
|
|
1517
|
+
throw new Error(`Invalid decimal amount '${amount}'`);
|
|
1518
|
+
}
|
|
1519
|
+
const whole = match.groups.whole;
|
|
1520
|
+
const fraction = match.groups.fraction ?? "";
|
|
1521
|
+
if (fraction.length > decimals) {
|
|
1522
|
+
throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
|
|
1523
|
+
}
|
|
1524
|
+
const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
|
|
1525
|
+
return normalized === "" ? "0" : normalized;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// src/protocols/x402/challenge.ts
|
|
1529
|
+
async function buildX402Challenge(opts) {
|
|
1530
|
+
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
|
|
1531
|
+
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
1532
|
+
const resource = buildChallengeResource(request, routeEntry);
|
|
1533
|
+
const requirements = await buildChallengeRequirements(
|
|
1534
|
+
server,
|
|
1535
|
+
request,
|
|
1536
|
+
price,
|
|
1537
|
+
accepts,
|
|
1538
|
+
resource,
|
|
1539
|
+
facilitatorsByNetwork
|
|
1540
|
+
);
|
|
1541
|
+
const paymentRequired = await server.createPaymentRequiredResponse(
|
|
1542
|
+
requirements,
|
|
1543
|
+
resource,
|
|
1544
|
+
void 0,
|
|
1545
|
+
extensions
|
|
1546
|
+
);
|
|
1547
|
+
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1548
|
+
return { encoded, requirements };
|
|
1549
|
+
}
|
|
1550
|
+
async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
|
|
1551
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1552
|
+
if (!needsFacilitatorEnrichment(accepts)) return requirements;
|
|
1553
|
+
return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
|
|
1554
|
+
}
|
|
1555
|
+
function needsFacilitatorEnrichment(accepts) {
|
|
1556
|
+
return accepts.some((accept) => accept.scheme !== "exact") || hasSolanaAccepts(accepts);
|
|
1557
|
+
}
|
|
1558
|
+
async function enrichGroup(group, resource) {
|
|
1559
|
+
const accepted = await enrichRequirementsWithFacilitatorAccepts(
|
|
1560
|
+
group.facilitator,
|
|
1561
|
+
resource,
|
|
1562
|
+
group.items.map(({ requirement }) => requirement)
|
|
1563
|
+
);
|
|
1564
|
+
if (accepted.length !== group.items.length) {
|
|
1565
|
+
throw new Error(
|
|
1566
|
+
`Facilitator /accepts returned ${accepted.length} requirements for ${group.items.length} inputs on ${group.facilitator.url ?? group.facilitator.network}`
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
return accepted;
|
|
1570
|
+
}
|
|
1571
|
+
async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
|
|
1572
|
+
const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
|
|
1573
|
+
if (groups.length === 0) return requirements;
|
|
1574
|
+
const results = await Promise.all(
|
|
1575
|
+
groups.map(async (group) => {
|
|
1627
1576
|
try {
|
|
1628
|
-
|
|
1577
|
+
return { success: true, group, accepted: await enrichGroup(group, resource) };
|
|
1629
1578
|
} catch (err) {
|
|
1630
|
-
const
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
});
|
|
1637
|
-
return fail(500, `MPP payment processing failed: ${message}`, meta, pluginCtx, body.data);
|
|
1579
|
+
const label = group.facilitator.url ?? group.facilitator.network;
|
|
1580
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1581
|
+
console.warn(
|
|
1582
|
+
`[router] ${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
|
|
1583
|
+
);
|
|
1584
|
+
return { success: false, group };
|
|
1638
1585
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1586
|
+
})
|
|
1587
|
+
);
|
|
1588
|
+
const enriched = [...requirements];
|
|
1589
|
+
results.filter((r) => r.success).forEach(({ group, accepted }) => {
|
|
1590
|
+
accepted.forEach((req, offset) => {
|
|
1591
|
+
const index = group.items[offset]?.index;
|
|
1592
|
+
if (index !== void 0) enriched[index] = req;
|
|
1593
|
+
});
|
|
1594
|
+
});
|
|
1595
|
+
const failedIndices = new Set(
|
|
1596
|
+
results.filter((r) => !r.success).flatMap(({ group }) => group.items.map(({ index }) => index))
|
|
1597
|
+
);
|
|
1598
|
+
const remaining = enriched.filter((_, i) => !failedIndices.has(i));
|
|
1599
|
+
if (remaining.length === 0) {
|
|
1600
|
+
throw new Error(
|
|
1601
|
+
"All facilitator enrichments failed; no payment requirements remain for challenge"
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
return remaining;
|
|
1605
|
+
}
|
|
1606
|
+
function collectEnrichmentGroups(requirements, facilitatorsByNetwork) {
|
|
1607
|
+
const groups = [];
|
|
1608
|
+
requirements.forEach((requirement, index) => {
|
|
1609
|
+
if (!requiresFacilitatorEnrichment(requirement)) return;
|
|
1610
|
+
const facilitator = getRequiredFacilitator(requirement, facilitatorsByNetwork);
|
|
1611
|
+
const existing = groups.find(
|
|
1612
|
+
(group) => sameResolvedX402Facilitator(group.facilitator, facilitator)
|
|
1613
|
+
);
|
|
1614
|
+
if (existing) {
|
|
1615
|
+
existing.items.push({ index, requirement });
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
groups.push({
|
|
1619
|
+
facilitator,
|
|
1620
|
+
items: [{ index, requirement }]
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
return groups;
|
|
1624
|
+
}
|
|
1625
|
+
function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
|
|
1626
|
+
const facilitator = getFacilitatorForRequirement(facilitatorsByNetwork, requirement);
|
|
1627
|
+
if (!facilitator) {
|
|
1628
|
+
throw new Error(
|
|
1629
|
+
`Missing x402 facilitator for ${requirement.scheme} requirement on ${requirement.network}`
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
return facilitator;
|
|
1633
|
+
}
|
|
1634
|
+
function requiresFacilitatorEnrichment(requirement) {
|
|
1635
|
+
return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
|
|
1636
|
+
}
|
|
1637
|
+
function buildChallengeResource(request, routeEntry) {
|
|
1638
|
+
return {
|
|
1639
|
+
url: request.url,
|
|
1640
|
+
method: routeEntry.method,
|
|
1641
|
+
description: routeEntry.description,
|
|
1642
|
+
mimeType: "application/json"
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/protocols/x402/settle.ts
|
|
1647
|
+
async function settleX402Payment(server, payload, requirements) {
|
|
1648
|
+
const { encodePaymentResponseHeader } = await import("@x402/core/http");
|
|
1649
|
+
const result = await server.settlePayment(payload, requirements);
|
|
1650
|
+
const encoded = encodePaymentResponseHeader(result);
|
|
1651
|
+
return { encoded, result };
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/protocols/x402/verify.ts
|
|
1655
|
+
async function verifyX402Payment(opts) {
|
|
1656
|
+
const { server, request, price, accepts } = opts;
|
|
1657
|
+
const payload = await readPaymentPayload(request);
|
|
1658
|
+
if (!payload) return null;
|
|
1659
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1660
|
+
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
1661
|
+
if (!matching) {
|
|
1662
|
+
return invalidPaymentVerification();
|
|
1663
|
+
}
|
|
1664
|
+
let verify;
|
|
1665
|
+
try {
|
|
1666
|
+
verify = await server.verifyPayment(payload, matching);
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
const sc = err.statusCode;
|
|
1669
|
+
if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
|
|
1670
|
+
throw err;
|
|
1671
|
+
}
|
|
1672
|
+
if (!verify.isValid) return invalidPaymentVerification();
|
|
1673
|
+
if (typeof verify.payer !== "string" || verify.payer.length === 0) {
|
|
1674
|
+
throw new Error("x402 verification succeeded without a payer address");
|
|
1675
|
+
}
|
|
1676
|
+
return {
|
|
1677
|
+
valid: true,
|
|
1678
|
+
payer: verify.payer,
|
|
1679
|
+
payload,
|
|
1680
|
+
requirements: matching
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function findVerifiableRequirements(server, requirements, payload) {
|
|
1684
|
+
const strictMatch = server.findMatchingRequirements(requirements, payload);
|
|
1685
|
+
if (strictMatch) {
|
|
1686
|
+
return payload.x402Version === 2 ? payload.accepted : strictMatch;
|
|
1687
|
+
}
|
|
1688
|
+
if (payload.x402Version !== 2) {
|
|
1689
|
+
return null;
|
|
1690
|
+
}
|
|
1691
|
+
const stableMatch = requirements.find(
|
|
1692
|
+
(requirement) => matchesStableFields(requirement, payload.accepted)
|
|
1693
|
+
);
|
|
1694
|
+
return stableMatch ? payload.accepted : null;
|
|
1695
|
+
}
|
|
1696
|
+
function matchesStableFields(requirement, accepted) {
|
|
1697
|
+
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;
|
|
1698
|
+
}
|
|
1699
|
+
async function readPaymentPayload(request) {
|
|
1700
|
+
const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
|
|
1701
|
+
if (!paymentHeader) return null;
|
|
1702
|
+
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
1703
|
+
return decodePaymentSignatureHeader(paymentHeader);
|
|
1704
|
+
}
|
|
1705
|
+
function invalidPaymentVerification() {
|
|
1706
|
+
return { valid: false, payload: null, requirements: null, payer: null };
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// src/protocols/x402/strategy.ts
|
|
1710
|
+
var x402Strategy = {
|
|
1711
|
+
protocol: "x402",
|
|
1712
|
+
detects(request) {
|
|
1713
|
+
return Boolean(
|
|
1714
|
+
request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)
|
|
1715
|
+
);
|
|
1716
|
+
},
|
|
1717
|
+
async verify(args) {
|
|
1718
|
+
const { request, body, price, routeEntry, deps } = args;
|
|
1719
|
+
if (!deps.x402Server) {
|
|
1720
|
+
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";
|
|
1721
|
+
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
1722
|
+
return { ok: false, kind: "config", message: reason };
|
|
1723
|
+
}
|
|
1724
|
+
const accepts = await resolveX402Accepts(
|
|
1725
|
+
request,
|
|
1726
|
+
routeEntry,
|
|
1727
|
+
deps.x402Accepts,
|
|
1728
|
+
deps.payeeAddress,
|
|
1729
|
+
body
|
|
1730
|
+
);
|
|
1731
|
+
const verifyResult = await verifyX402Payment({
|
|
1732
|
+
server: deps.x402Server,
|
|
1733
|
+
request,
|
|
1734
|
+
price,
|
|
1735
|
+
accepts
|
|
1736
|
+
});
|
|
1737
|
+
if (!verifyResult?.valid) return { ok: false, kind: "invalid" };
|
|
1738
|
+
const wallet = normalizeWalletAddress(verifyResult.payer);
|
|
1739
|
+
const matchedNetwork = getRequirementNetwork(verifyResult.requirements, deps.network);
|
|
1740
|
+
const matchedRecipient = getRequirementRecipient(verifyResult.requirements);
|
|
1741
|
+
const payment = {
|
|
1742
|
+
protocol: "x402",
|
|
1743
|
+
status: "verified",
|
|
1744
|
+
payer: wallet,
|
|
1745
|
+
amount: price,
|
|
1746
|
+
network: matchedNetwork,
|
|
1747
|
+
...matchedRecipient ? { recipient: matchedRecipient } : {}
|
|
1748
|
+
};
|
|
1749
|
+
return {
|
|
1750
|
+
ok: true,
|
|
1751
|
+
wallet,
|
|
1752
|
+
payment,
|
|
1753
|
+
token: {
|
|
1754
|
+
payload: verifyResult.payload,
|
|
1755
|
+
requirements: verifyResult.requirements
|
|
1657
1756
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1757
|
+
};
|
|
1758
|
+
},
|
|
1759
|
+
async settle(args) {
|
|
1760
|
+
const { response, payment, token, deps } = args;
|
|
1761
|
+
const x402Token = token;
|
|
1762
|
+
try {
|
|
1763
|
+
const settle = await settleX402Payment(
|
|
1764
|
+
deps.x402Server,
|
|
1765
|
+
x402Token.payload,
|
|
1766
|
+
x402Token.requirements
|
|
1661
1767
|
);
|
|
1662
|
-
if (
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1768
|
+
if (!settle.result?.success) {
|
|
1769
|
+
const reason = settle.result?.errorReason || "x402 settlement returned success=false";
|
|
1770
|
+
const error = new Error(reason);
|
|
1771
|
+
error.errorReason = reason;
|
|
1772
|
+
throw error;
|
|
1667
1773
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1774
|
+
response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
|
|
1775
|
+
response.headers.set("Cache-Control", "private");
|
|
1776
|
+
const transaction = String(settle.result?.transaction ?? "");
|
|
1777
|
+
const settledPayment = {
|
|
1778
|
+
...payment,
|
|
1779
|
+
status: "settled",
|
|
1780
|
+
...transaction ? { transaction } : {}
|
|
1781
|
+
};
|
|
1782
|
+
return { ok: true, response, settledPayment };
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
const errObj = err;
|
|
1785
|
+
console.error("Settlement failed", {
|
|
1786
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1787
|
+
route: args.routeEntry.key,
|
|
1788
|
+
network: payment.network,
|
|
1789
|
+
errorReason: errObj.errorReason,
|
|
1790
|
+
facilitatorStatus: errObj.response?.status,
|
|
1791
|
+
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
1674
1792
|
});
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1793
|
+
return { ok: false, error: err, failMessage: "Settlement failed" };
|
|
1794
|
+
}
|
|
1795
|
+
},
|
|
1796
|
+
async buildChallenge(args) {
|
|
1797
|
+
const { request, routeEntry, body, price, extensions, deps } = args;
|
|
1798
|
+
if (!deps.x402Server) return {};
|
|
1799
|
+
const accepts = await resolveX402Accepts(
|
|
1800
|
+
request,
|
|
1801
|
+
routeEntry,
|
|
1802
|
+
deps.x402Accepts,
|
|
1803
|
+
deps.payeeAddress,
|
|
1804
|
+
body
|
|
1805
|
+
);
|
|
1806
|
+
const { encoded } = await buildX402Challenge({
|
|
1807
|
+
server: deps.x402Server,
|
|
1808
|
+
routeEntry,
|
|
1809
|
+
request,
|
|
1810
|
+
price,
|
|
1811
|
+
accepts,
|
|
1812
|
+
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
1813
|
+
extensions
|
|
1814
|
+
});
|
|
1815
|
+
return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
function getRequirementNetwork(requirements, fallback) {
|
|
1819
|
+
const network = requirements?.network;
|
|
1820
|
+
return typeof network === "string" ? network : fallback;
|
|
1821
|
+
}
|
|
1822
|
+
function getRequirementRecipient(requirements) {
|
|
1823
|
+
const payTo = requirements?.payTo;
|
|
1824
|
+
return typeof payTo === "string" ? payTo : void 0;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/protocols/detect.ts
|
|
1828
|
+
function detectProtocol(request) {
|
|
1829
|
+
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
1830
|
+
return "x402";
|
|
1831
|
+
}
|
|
1832
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
1833
|
+
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
1834
|
+
return "mpp";
|
|
1835
|
+
}
|
|
1836
|
+
if (request.headers.get(HEADERS.SIWX)) {
|
|
1837
|
+
return "siwx";
|
|
1838
|
+
}
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/protocols/index.ts
|
|
1843
|
+
var STRATEGIES = {
|
|
1844
|
+
x402: x402Strategy,
|
|
1845
|
+
mpp: mppStrategy
|
|
1846
|
+
};
|
|
1847
|
+
function selectIncomingStrategy(request, allowed) {
|
|
1848
|
+
for (const name of allowed) {
|
|
1849
|
+
const strategy = STRATEGIES[name];
|
|
1850
|
+
if (strategy.detects(request)) return strategy;
|
|
1851
|
+
}
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
function getAllowedStrategies(allowed) {
|
|
1855
|
+
return allowed.map((name) => STRATEGIES[name]);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// src/pipeline/challenge.ts
|
|
1859
|
+
var import_server4 = require("next/server");
|
|
1860
|
+
async function build402(ctx, pricing, body) {
|
|
1861
|
+
let challengePrice;
|
|
1862
|
+
try {
|
|
1863
|
+
challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
|
|
1864
|
+
} catch (err) {
|
|
1865
|
+
const message = errorMessage(err, "Price calculation failed");
|
|
1866
|
+
const errorResponse = import_server4.NextResponse.json(
|
|
1867
|
+
{ success: false, error: message },
|
|
1868
|
+
{ status: errorStatus(err, 500) }
|
|
1869
|
+
);
|
|
1870
|
+
firePluginResponse(ctx, errorResponse);
|
|
1871
|
+
return errorResponse;
|
|
1872
|
+
}
|
|
1873
|
+
const extensions = await buildChallengeExtensions(ctx);
|
|
1874
|
+
const response = new import_server4.NextResponse(null, {
|
|
1875
|
+
status: 402,
|
|
1876
|
+
headers: {
|
|
1877
|
+
"Content-Type": "application/json",
|
|
1878
|
+
"Cache-Control": "no-store"
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
|
|
1882
|
+
try {
|
|
1883
|
+
const contribution = await strategy.buildChallenge({
|
|
1884
|
+
request: ctx.request,
|
|
1885
|
+
routeEntry: ctx.routeEntry,
|
|
1886
|
+
body,
|
|
1887
|
+
price: challengePrice,
|
|
1888
|
+
extensions,
|
|
1889
|
+
deps: ctx.deps
|
|
1890
|
+
});
|
|
1891
|
+
if (contribution.headers) {
|
|
1892
|
+
for (const [name, value] of Object.entries(contribution.headers)) {
|
|
1893
|
+
response.headers.set(name, value);
|
|
1704
1894
|
}
|
|
1705
|
-
const receiptResponse = mppResult.withReceipt(response);
|
|
1706
|
-
receiptResponse.headers.set("Cache-Control", "private");
|
|
1707
|
-
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1708
|
-
protocol: "mpp",
|
|
1709
|
-
payer: wallet,
|
|
1710
|
-
transaction: txHash,
|
|
1711
|
-
network: "tempo:4217"
|
|
1712
|
-
});
|
|
1713
|
-
finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
|
|
1714
|
-
return receiptResponse;
|
|
1715
1895
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
|
|
1898
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
1899
|
+
level: "critical",
|
|
1900
|
+
message,
|
|
1901
|
+
route: ctx.routeEntry.key
|
|
1902
|
+
});
|
|
1903
|
+
if (strategy.protocol === "x402") {
|
|
1904
|
+
const errorResponse = import_server4.NextResponse.json(
|
|
1905
|
+
{ success: false, error: message },
|
|
1906
|
+
{ status: 500 }
|
|
1907
|
+
);
|
|
1908
|
+
firePluginResponse(ctx, errorResponse);
|
|
1909
|
+
return errorResponse;
|
|
1910
|
+
}
|
|
1718
1911
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1912
|
+
}
|
|
1913
|
+
firePluginResponse(ctx, response);
|
|
1914
|
+
return response;
|
|
1721
1915
|
}
|
|
1722
|
-
async function
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
)
|
|
1916
|
+
async function buildChallengeExtensions(ctx) {
|
|
1917
|
+
const { routeEntry } = ctx;
|
|
1918
|
+
let extensions;
|
|
1919
|
+
try {
|
|
1920
|
+
const { z } = await import("zod");
|
|
1921
|
+
const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
|
|
1922
|
+
const toJSON = (schema) => z.toJSONSchema(schema, {
|
|
1923
|
+
target: "draft-2020-12",
|
|
1924
|
+
unrepresentable: "any"
|
|
1925
|
+
});
|
|
1926
|
+
const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
|
|
1927
|
+
const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
|
|
1928
|
+
if (inputSchema) {
|
|
1929
|
+
const config = {
|
|
1930
|
+
method: routeEntry.method,
|
|
1931
|
+
bodyType: routeEntry.bodySchema ? "json" : void 0,
|
|
1932
|
+
inputSchema
|
|
1933
|
+
};
|
|
1934
|
+
if (routeEntry.inputExample !== void 0) {
|
|
1935
|
+
config.input = routeEntry.inputExample;
|
|
1936
|
+
}
|
|
1937
|
+
if (outputSchema && routeEntry.outputExample !== void 0) {
|
|
1938
|
+
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
1939
|
+
}
|
|
1940
|
+
extensions = declareDiscoveryExtension(config);
|
|
1941
|
+
}
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
1944
|
+
level: "warn",
|
|
1945
|
+
message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1946
|
+
route: routeEntry.key
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
if (routeEntry.siwxEnabled) {
|
|
1950
|
+
try {
|
|
1951
|
+
const siwxExtension = await buildSIWXExtension();
|
|
1952
|
+
if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
|
|
1953
|
+
extensions = {
|
|
1954
|
+
...extensions ?? {},
|
|
1955
|
+
...siwxExtension
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
} catch {
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
return extensions;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// src/pipeline/flows/paid.ts
|
|
1965
|
+
async function runPaidFlow(ctx) {
|
|
1966
|
+
const { request, routeEntry, deps } = ctx;
|
|
1967
|
+
let account = void 0;
|
|
1968
|
+
if (routeEntry.apiKeyResolver) {
|
|
1969
|
+
const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
|
|
1970
|
+
if (!apiKeyResult.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
1971
|
+
account = apiKeyResult.account;
|
|
1972
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
1973
|
+
authMode: "apiKey",
|
|
1974
|
+
wallet: null,
|
|
1975
|
+
route: routeEntry.key,
|
|
1976
|
+
account
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
const alertFn = (level, message, meta) => {
|
|
1980
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
1981
|
+
level,
|
|
1982
|
+
message,
|
|
1983
|
+
route: routeEntry.key,
|
|
1984
|
+
meta
|
|
1985
|
+
});
|
|
1986
|
+
};
|
|
1987
|
+
const pricing = selectPricing(routeEntry.pricing, {
|
|
1988
|
+
alert: alertFn,
|
|
1989
|
+
maxPrice: routeEntry.maxPrice,
|
|
1990
|
+
minPrice: routeEntry.minPrice,
|
|
1991
|
+
route: routeEntry.key
|
|
1992
|
+
});
|
|
1993
|
+
const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
|
|
1994
|
+
let earlyBody = void 0;
|
|
1995
|
+
if (shouldParseBodyEarly(incomingStrategy, routeEntry, pricing)) {
|
|
1996
|
+
const earlyClone = request.clone();
|
|
1997
|
+
const earlyResult = await parseBody(earlyClone, routeEntry);
|
|
1998
|
+
if (earlyResult.ok) {
|
|
1999
|
+
earlyBody = earlyResult.data;
|
|
2000
|
+
const validateErr2 = await runValidate(ctx, earlyBody);
|
|
2001
|
+
if (validateErr2) return validateErr2;
|
|
2002
|
+
} else {
|
|
2003
|
+
firePluginResponse(ctx, earlyResult.response);
|
|
2004
|
+
return earlyResult.response;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
const siwxFastPath = await trySiwxFastPath(ctx, account);
|
|
2008
|
+
if (siwxFastPath) return siwxFastPath;
|
|
2009
|
+
if (!incomingStrategy) {
|
|
2010
|
+
const initError = protocolInitError(routeEntry, deps);
|
|
2011
|
+
if (initError) return fail(ctx, 500, initError);
|
|
2012
|
+
return build402(ctx, pricing, earlyBody);
|
|
2013
|
+
}
|
|
2014
|
+
const body = await parseBody(request, routeEntry);
|
|
2015
|
+
if (!body.ok) {
|
|
2016
|
+
firePluginResponse(ctx, body.response);
|
|
2017
|
+
return body.response;
|
|
2018
|
+
}
|
|
2019
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
2020
|
+
if (validateErr) return validateErr;
|
|
2021
|
+
if (!pricing) {
|
|
2022
|
+
return fail(ctx, 500, "Pricing not configured", body.data);
|
|
2023
|
+
}
|
|
2024
|
+
let price;
|
|
2025
|
+
try {
|
|
2026
|
+
price = await pricing.quote(body.data);
|
|
2027
|
+
} catch (err) {
|
|
2028
|
+
return fail(
|
|
2029
|
+
ctx,
|
|
2030
|
+
errorStatus(err, 500),
|
|
2031
|
+
errorMessage(err, "Price calculation failed"),
|
|
2032
|
+
body.data
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
const verifyOutcome = await incomingStrategy.verify({
|
|
2036
|
+
request,
|
|
2037
|
+
body: body.data,
|
|
2038
|
+
price,
|
|
2039
|
+
routeEntry,
|
|
2040
|
+
deps
|
|
2041
|
+
});
|
|
2042
|
+
if (verifyOutcome.ok === false) {
|
|
2043
|
+
if (verifyOutcome.kind === "config") {
|
|
2044
|
+
return fail(ctx, 500, verifyOutcome.message, body.data);
|
|
2045
|
+
}
|
|
2046
|
+
return build402(ctx, pricing, body.data);
|
|
2047
|
+
}
|
|
2048
|
+
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2049
|
+
firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
|
|
2050
|
+
protocol: incomingStrategy.protocol,
|
|
2051
|
+
payer: verifyOutcome.wallet,
|
|
2052
|
+
amount: price,
|
|
2053
|
+
network: verifyOutcome.payment.network
|
|
2054
|
+
});
|
|
2055
|
+
const result = await invoke(ctx, verifyOutcome.wallet, account, body.data, verifyOutcome.payment);
|
|
2056
|
+
const settleScope = {
|
|
2057
|
+
wallet: verifyOutcome.wallet,
|
|
2058
|
+
account,
|
|
2059
|
+
body: body.data,
|
|
2060
|
+
payment: verifyOutcome.payment,
|
|
2061
|
+
response: result.response,
|
|
2062
|
+
rawResult: result.rawResult,
|
|
2063
|
+
handlerError: result.handlerError
|
|
1733
2064
|
};
|
|
2065
|
+
if (verifyOutcome.alreadySettled) {
|
|
2066
|
+
if (result.response.status >= 400) {
|
|
2067
|
+
const settledScope = settleScope;
|
|
2068
|
+
await runSettledHandlerError(ctx, settledScope);
|
|
2069
|
+
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
2070
|
+
}
|
|
2071
|
+
return settleAndFinalize({
|
|
2072
|
+
ctx,
|
|
2073
|
+
strategy: incomingStrategy,
|
|
2074
|
+
verifyOutcome,
|
|
2075
|
+
scope: settleScope,
|
|
2076
|
+
rawResult: result.rawResult,
|
|
2077
|
+
body: body.data
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
if (result.response.status >= 400) {
|
|
2081
|
+
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
2082
|
+
}
|
|
2083
|
+
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2084
|
+
if (beforeErr) return beforeErr;
|
|
2085
|
+
return settleAndFinalize({
|
|
2086
|
+
ctx,
|
|
2087
|
+
strategy: incomingStrategy,
|
|
2088
|
+
verifyOutcome,
|
|
2089
|
+
scope: settleScope,
|
|
2090
|
+
rawResult: result.rawResult,
|
|
2091
|
+
body: body.data,
|
|
2092
|
+
onSettleError: async (error, failMessage) => {
|
|
2093
|
+
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2094
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2095
|
+
level: "critical",
|
|
2096
|
+
message: `${incomingStrategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
|
|
2097
|
+
route: routeEntry.key
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/pipeline/flows/siwx-only.ts
|
|
2104
|
+
var import_server5 = require("next/server");
|
|
2105
|
+
|
|
2106
|
+
// src/auth/nonce.ts
|
|
2107
|
+
var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
|
|
2108
|
+
var MemoryNonceStore = class {
|
|
2109
|
+
seen = /* @__PURE__ */ new Map();
|
|
2110
|
+
async check(nonce) {
|
|
2111
|
+
this.evict();
|
|
2112
|
+
if (this.seen.has(nonce)) return false;
|
|
2113
|
+
this.seen.set(nonce, Date.now() + SIWX_CHALLENGE_EXPIRY_MS);
|
|
2114
|
+
return true;
|
|
2115
|
+
}
|
|
2116
|
+
evict() {
|
|
2117
|
+
const now = Date.now();
|
|
2118
|
+
for (const [n, exp] of this.seen) {
|
|
2119
|
+
if (exp < now) this.seen.delete(n);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
function detectRedisClientType(client) {
|
|
2124
|
+
if (!client || typeof client !== "object") {
|
|
2125
|
+
throw new Error(
|
|
2126
|
+
"createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
if ("options" in client && "status" in client) {
|
|
2130
|
+
return "ioredis";
|
|
2131
|
+
}
|
|
2132
|
+
const constructor = client.constructor?.name;
|
|
2133
|
+
if (constructor === "Redis" && "url" in client) {
|
|
2134
|
+
return "upstash";
|
|
2135
|
+
}
|
|
2136
|
+
if (typeof client.set === "function") {
|
|
2137
|
+
return "upstash";
|
|
2138
|
+
}
|
|
2139
|
+
throw new Error(
|
|
2140
|
+
"Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
|
|
2141
|
+
);
|
|
1734
2142
|
}
|
|
1735
|
-
function
|
|
2143
|
+
function createRedisNonceStore(client, opts) {
|
|
2144
|
+
const prefix = opts?.prefix ?? "siwx:nonce:";
|
|
2145
|
+
const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
2146
|
+
const clientType = detectRedisClientType(client);
|
|
1736
2147
|
return {
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
2148
|
+
async check(nonce) {
|
|
2149
|
+
const key = `${prefix}${nonce}`;
|
|
2150
|
+
if (clientType === "upstash") {
|
|
2151
|
+
const redis = client;
|
|
2152
|
+
const result = await redis.set(key, "1", { ex: ttlSeconds, nx: true });
|
|
2153
|
+
return result !== null;
|
|
2154
|
+
}
|
|
2155
|
+
if (clientType === "ioredis") {
|
|
2156
|
+
const redis = client;
|
|
2157
|
+
const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
|
|
2158
|
+
return result === "OK";
|
|
2159
|
+
}
|
|
2160
|
+
throw new Error("Unknown Redis client type");
|
|
2161
|
+
}
|
|
1748
2162
|
};
|
|
1749
2163
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
2164
|
+
|
|
2165
|
+
// src/protocols/mpp/siwx-mode.ts
|
|
2166
|
+
var import_mppx3 = require("mppx");
|
|
2167
|
+
async function verifyMppSiwx(request, mppx) {
|
|
2168
|
+
const result = await mppx.charge({ amount: "0" })(request);
|
|
2169
|
+
if (result.status === 402) {
|
|
2170
|
+
return { valid: false, challenge: result.challenge };
|
|
2171
|
+
}
|
|
2172
|
+
const credential = import_mppx3.Credential.fromRequest(request);
|
|
2173
|
+
const rawSource = credential?.source ?? "";
|
|
2174
|
+
const wallet = walletFromDid(rawSource);
|
|
2175
|
+
return { valid: true, wallet, withReceipt: result.withReceipt };
|
|
1755
2176
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
route: routeEntry.key,
|
|
1767
|
-
meta: { calculated: price, maxPrice: routeEntry.maxPrice, body: bodyData }
|
|
1768
|
-
});
|
|
1769
|
-
price = routeEntry.maxPrice;
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
return { price };
|
|
1773
|
-
} catch (err) {
|
|
1774
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1775
|
-
level: "error",
|
|
1776
|
-
message: `Pricing function failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1777
|
-
route: routeEntry.key,
|
|
1778
|
-
meta: { error: err instanceof Error ? err.stack : String(err), body: bodyData }
|
|
1779
|
-
});
|
|
1780
|
-
if (routeEntry.maxPrice) {
|
|
1781
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1782
|
-
level: "warn",
|
|
1783
|
-
message: `Using maxPrice ${routeEntry.maxPrice} as fallback after pricing error`,
|
|
1784
|
-
route: routeEntry.key
|
|
1785
|
-
});
|
|
1786
|
-
return { price: routeEntry.maxPrice };
|
|
2177
|
+
|
|
2178
|
+
// src/pipeline/flows/siwx-only.ts
|
|
2179
|
+
async function runSiwxOnlyFlow(ctx) {
|
|
2180
|
+
const { request, routeEntry, deps } = ctx;
|
|
2181
|
+
if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get(HEADERS.SIWX)) {
|
|
2182
|
+
const earlyClone = request.clone();
|
|
2183
|
+
const earlyBody = await parseBody(earlyClone, routeEntry);
|
|
2184
|
+
if (earlyBody.ok) {
|
|
2185
|
+
const validateErr = await runValidate(ctx, earlyBody.data);
|
|
2186
|
+
if (validateErr) return validateErr;
|
|
1787
2187
|
} else {
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
{ status: 500 }
|
|
1791
|
-
);
|
|
1792
|
-
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
1793
|
-
return { error: errorResponse };
|
|
2188
|
+
firePluginResponse(ctx, earlyBody.response);
|
|
2189
|
+
return earlyBody.response;
|
|
1794
2190
|
}
|
|
1795
2191
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
headers: {
|
|
1801
|
-
"Content-Type": "application/json",
|
|
1802
|
-
"Cache-Control": "no-store"
|
|
1803
|
-
}
|
|
1804
|
-
});
|
|
1805
|
-
let challengePrice;
|
|
1806
|
-
if (bodyData !== void 0 && typeof routeEntry.pricing !== "string" && routeEntry.pricing != null) {
|
|
1807
|
-
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
|
1808
|
-
if ("error" in result) return result.error;
|
|
1809
|
-
challengePrice = result.price;
|
|
1810
|
-
} else if (routeEntry.maxPrice) {
|
|
1811
|
-
challengePrice = routeEntry.maxPrice;
|
|
1812
|
-
} else if (routeEntry.pricing) {
|
|
2192
|
+
const siwxHeader = request.headers.get(HEADERS.SIWX);
|
|
2193
|
+
const protocol = detectProtocol(request);
|
|
2194
|
+
if (!siwxHeader && protocol === "mpp" && deps.mppx) {
|
|
2195
|
+
let mppSiwxResult;
|
|
1813
2196
|
try {
|
|
1814
|
-
|
|
1815
|
-
} catch {
|
|
1816
|
-
|
|
2197
|
+
mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2200
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2201
|
+
level: "critical",
|
|
2202
|
+
message: `MPP SIWX verification failed: ${message}`,
|
|
2203
|
+
route: routeEntry.key
|
|
2204
|
+
});
|
|
2205
|
+
return fail(ctx, 500, `MPP SIWX verification failed: ${message}`);
|
|
2206
|
+
}
|
|
2207
|
+
if (mppSiwxResult.valid) {
|
|
2208
|
+
ctx.pluginCtx.setVerifiedWallet(mppSiwxResult.wallet);
|
|
2209
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
2210
|
+
authMode: "siwx",
|
|
2211
|
+
wallet: mppSiwxResult.wallet,
|
|
2212
|
+
route: routeEntry.key
|
|
2213
|
+
});
|
|
2214
|
+
const authResponse = await runHandlerOnly(ctx, mppSiwxResult.wallet, void 0);
|
|
2215
|
+
if (authResponse.status < 400) {
|
|
2216
|
+
return mppSiwxResult.withReceipt(authResponse);
|
|
2217
|
+
}
|
|
2218
|
+
return authResponse;
|
|
1817
2219
|
}
|
|
1818
|
-
} else {
|
|
1819
|
-
challengePrice = "0";
|
|
1820
2220
|
}
|
|
1821
|
-
|
|
2221
|
+
if (!siwxHeader) {
|
|
2222
|
+
return buildSiwxChallenge(ctx);
|
|
2223
|
+
}
|
|
2224
|
+
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
2225
|
+
if (!siwx.valid) {
|
|
2226
|
+
const response = import_server5.NextResponse.json(
|
|
2227
|
+
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
2228
|
+
{ status: 402 }
|
|
2229
|
+
);
|
|
2230
|
+
firePluginResponse(ctx, response);
|
|
2231
|
+
return response;
|
|
2232
|
+
}
|
|
2233
|
+
const wallet = normalizeWalletAddress(siwx.wallet);
|
|
2234
|
+
ctx.pluginCtx.setVerifiedWallet(wallet);
|
|
2235
|
+
firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
2236
|
+
authMode: "siwx",
|
|
2237
|
+
wallet,
|
|
2238
|
+
route: routeEntry.key
|
|
2239
|
+
});
|
|
2240
|
+
return runHandlerOnly(ctx, wallet, void 0);
|
|
2241
|
+
}
|
|
2242
|
+
async function buildSiwxChallenge(ctx) {
|
|
2243
|
+
const { request, routeEntry, deps } = ctx;
|
|
2244
|
+
const url = new URL(request.url);
|
|
2245
|
+
const nonce = crypto.randomUUID().replace(/-/g, "");
|
|
2246
|
+
const supportedChains = getSupportedChains(deps.x402Accepts, deps.network);
|
|
2247
|
+
const primaryChain = supportedChains[0];
|
|
2248
|
+
const siwxInfo = {
|
|
2249
|
+
domain: url.hostname,
|
|
2250
|
+
uri: request.url,
|
|
2251
|
+
version: "1",
|
|
2252
|
+
chainId: primaryChain.chainId,
|
|
2253
|
+
type: primaryChain.type,
|
|
2254
|
+
nonce,
|
|
2255
|
+
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2256
|
+
expirationTime: new Date(Date.now() + SIWX_CHALLENGE_EXPIRY_MS).toISOString(),
|
|
2257
|
+
statement: "Sign in to verify your wallet identity"
|
|
2258
|
+
};
|
|
2259
|
+
let siwxSchema;
|
|
1822
2260
|
try {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
2261
|
+
siwxSchema = await buildSIWXExtension();
|
|
2262
|
+
} catch {
|
|
2263
|
+
}
|
|
2264
|
+
const paymentRequired = {
|
|
2265
|
+
x402Version: 2,
|
|
2266
|
+
error: "SIWX authentication required",
|
|
2267
|
+
resource: {
|
|
2268
|
+
url: request.url,
|
|
2269
|
+
description: routeEntry.description ?? "SIWX-protected endpoint",
|
|
2270
|
+
mimeType: "application/json"
|
|
2271
|
+
},
|
|
2272
|
+
accepts: [],
|
|
2273
|
+
extensions: {
|
|
2274
|
+
"sign-in-with-x": {
|
|
2275
|
+
info: siwxInfo,
|
|
2276
|
+
// Required by MCP tools at the top level for chain detection.
|
|
2277
|
+
supportedChains,
|
|
2278
|
+
...siwxSchema ? { schema: siwxSchema } : {}
|
|
1842
2279
|
}
|
|
1843
|
-
extensions = declareDiscoveryExtension(config);
|
|
1844
2280
|
}
|
|
2281
|
+
};
|
|
2282
|
+
let encoded;
|
|
2283
|
+
try {
|
|
2284
|
+
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
2285
|
+
encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1845
2286
|
} catch (err) {
|
|
1846
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
2287
|
+
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
1847
2288
|
level: "warn",
|
|
1848
|
-
message: `
|
|
2289
|
+
message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1849
2290
|
route: routeEntry.key
|
|
1850
2291
|
});
|
|
1851
2292
|
}
|
|
1852
|
-
|
|
2293
|
+
const response = new import_server5.NextResponse(JSON.stringify(paymentRequired), {
|
|
2294
|
+
status: 402,
|
|
2295
|
+
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
2296
|
+
});
|
|
2297
|
+
if (encoded) response.headers.set(HEADERS.X402_PAYMENT_REQUIRED, encoded);
|
|
2298
|
+
if (deps.mppx) {
|
|
1853
2299
|
try {
|
|
1854
|
-
const
|
|
1855
|
-
if (
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
...siwxExtension
|
|
1859
|
-
};
|
|
2300
|
+
const mppChallenge = await deps.mppx.charge({ amount: "0" })(request);
|
|
2301
|
+
if (mppChallenge.status === 402) {
|
|
2302
|
+
const wwwAuth = mppChallenge.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
2303
|
+
if (wwwAuth) response.headers.set(HEADERS.WWW_AUTHENTICATE, wwwAuth);
|
|
1860
2304
|
}
|
|
1861
2305
|
} catch {
|
|
1862
2306
|
}
|
|
1863
2307
|
}
|
|
1864
|
-
|
|
1865
|
-
try {
|
|
1866
|
-
const accepts = await resolveX402Accepts(
|
|
1867
|
-
request,
|
|
1868
|
-
routeEntry,
|
|
1869
|
-
deps.x402Accepts,
|
|
1870
|
-
deps.payeeAddress,
|
|
1871
|
-
bodyData
|
|
1872
|
-
);
|
|
1873
|
-
const { encoded } = await buildX402Challenge({
|
|
1874
|
-
server: deps.x402Server,
|
|
1875
|
-
routeEntry,
|
|
1876
|
-
request,
|
|
1877
|
-
price: challengePrice,
|
|
1878
|
-
accepts,
|
|
1879
|
-
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
1880
|
-
extensions
|
|
1881
|
-
});
|
|
1882
|
-
response.headers.set("PAYMENT-REQUIRED", encoded);
|
|
1883
|
-
} catch (err) {
|
|
1884
|
-
const message = `x402 challenge build failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1885
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1886
|
-
level: "critical",
|
|
1887
|
-
message,
|
|
1888
|
-
route: routeEntry.key
|
|
1889
|
-
});
|
|
1890
|
-
const errorResponse = import_server2.NextResponse.json({ success: false, error: message }, { status: 500 });
|
|
1891
|
-
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
1892
|
-
return errorResponse;
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
if (routeEntry.protocols.includes("mpp") && deps.mppx) {
|
|
1896
|
-
try {
|
|
1897
|
-
const result = await deps.mppx.charge({ amount: challengePrice })(request);
|
|
1898
|
-
if (result.status === 402) {
|
|
1899
|
-
const wwwAuth = result.challenge.headers.get("WWW-Authenticate");
|
|
1900
|
-
if (wwwAuth) response.headers.set("WWW-Authenticate", wwwAuth);
|
|
1901
|
-
}
|
|
1902
|
-
} catch (err) {
|
|
1903
|
-
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
1904
|
-
level: "critical",
|
|
1905
|
-
message: `MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1906
|
-
route: routeEntry.key
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
firePluginResponse(deps, pluginCtx, meta, response);
|
|
2308
|
+
firePluginResponse(ctx, response);
|
|
1911
2309
|
return response;
|
|
1912
2310
|
}
|
|
1913
|
-
function
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
firePluginHook(deps.plugin, "onError", pluginCtx, {
|
|
1925
|
-
status: response.status,
|
|
1926
|
-
message: response.statusText || `HTTP ${response.status}`,
|
|
1927
|
-
settled: false
|
|
1928
|
-
});
|
|
2311
|
+
function siwxSignatureType(network) {
|
|
2312
|
+
return network.startsWith("solana:") ? "ed25519" : "eip191";
|
|
2313
|
+
}
|
|
2314
|
+
function getSupportedChains(x402Accepts, fallbackNetwork) {
|
|
2315
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2316
|
+
const chains = [];
|
|
2317
|
+
for (const accept of x402Accepts) {
|
|
2318
|
+
if (accept.network && !seen.has(accept.network)) {
|
|
2319
|
+
seen.add(accept.network);
|
|
2320
|
+
chains.push({ chainId: accept.network, type: siwxSignatureType(accept.network) });
|
|
2321
|
+
}
|
|
1929
2322
|
}
|
|
2323
|
+
if (chains.length === 0) {
|
|
2324
|
+
chains.push({ chainId: fallbackNetwork, type: siwxSignatureType(fallbackNetwork) });
|
|
2325
|
+
}
|
|
2326
|
+
return chains;
|
|
1930
2327
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
return "healthy";
|
|
2328
|
+
|
|
2329
|
+
// src/pipeline/flows/unprotected.ts
|
|
2330
|
+
async function runUnprotectedFlow(ctx) {
|
|
2331
|
+
return runHandlerOnly(ctx, null, void 0);
|
|
1936
2332
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
const
|
|
1943
|
-
if (
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
remaining: quota.remaining,
|
|
1950
|
-
limit: quota.limit,
|
|
1951
|
-
spend: quota.spend,
|
|
1952
|
-
level,
|
|
1953
|
-
overage,
|
|
1954
|
-
message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
|
|
1955
|
-
};
|
|
1956
|
-
firePluginHook(deps.plugin, "onProviderQuota", pluginCtx, event);
|
|
1957
|
-
} catch {
|
|
1958
|
-
}
|
|
2333
|
+
|
|
2334
|
+
// src/orchestrate.ts
|
|
2335
|
+
function createRequestHandler(routeEntry, handler, deps) {
|
|
2336
|
+
return async (request) => {
|
|
2337
|
+
await deps.initPromise;
|
|
2338
|
+
const ctx = preflight(routeEntry, handler, deps, request);
|
|
2339
|
+
if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
|
|
2340
|
+
if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
|
|
2341
|
+
if (routeEntry.pricing) return runPaidFlow(ctx);
|
|
2342
|
+
if (routeEntry.apiKeyResolver) return runApiKeyOnlyFlow(ctx);
|
|
2343
|
+
return runUnprotectedFlow(ctx);
|
|
2344
|
+
};
|
|
1959
2345
|
}
|
|
1960
2346
|
|
|
1961
2347
|
// src/validate-examples.ts
|
|
1962
2348
|
function validateExamples(key, bodySchema, querySchema, outputSchema, inputExample, hasInputExample, outputExample, hasOutputExample) {
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
);
|
|
1967
|
-
}
|
|
1968
|
-
if (querySchema && !hasInputExample) {
|
|
1969
|
-
throw new Error(
|
|
1970
|
-
`route '${key}': .query() requires a matching .inputExample() \u2014 the bazaar discovery extension needs a conforming sample query to advertise.`
|
|
1971
|
-
);
|
|
2349
|
+
const inputSchema = bodySchema ?? querySchema;
|
|
2350
|
+
if (hasInputExample && !inputSchema) {
|
|
2351
|
+
throw new Error(`route '${key}': .inputExample() requires .body() or .query()`);
|
|
1972
2352
|
}
|
|
1973
|
-
if (
|
|
1974
|
-
throw new Error(
|
|
1975
|
-
`route '${key}': .output() requires a matching .outputExample() \u2014 the bazaar discovery extension needs a conforming sample response to advertise.`
|
|
1976
|
-
);
|
|
2353
|
+
if (hasOutputExample && !outputSchema) {
|
|
2354
|
+
throw new Error(`route '${key}': .outputExample() requires .output()`);
|
|
1977
2355
|
}
|
|
1978
|
-
const inputSchema = bodySchema ?? querySchema;
|
|
1979
2356
|
if (inputSchema && hasInputExample) {
|
|
1980
2357
|
const result = inputSchema.safeParse(inputExample);
|
|
1981
2358
|
if (!result.success) {
|
|
@@ -2049,6 +2426,8 @@ var RouteBuilder = class {
|
|
|
2049
2426
|
/** @internal */
|
|
2050
2427
|
_validateFn;
|
|
2051
2428
|
/** @internal */
|
|
2429
|
+
_settlement;
|
|
2430
|
+
/** @internal */
|
|
2052
2431
|
_mppInfo;
|
|
2053
2432
|
constructor(key, registry, deps) {
|
|
2054
2433
|
this._key = key;
|
|
@@ -2062,11 +2441,21 @@ var RouteBuilder = class {
|
|
|
2062
2441
|
return next;
|
|
2063
2442
|
}
|
|
2064
2443
|
paid(pricing, options) {
|
|
2444
|
+
if (this._authMode === "unprotected") {
|
|
2445
|
+
throw new Error(
|
|
2446
|
+
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
if (this._pricing !== void 0) {
|
|
2450
|
+
throw new Error(
|
|
2451
|
+
`route '${this._key}': Cannot call .paid() more than once on the same route.`
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2065
2454
|
const next = this.fork();
|
|
2066
2455
|
next._authMode = "paid";
|
|
2067
2456
|
next._pricing = pricing;
|
|
2068
2457
|
if (options?.protocols) {
|
|
2069
|
-
next._protocols = options.protocols;
|
|
2458
|
+
next._protocols = [...options.protocols];
|
|
2070
2459
|
} else if (next._protocols.length === 0) {
|
|
2071
2460
|
next._protocols = ["x402"];
|
|
2072
2461
|
}
|
|
@@ -2131,6 +2520,16 @@ var RouteBuilder = class {
|
|
|
2131
2520
|
return next;
|
|
2132
2521
|
}
|
|
2133
2522
|
unprotected() {
|
|
2523
|
+
if (this._authMode && this._authMode !== "unprotected") {
|
|
2524
|
+
throw new Error(
|
|
2525
|
+
`route '${this._key}': Cannot combine .unprotected() and .${this._authMode}() on the same route.`
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
if (this._pricing) {
|
|
2529
|
+
throw new Error(
|
|
2530
|
+
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2134
2533
|
const next = this.fork();
|
|
2135
2534
|
next._authMode = "unprotected";
|
|
2136
2535
|
next._protocols = [];
|
|
@@ -2145,32 +2544,43 @@ var RouteBuilder = class {
|
|
|
2145
2544
|
next._providerConfig = config ?? {};
|
|
2146
2545
|
return next;
|
|
2147
2546
|
}
|
|
2148
|
-
|
|
2149
|
-
// Schema methods
|
|
2150
|
-
// -------------------------------------------------------------------------
|
|
2151
|
-
body(schema) {
|
|
2547
|
+
body(schema, example) {
|
|
2152
2548
|
const next = this.fork();
|
|
2153
2549
|
next._bodySchema = schema;
|
|
2550
|
+
if (example !== void 0) {
|
|
2551
|
+
next._inputExample = example;
|
|
2552
|
+
next._hasInputExample = true;
|
|
2553
|
+
}
|
|
2154
2554
|
return next;
|
|
2155
2555
|
}
|
|
2156
|
-
query(schema) {
|
|
2556
|
+
query(schema, example) {
|
|
2157
2557
|
const next = this.fork();
|
|
2158
2558
|
next._querySchema = schema;
|
|
2559
|
+
if (example !== void 0) {
|
|
2560
|
+
next._inputExample = example;
|
|
2561
|
+
next._hasInputExample = true;
|
|
2562
|
+
}
|
|
2159
2563
|
next._method = "GET";
|
|
2160
2564
|
return next;
|
|
2161
2565
|
}
|
|
2162
|
-
output(schema) {
|
|
2566
|
+
output(schema, example) {
|
|
2163
2567
|
const next = this.fork();
|
|
2164
2568
|
next._outputSchema = schema;
|
|
2569
|
+
if (example !== void 0) {
|
|
2570
|
+
next._outputExample = example;
|
|
2571
|
+
next._hasOutputExample = true;
|
|
2572
|
+
}
|
|
2165
2573
|
return next;
|
|
2166
2574
|
}
|
|
2167
2575
|
/**
|
|
2168
2576
|
* Provide a conforming example of the request input (body or query params).
|
|
2169
2577
|
*
|
|
2170
|
-
*
|
|
2171
|
-
*
|
|
2172
|
-
*
|
|
2173
|
-
*
|
|
2578
|
+
* Optional. When provided, the example is validated against the request schema
|
|
2579
|
+
* at route registration and embedded in the bazaar discovery extension so
|
|
2580
|
+
* indexers can advertise a working sample call.
|
|
2581
|
+
*
|
|
2582
|
+
* For the common case, pass the example directly to `.body(schema, example)` or
|
|
2583
|
+
* `.query(schema, example)` instead.
|
|
2174
2584
|
*
|
|
2175
2585
|
* @example
|
|
2176
2586
|
* ```ts
|
|
@@ -2190,10 +2600,11 @@ var RouteBuilder = class {
|
|
|
2190
2600
|
/**
|
|
2191
2601
|
* Provide a conforming example of the response output.
|
|
2192
2602
|
*
|
|
2193
|
-
*
|
|
2194
|
-
*
|
|
2195
|
-
*
|
|
2196
|
-
*
|
|
2603
|
+
* Optional. When provided, the example is validated against the output schema
|
|
2604
|
+
* at route registration and embedded in the bazaar discovery extension so
|
|
2605
|
+
* indexers can advertise the response shape.
|
|
2606
|
+
*
|
|
2607
|
+
* For the common case, pass the example directly to `.output(schema, example)` instead.
|
|
2197
2608
|
*
|
|
2198
2609
|
* Accepts any JSON value (objects, arrays, or primitives) — top-level array
|
|
2199
2610
|
* or primitive responses (e.g. `z.array(...)`) are supported alongside the
|
|
@@ -2266,15 +2677,39 @@ var RouteBuilder = class {
|
|
|
2266
2677
|
return next;
|
|
2267
2678
|
}
|
|
2268
2679
|
// -------------------------------------------------------------------------
|
|
2680
|
+
// Settlement lifecycle
|
|
2681
|
+
// -------------------------------------------------------------------------
|
|
2682
|
+
/**
|
|
2683
|
+
* Add route-specific settlement hooks.
|
|
2684
|
+
*
|
|
2685
|
+
* `beforeSettle` runs after a successful handler response but before
|
|
2686
|
+
* router-controlled settlement/broadcast, so it can still prevent the charge
|
|
2687
|
+
* for x402 and MPP transaction-payload flows. `afterSettle` runs after
|
|
2688
|
+
* settlement and is intended for durable ledgers or app-owned refund queues.
|
|
2689
|
+
*/
|
|
2690
|
+
settlement(lifecycle) {
|
|
2691
|
+
const next = this.fork();
|
|
2692
|
+
next._settlement = lifecycle;
|
|
2693
|
+
return next;
|
|
2694
|
+
}
|
|
2695
|
+
// -------------------------------------------------------------------------
|
|
2269
2696
|
// Terminal method
|
|
2270
2697
|
// -------------------------------------------------------------------------
|
|
2271
2698
|
handler(fn) {
|
|
2272
2699
|
const handlerFn = fn;
|
|
2700
|
+
if (!this._authMode) {
|
|
2701
|
+
throw new Error(
|
|
2702
|
+
`route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2273
2705
|
if (this._validateFn && !this._bodySchema) {
|
|
2274
2706
|
throw new Error(
|
|
2275
2707
|
`route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
|
|
2276
2708
|
);
|
|
2277
2709
|
}
|
|
2710
|
+
if (this._settlement && !this._pricing) {
|
|
2711
|
+
throw new Error(`route '${this._key}': .settlement() requires a paid route`);
|
|
2712
|
+
}
|
|
2278
2713
|
validateExamples(
|
|
2279
2714
|
this._key,
|
|
2280
2715
|
this._bodySchema,
|
|
@@ -2306,6 +2741,7 @@ var RouteBuilder = class {
|
|
|
2306
2741
|
providerName: this._providerName,
|
|
2307
2742
|
providerConfig: this._providerConfig,
|
|
2308
2743
|
validateFn: this._validateFn,
|
|
2744
|
+
settlement: this._settlement,
|
|
2309
2745
|
mppInfo: this._mppInfo
|
|
2310
2746
|
};
|
|
2311
2747
|
this._registry.register(entry);
|
|
@@ -2380,7 +2816,7 @@ function createRedisEntitlementStore(client, options) {
|
|
|
2380
2816
|
}
|
|
2381
2817
|
|
|
2382
2818
|
// src/discovery/well-known.ts
|
|
2383
|
-
var
|
|
2819
|
+
var import_server6 = require("next/server");
|
|
2384
2820
|
|
|
2385
2821
|
// src/discovery/utils/guidance.ts
|
|
2386
2822
|
async function resolveGuidance(discovery) {
|
|
@@ -2424,7 +2860,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2424
2860
|
if (instructions) {
|
|
2425
2861
|
body.instructions = instructions;
|
|
2426
2862
|
}
|
|
2427
|
-
return
|
|
2863
|
+
return import_server6.NextResponse.json(body, {
|
|
2428
2864
|
headers: {
|
|
2429
2865
|
"Access-Control-Allow-Origin": "*",
|
|
2430
2866
|
"Access-Control-Allow-Methods": "GET",
|
|
@@ -2441,13 +2877,14 @@ function toDiscoveryResource(method, url, mode) {
|
|
|
2441
2877
|
}
|
|
2442
2878
|
|
|
2443
2879
|
// src/discovery/openapi.ts
|
|
2444
|
-
var
|
|
2880
|
+
var import_server7 = require("next/server");
|
|
2881
|
+
init_constants();
|
|
2445
2882
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
2446
2883
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
2447
2884
|
let cached = null;
|
|
2448
2885
|
let validated = false;
|
|
2449
2886
|
return async (_request) => {
|
|
2450
|
-
if (cached) return
|
|
2887
|
+
if (cached) return import_server7.NextResponse.json(cached);
|
|
2451
2888
|
if (!validated && pricesKeys) {
|
|
2452
2889
|
registry.validate(pricesKeys);
|
|
2453
2890
|
validated = true;
|
|
@@ -2472,14 +2909,14 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2472
2909
|
securitySchemes.siwx = {
|
|
2473
2910
|
type: "apiKey",
|
|
2474
2911
|
in: "header",
|
|
2475
|
-
name:
|
|
2912
|
+
name: HEADERS.SIWX
|
|
2476
2913
|
};
|
|
2477
2914
|
}
|
|
2478
2915
|
if (requiresApiKeyScheme) {
|
|
2479
2916
|
securitySchemes.apiKey = {
|
|
2480
2917
|
type: "apiKey",
|
|
2481
2918
|
in: "header",
|
|
2482
|
-
name:
|
|
2919
|
+
name: HEADERS.API_KEY
|
|
2483
2920
|
};
|
|
2484
2921
|
}
|
|
2485
2922
|
const discoveryMetadata = {};
|
|
@@ -2510,7 +2947,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2510
2947
|
paths
|
|
2511
2948
|
};
|
|
2512
2949
|
cached = createDocument(openApiDocument);
|
|
2513
|
-
return
|
|
2950
|
+
return import_server7.NextResponse.json(cached);
|
|
2514
2951
|
};
|
|
2515
2952
|
}
|
|
2516
2953
|
function deriveTag(routeKey) {
|
|
@@ -2583,7 +3020,7 @@ function toProtocolObject(protocol, mppInfo) {
|
|
|
2583
3020
|
mpp: {
|
|
2584
3021
|
method: mppInfo?.method ?? "tempo",
|
|
2585
3022
|
intent: mppInfo?.intent ?? "charge",
|
|
2586
|
-
currency: mppInfo?.currency ??
|
|
3023
|
+
currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
|
|
2587
3024
|
}
|
|
2588
3025
|
};
|
|
2589
3026
|
}
|
|
@@ -2633,11 +3070,11 @@ function buildPricingInfo(entry) {
|
|
|
2633
3070
|
}
|
|
2634
3071
|
|
|
2635
3072
|
// src/discovery/llms-txt.ts
|
|
2636
|
-
var
|
|
3073
|
+
var import_server8 = require("next/server");
|
|
2637
3074
|
function createLlmsTxtHandler(discovery) {
|
|
2638
3075
|
return async (_request) => {
|
|
2639
3076
|
const guidance = await resolveGuidance(discovery) ?? "";
|
|
2640
|
-
return new
|
|
3077
|
+
return new import_server8.NextResponse(guidance, {
|
|
2641
3078
|
headers: {
|
|
2642
3079
|
"Content-Type": "text/plain; charset=utf-8",
|
|
2643
3080
|
"Access-Control-Allow-Origin": "*"
|
|
@@ -2647,14 +3084,14 @@ function createLlmsTxtHandler(discovery) {
|
|
|
2647
3084
|
}
|
|
2648
3085
|
|
|
2649
3086
|
// src/index.ts
|
|
2650
|
-
|
|
3087
|
+
init_accepts();
|
|
2651
3088
|
init_constants();
|
|
2652
3089
|
|
|
2653
3090
|
// src/config.ts
|
|
2654
3091
|
init_constants();
|
|
2655
3092
|
init_evm();
|
|
2656
3093
|
init_solana();
|
|
2657
|
-
|
|
3094
|
+
init_accepts();
|
|
2658
3095
|
var RouterConfigError = class extends Error {
|
|
2659
3096
|
issues;
|
|
2660
3097
|
constructor(issues) {
|
|
@@ -2698,6 +3135,8 @@ function mppFromEnv(env, options = {}) {
|
|
|
2698
3135
|
const secretKey = env.MPP_SECRET_KEY;
|
|
2699
3136
|
const currency = env.MPP_CURRENCY;
|
|
2700
3137
|
const rpcUrl = env.TEMPO_RPC_URL;
|
|
3138
|
+
const feePayerKey = options.feePayerKey ?? env.MPP_FEE_PAYER_KEY;
|
|
3139
|
+
const feePayerKeySource = options.feePayerKey !== void 0 ? "feePayerKey" : "MPP_FEE_PAYER_KEY";
|
|
2701
3140
|
const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
|
|
2702
3141
|
if (!hasAnyMppEnv) return void 0;
|
|
2703
3142
|
const missing = [
|
|
@@ -2708,12 +3147,21 @@ function mppFromEnv(env, options = {}) {
|
|
|
2708
3147
|
if (missing.length > 0) {
|
|
2709
3148
|
throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
|
|
2710
3149
|
}
|
|
3150
|
+
if (!isEvmAddress(currency)) {
|
|
3151
|
+
throw new Error("MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address");
|
|
3152
|
+
}
|
|
3153
|
+
if (options.recipient && !isEvmAddress(options.recipient)) {
|
|
3154
|
+
throw new Error("MPP recipient must be a 0x-prefixed EVM address");
|
|
3155
|
+
}
|
|
3156
|
+
if (feePayerKey && !isEvmPrivateKey(feePayerKey)) {
|
|
3157
|
+
throw new Error(`${feePayerKeySource} must be a 0x-prefixed 32-byte EVM private key`);
|
|
3158
|
+
}
|
|
2711
3159
|
return {
|
|
2712
3160
|
secretKey,
|
|
2713
3161
|
currency,
|
|
2714
3162
|
rpcUrl,
|
|
2715
3163
|
...options.recipient ? { recipient: options.recipient } : {},
|
|
2716
|
-
...
|
|
3164
|
+
...feePayerKey ? { feePayerKey } : {},
|
|
2717
3165
|
...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
|
|
2718
3166
|
};
|
|
2719
3167
|
}
|
|
@@ -2851,13 +3299,26 @@ function validateMppConfig(config, env) {
|
|
|
2851
3299
|
protocol: "mpp",
|
|
2852
3300
|
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
2853
3301
|
});
|
|
3302
|
+
} else if (!isEvmAddress(mpp.currency)) {
|
|
3303
|
+
issues.push({
|
|
3304
|
+
code: "invalid_mpp_currency",
|
|
3305
|
+
protocol: "mpp",
|
|
3306
|
+
message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
|
|
3307
|
+
});
|
|
2854
3308
|
}
|
|
2855
|
-
|
|
3309
|
+
const mppRecipient = mpp.recipient ?? config.payeeAddress;
|
|
3310
|
+
if (!mppRecipient) {
|
|
2856
3311
|
issues.push({
|
|
2857
3312
|
code: "missing_mpp_recipient",
|
|
2858
3313
|
protocol: "mpp",
|
|
2859
3314
|
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
2860
3315
|
});
|
|
3316
|
+
} else if (!isEvmAddress(mppRecipient)) {
|
|
3317
|
+
issues.push({
|
|
3318
|
+
code: "invalid_mpp_recipient",
|
|
3319
|
+
protocol: "mpp",
|
|
3320
|
+
message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
3321
|
+
});
|
|
2861
3322
|
}
|
|
2862
3323
|
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
2863
3324
|
if (placeholder) {
|
|
@@ -2874,6 +3335,13 @@ function validateMppConfig(config, env) {
|
|
|
2874
3335
|
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
2875
3336
|
});
|
|
2876
3337
|
}
|
|
3338
|
+
if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
|
|
3339
|
+
issues.push({
|
|
3340
|
+
code: "invalid_mpp_fee_payer_key",
|
|
3341
|
+
protocol: "mpp",
|
|
3342
|
+
message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
2877
3345
|
if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
|
|
2878
3346
|
issues.push({
|
|
2879
3347
|
code: "missing_mpp_default_store_env",
|
|
@@ -2891,8 +3359,14 @@ function usesDefaultEvmFacilitator(config) {
|
|
|
2891
3359
|
function isSupportedX402Network(network) {
|
|
2892
3360
|
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2893
3361
|
}
|
|
3362
|
+
function isEvmAddress(value) {
|
|
3363
|
+
return /^0x[a-fA-F0-9]{40}$/.test(value);
|
|
3364
|
+
}
|
|
3365
|
+
function isEvmPrivateKey(value) {
|
|
3366
|
+
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
3367
|
+
}
|
|
2894
3368
|
function findPlaceholderPayee(values) {
|
|
2895
|
-
return values.find((value) => value
|
|
3369
|
+
return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
|
|
2896
3370
|
}
|
|
2897
3371
|
|
|
2898
3372
|
// src/index.ts
|
|
@@ -2941,6 +3415,7 @@ function createRouter(config) {
|
|
|
2941
3415
|
nonceStore,
|
|
2942
3416
|
entitlementStore,
|
|
2943
3417
|
payeeAddress: config.payeeAddress ?? "",
|
|
3418
|
+
mppRecipient: config.mpp?.recipient ?? config.payeeAddress,
|
|
2944
3419
|
network,
|
|
2945
3420
|
x402FacilitatorsByNetwork: void 0,
|
|
2946
3421
|
x402Accepts,
|