@agentcash/router 1.5.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +85 -0
- package/README.md +138 -526
- package/dist/index.cjs +2380 -1244
- package/dist/index.d.cts +451 -317
- package/dist/index.d.ts +451 -317
- package/dist/index.js +2372 -1227
- package/package.json +16 -24
- package/.claude/CLAUDE.md +0 -229
- package/.claude/skills/router-guide/SKILL.md +0 -585
package/dist/index.js
CHANGED
|
@@ -9,14 +9,18 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/constants.ts
|
|
12
|
-
var
|
|
12
|
+
var BASE_MAINNET_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_ADDRESS, TEMPO_USDC_DECIMALS, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, ZERO_EVM_ADDRESS, DEFAULT_SOLANA_FACILITATOR_URL;
|
|
13
13
|
var init_constants = __esm({
|
|
14
14
|
"src/constants.ts"() {
|
|
15
15
|
"use strict";
|
|
16
|
-
|
|
16
|
+
BASE_MAINNET_NETWORK = "eip155:8453";
|
|
17
17
|
SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
18
|
-
|
|
18
|
+
TEMPO_USDC_ADDRESS = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
19
|
+
TEMPO_USDC_DECIMALS = 6;
|
|
20
|
+
BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
21
|
+
BASE_USDC_DECIMALS = 6;
|
|
19
22
|
ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
23
|
+
DEFAULT_SOLANA_FACILITATOR_URL = "https://facilitator.corbits.dev";
|
|
20
24
|
}
|
|
21
25
|
});
|
|
22
26
|
|
|
@@ -33,7 +37,7 @@ function getConfiguredX402Accepts(config) {
|
|
|
33
37
|
return [
|
|
34
38
|
{
|
|
35
39
|
scheme: "exact",
|
|
36
|
-
network: config.network ??
|
|
40
|
+
network: config.network ?? BASE_MAINNET_NETWORK,
|
|
37
41
|
payTo: config.payeeAddress
|
|
38
42
|
}
|
|
39
43
|
];
|
|
@@ -83,6 +87,18 @@ function buildEvmExactOptions(accepts, price) {
|
|
|
83
87
|
payTo
|
|
84
88
|
}));
|
|
85
89
|
}
|
|
90
|
+
function buildEvmUptoOptions(accepts, price) {
|
|
91
|
+
return accepts.filter(
|
|
92
|
+
(accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
|
|
93
|
+
).map((accept) => ({
|
|
94
|
+
scheme: "upto",
|
|
95
|
+
network: accept.network,
|
|
96
|
+
payTo: accept.payTo,
|
|
97
|
+
price,
|
|
98
|
+
...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
|
|
99
|
+
...accept.extra ? { extra: accept.extra } : {}
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
86
102
|
var init_evm = __esm({
|
|
87
103
|
"src/protocols/x402/evm.ts"() {
|
|
88
104
|
"use strict";
|
|
@@ -201,7 +217,10 @@ async function getAcceptsHeadersForFacilitator(facilitator) {
|
|
|
201
217
|
return {};
|
|
202
218
|
}
|
|
203
219
|
function resolveX402FacilitatorTarget(config, network, defaultEvmFacilitator) {
|
|
204
|
-
|
|
220
|
+
if (isSolanaNetwork(network)) {
|
|
221
|
+
return config.x402?.facilitators?.solana ?? DEFAULT_SOLANA_FACILITATOR_URL;
|
|
222
|
+
}
|
|
223
|
+
return defaultEvmFacilitator;
|
|
205
224
|
}
|
|
206
225
|
function normalizeFacilitatorTarget(target) {
|
|
207
226
|
return typeof target === "string" ? { url: target } : target;
|
|
@@ -214,19 +233,18 @@ function getNetworkFamily(network) {
|
|
|
214
233
|
function sameFacilitatorConfig(a, b) {
|
|
215
234
|
return a.url === b.url && a.createAuthHeaders === b.createAuthHeaders && a.createAcceptsHeaders === b.createAcceptsHeaders;
|
|
216
235
|
}
|
|
217
|
-
var DEFAULT_SOLANA_FACILITATOR_URL;
|
|
218
236
|
var init_facilitators = __esm({
|
|
219
237
|
"src/protocols/x402/facilitators.ts"() {
|
|
220
238
|
"use strict";
|
|
221
239
|
init_evm();
|
|
222
240
|
init_solana();
|
|
223
|
-
|
|
241
|
+
init_constants();
|
|
224
242
|
}
|
|
225
243
|
});
|
|
226
244
|
|
|
227
|
-
// src/server.ts
|
|
228
|
-
var
|
|
229
|
-
__export(
|
|
245
|
+
// src/init/x402-server.ts
|
|
246
|
+
var x402_server_exports = {};
|
|
247
|
+
__export(x402_server_exports, {
|
|
230
248
|
createX402Server: () => createX402Server
|
|
231
249
|
});
|
|
232
250
|
async function createX402Server(config) {
|
|
@@ -267,44 +285,44 @@ async function createX402Server(config) {
|
|
|
267
285
|
facilitatorsByNetwork
|
|
268
286
|
};
|
|
269
287
|
}
|
|
270
|
-
function
|
|
288
|
+
function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
|
|
289
|
+
const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
|
|
290
|
+
return groups.map((group) => {
|
|
291
|
+
const inner = new HTTPFacilitatorClient(group.config);
|
|
292
|
+
const kinds = buildSupportedKinds(group);
|
|
293
|
+
return hardcodedSupportedClient(inner, kinds);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function hardcodedSupportedClient(inner, kinds) {
|
|
271
297
|
return {
|
|
272
298
|
verify: inner.verify.bind(inner),
|
|
273
299
|
settle: inner.settle.bind(inner),
|
|
274
|
-
getSupported: async () => ({
|
|
275
|
-
kinds,
|
|
276
|
-
extensions: [],
|
|
277
|
-
signers: {}
|
|
278
|
-
})
|
|
300
|
+
getSupported: async () => ({ kinds, extensions: [], signers: {} })
|
|
279
301
|
};
|
|
280
302
|
}
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
extra: {
|
|
292
|
-
features: {
|
|
293
|
-
xSettlementAccountSupported: true
|
|
294
|
-
}
|
|
303
|
+
function buildSupportedKinds(group) {
|
|
304
|
+
return group.networks.flatMap((network) => {
|
|
305
|
+
const exactKind = {
|
|
306
|
+
x402Version: 2,
|
|
307
|
+
scheme: "exact",
|
|
308
|
+
network,
|
|
309
|
+
...group.family === "solana" ? {
|
|
310
|
+
extra: {
|
|
311
|
+
features: {
|
|
312
|
+
xSettlementAccountSupported: true
|
|
295
313
|
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return [exactKind];
|
|
302
|
-
}
|
|
303
|
-
return
|
|
314
|
+
}
|
|
315
|
+
} : {}
|
|
316
|
+
};
|
|
317
|
+
const uptoKind = { x402Version: 2, scheme: "upto", network };
|
|
318
|
+
if (group.family === "evm") {
|
|
319
|
+
return [exactKind, uptoKind];
|
|
320
|
+
}
|
|
321
|
+
return [exactKind, uptoKind];
|
|
304
322
|
});
|
|
305
323
|
}
|
|
306
|
-
var
|
|
307
|
-
"src/server.ts"() {
|
|
324
|
+
var init_x402_server = __esm({
|
|
325
|
+
"src/init/x402-server.ts"() {
|
|
308
326
|
"use strict";
|
|
309
327
|
init_evm();
|
|
310
328
|
init_solana();
|
|
@@ -313,65 +331,9 @@ var init_server = __esm({
|
|
|
313
331
|
}
|
|
314
332
|
});
|
|
315
333
|
|
|
316
|
-
// src/upstash-rest.ts
|
|
317
|
-
var upstash_rest_exports = {};
|
|
318
|
-
__export(upstash_rest_exports, {
|
|
319
|
-
createUpstashRest: () => createUpstashRest
|
|
320
|
-
});
|
|
321
|
-
function createUpstashRest(url, token) {
|
|
322
|
-
const base = url.replace(/\/+$/, "");
|
|
323
|
-
const headers = { Authorization: `Bearer ${token}` };
|
|
324
|
-
async function get(key) {
|
|
325
|
-
const res = await fetch(`${base}/get/${key}`, { headers });
|
|
326
|
-
if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
|
|
327
|
-
const { result } = await res.json();
|
|
328
|
-
return result ?? null;
|
|
329
|
-
}
|
|
330
|
-
async function set(key, value) {
|
|
331
|
-
const res = await fetch(`${base}`, {
|
|
332
|
-
method: "POST",
|
|
333
|
-
headers: { ...headers, "Content-Type": "application/json" },
|
|
334
|
-
body: JSON.stringify(["SET", key, JSON.stringify(value)])
|
|
335
|
-
});
|
|
336
|
-
if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
|
|
337
|
-
return await res.json();
|
|
338
|
-
}
|
|
339
|
-
async function del(key) {
|
|
340
|
-
const res = await fetch(`${base}`, {
|
|
341
|
-
method: "POST",
|
|
342
|
-
headers: { ...headers, "Content-Type": "application/json" },
|
|
343
|
-
body: JSON.stringify(["DEL", key])
|
|
344
|
-
});
|
|
345
|
-
if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
|
|
346
|
-
return await res.json();
|
|
347
|
-
}
|
|
348
|
-
return {
|
|
349
|
-
get,
|
|
350
|
-
set,
|
|
351
|
-
del,
|
|
352
|
-
async update(key, fn) {
|
|
353
|
-
const current = await get(key);
|
|
354
|
-
const change = fn(current);
|
|
355
|
-
if (change.op === "set") await set(key, change.value);
|
|
356
|
-
if (change.op === "delete") await del(key);
|
|
357
|
-
return change.result;
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
var init_upstash_rest = __esm({
|
|
362
|
-
"src/upstash-rest.ts"() {
|
|
363
|
-
"use strict";
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
|
|
367
334
|
// src/registry.ts
|
|
368
335
|
var RouteRegistry = class {
|
|
369
336
|
routes = /* @__PURE__ */ new Map();
|
|
370
|
-
// Internal map key includes the HTTP method so that POST and DELETE on the
|
|
371
|
-
// same path coexist. Within the same path+method, last-write-wins is still
|
|
372
|
-
// intentional — Next.js module loading order is non-deterministic during
|
|
373
|
-
// build and discovery stubs may register the same route in either order.
|
|
374
|
-
// Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
|
|
375
337
|
mapKey(entry) {
|
|
376
338
|
return `${entry.key}:${entry.method}`;
|
|
377
339
|
}
|
|
@@ -384,8 +346,6 @@ var RouteRegistry = class {
|
|
|
384
346
|
}
|
|
385
347
|
this.routes.set(k, entry);
|
|
386
348
|
}
|
|
387
|
-
// Accepts either a compound key ("site/domain:DELETE") or a path-only key
|
|
388
|
-
// ("site/domain") — path-only returns the first registered method for that path.
|
|
389
349
|
get(key) {
|
|
390
350
|
const direct = this.routes.get(key);
|
|
391
351
|
if (direct) return direct;
|
|
@@ -417,24 +377,17 @@ var RouteRegistry = class {
|
|
|
417
377
|
|
|
418
378
|
// src/headers.ts
|
|
419
379
|
var HEADERS = {
|
|
420
|
-
// ---- Standard HTTP ----
|
|
421
380
|
AUTHORIZATION: "Authorization",
|
|
422
381
|
WWW_AUTHENTICATE: "WWW-Authenticate",
|
|
423
|
-
// ---- Auth ----
|
|
424
382
|
API_KEY: "X-API-Key",
|
|
425
|
-
// ---- Request meta (used by plugin/observability) ----
|
|
426
383
|
WALLET_ADDRESS: "X-Wallet-Address",
|
|
427
384
|
CLIENT_ID: "X-Client-ID",
|
|
428
385
|
SESSION_ID: "X-Session-ID",
|
|
429
|
-
// ---- SIWX ----
|
|
430
386
|
SIWX: "SIGN-IN-WITH-X",
|
|
431
|
-
// ---- x402 (payment) ----
|
|
432
387
|
X402_PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
|
|
433
|
-
/** Legacy x402 payment header — accepted alongside PAYMENT-SIGNATURE. */
|
|
434
388
|
X402_PAYMENT_LEGACY: "X-PAYMENT",
|
|
435
389
|
X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
|
|
436
390
|
X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
|
|
437
|
-
// ---- MPP (payment) ----
|
|
438
391
|
MPP_PAYMENT_RECEIPT: "Payment-Receipt"
|
|
439
392
|
};
|
|
440
393
|
var AUTH_SCHEME = {
|
|
@@ -442,7 +395,7 @@ var AUTH_SCHEME = {
|
|
|
442
395
|
MPP_PAYMENT: "Payment "
|
|
443
396
|
};
|
|
444
397
|
|
|
445
|
-
// src/plugin.ts
|
|
398
|
+
// src/plugin/index.ts
|
|
446
399
|
function createDefaultContext(meta) {
|
|
447
400
|
const ctx = {
|
|
448
401
|
requestId: meta.requestId,
|
|
@@ -478,50 +431,32 @@ function firePluginHook(plugin, method, ...args) {
|
|
|
478
431
|
return void 0;
|
|
479
432
|
}
|
|
480
433
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
onPaymentVerified(_ctx, payment) {
|
|
492
|
-
console.log(`[router] VERIFIED ${payment.protocol} ${payment.payer} ${payment.amount}`);
|
|
493
|
-
},
|
|
494
|
-
onPaymentSettled(_ctx, settlement) {
|
|
495
|
-
console.log(`[router] SETTLED ${settlement.protocol} tx=${settlement.transaction}`);
|
|
496
|
-
},
|
|
497
|
-
onResponse(ctx, response) {
|
|
498
|
-
const wallet = ctx.verifiedWallet ? ` wallet=${ctx.verifiedWallet}` : "";
|
|
499
|
-
console.log(
|
|
500
|
-
`[router] ${ctx.route} \u2192 ${response.statusCode} (${response.duration}ms)${wallet}`
|
|
501
|
-
);
|
|
502
|
-
},
|
|
503
|
-
onError(_ctx, error) {
|
|
504
|
-
console.error(`[router] ERROR ${error.status}: ${error.message}`);
|
|
505
|
-
},
|
|
506
|
-
onAlert(_ctx, alert) {
|
|
507
|
-
const logFn = alert.level === "critical" || alert.level === "error" ? console.error : alert.level === "warn" ? console.warn : console.log;
|
|
508
|
-
logFn(
|
|
509
|
-
`[router] ${alert.level.toUpperCase()} ${alert.route}: ${alert.message}`,
|
|
510
|
-
alert.meta ?? ""
|
|
511
|
-
);
|
|
512
|
-
},
|
|
513
|
-
onProviderQuota(_ctx, event) {
|
|
514
|
-
const logFn = event.level === "critical" ? console.error : event.level === "warn" ? console.warn : console.log;
|
|
515
|
-
logFn(`[router] QUOTA ${event.level.toUpperCase()} ${event.provider}: ${event.message}`);
|
|
516
|
-
}
|
|
434
|
+
|
|
435
|
+
// src/plugin/reporter.ts
|
|
436
|
+
function createReporter(plugin, pluginCtx, route) {
|
|
437
|
+
return (level, message, meta) => {
|
|
438
|
+
firePluginHook(plugin, "onAlert", pluginCtx, {
|
|
439
|
+
level,
|
|
440
|
+
message,
|
|
441
|
+
route,
|
|
442
|
+
...meta ? { meta } : {}
|
|
443
|
+
});
|
|
517
444
|
};
|
|
518
445
|
}
|
|
519
446
|
|
|
520
|
-
// src/pipeline/
|
|
447
|
+
// src/pipeline/steps/preflight.ts
|
|
521
448
|
function preflight(routeEntry, handler, deps, request) {
|
|
522
449
|
const meta = buildMeta(request, routeEntry);
|
|
523
450
|
const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
|
|
524
|
-
return {
|
|
451
|
+
return {
|
|
452
|
+
routeEntry,
|
|
453
|
+
handler,
|
|
454
|
+
deps,
|
|
455
|
+
request,
|
|
456
|
+
meta,
|
|
457
|
+
pluginCtx,
|
|
458
|
+
report: createReporter(deps.plugin, pluginCtx, routeEntry.key)
|
|
459
|
+
};
|
|
525
460
|
}
|
|
526
461
|
function buildMeta(request, routeEntry) {
|
|
527
462
|
return {
|
|
@@ -539,10 +474,10 @@ function buildMeta(request, routeEntry) {
|
|
|
539
474
|
};
|
|
540
475
|
}
|
|
541
476
|
|
|
542
|
-
// src/pipeline/
|
|
477
|
+
// src/pipeline/steps/parse-body.ts
|
|
543
478
|
import { NextResponse } from "next/server";
|
|
544
479
|
|
|
545
|
-
// src/body.ts
|
|
480
|
+
// src/pipeline/body.ts
|
|
546
481
|
async function bufferBody(request) {
|
|
547
482
|
const text = await request.text();
|
|
548
483
|
if (!text) return void 0;
|
|
@@ -566,46 +501,19 @@ function validateBody(parsed, schema) {
|
|
|
566
501
|
};
|
|
567
502
|
}
|
|
568
503
|
|
|
569
|
-
// src/
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
return {
|
|
576
|
-
ok: false,
|
|
577
|
-
response: NextResponse.json(
|
|
578
|
-
{ success: false, error: result.error, issues: result.issues },
|
|
579
|
-
{ status: 400 }
|
|
580
|
-
)
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// src/pipeline/context/parse-query.ts
|
|
585
|
-
function parseQuery(request, routeEntry) {
|
|
586
|
-
if (!routeEntry.querySchema) return void 0;
|
|
587
|
-
const params = Object.fromEntries(request.nextUrl.searchParams.entries());
|
|
588
|
-
const result = routeEntry.querySchema.safeParse(params);
|
|
589
|
-
return result.success ? result.data : params;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// src/pipeline/context/errors.ts
|
|
593
|
-
function errorStatus(error, fallback) {
|
|
594
|
-
const status = error?.status;
|
|
595
|
-
return typeof status === "number" ? status : fallback;
|
|
504
|
+
// src/plugin/events.ts
|
|
505
|
+
function fireAuthVerified(ctx, event) {
|
|
506
|
+
firePluginHook(ctx.deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
507
|
+
...event,
|
|
508
|
+
route: ctx.routeEntry.key
|
|
509
|
+
});
|
|
596
510
|
}
|
|
597
|
-
function
|
|
598
|
-
|
|
511
|
+
function firePaymentVerified(ctx, event) {
|
|
512
|
+
firePluginHook(ctx.deps.plugin, "onPaymentVerified", ctx.pluginCtx, event);
|
|
599
513
|
}
|
|
600
|
-
function
|
|
601
|
-
|
|
602
|
-
return Object.assign(new Error(message), { status: response.status });
|
|
514
|
+
function firePaymentSettled(ctx, event) {
|
|
515
|
+
firePluginHook(ctx.deps.plugin, "onPaymentSettled", ctx.pluginCtx, event);
|
|
603
516
|
}
|
|
604
|
-
|
|
605
|
-
// src/pipeline/context/fail.ts
|
|
606
|
-
import { NextResponse as NextResponse2 } from "next/server";
|
|
607
|
-
|
|
608
|
-
// src/pipeline/context/fire-plugin-response.ts
|
|
609
517
|
function firePluginResponse(ctx, response, requestBody, responseBody) {
|
|
610
518
|
firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
|
|
611
519
|
statusCode: response.status,
|
|
@@ -624,15 +532,72 @@ function firePluginResponse(ctx, response, requestBody, responseBody) {
|
|
|
624
532
|
});
|
|
625
533
|
}
|
|
626
534
|
}
|
|
535
|
+
function fireProviderQuota(ctx, response, handlerResult) {
|
|
536
|
+
const { providerName, providerConfig } = ctx.routeEntry;
|
|
537
|
+
if (!providerName || !providerConfig?.extractQuota) return;
|
|
538
|
+
if (response.status >= 400) return;
|
|
539
|
+
try {
|
|
540
|
+
const quota = providerConfig.extractQuota(handlerResult, response.headers);
|
|
541
|
+
if (!quota) return;
|
|
542
|
+
const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
|
|
543
|
+
const overage = providerConfig.overage ?? "same-rate";
|
|
544
|
+
const event = {
|
|
545
|
+
provider: providerName,
|
|
546
|
+
route: ctx.routeEntry.key,
|
|
547
|
+
remaining: quota.remaining,
|
|
548
|
+
limit: quota.limit,
|
|
549
|
+
spend: quota.spend,
|
|
550
|
+
level,
|
|
551
|
+
overage,
|
|
552
|
+
message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
|
|
553
|
+
};
|
|
554
|
+
firePluginHook(ctx.deps.plugin, "onProviderQuota", ctx.pluginCtx, event);
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function computeQuotaLevel(remaining, warn, critical) {
|
|
559
|
+
if (remaining === null) return "healthy";
|
|
560
|
+
if (critical !== void 0 && remaining <= critical) return "critical";
|
|
561
|
+
if (warn !== void 0 && remaining <= warn) return "warn";
|
|
562
|
+
return "healthy";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/pipeline/steps/parse-body.ts
|
|
566
|
+
async function parseBody(ctx, request = ctx.request) {
|
|
567
|
+
if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
|
|
568
|
+
const raw = await bufferBody(request);
|
|
569
|
+
const result = validateBody(raw, ctx.routeEntry.bodySchema);
|
|
570
|
+
if (result.success) return { ok: true, data: result.data };
|
|
571
|
+
const response = NextResponse.json(
|
|
572
|
+
{ success: false, error: result.error, issues: result.issues },
|
|
573
|
+
{ status: 400 }
|
|
574
|
+
);
|
|
575
|
+
firePluginResponse(ctx, response);
|
|
576
|
+
return { ok: false, response };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/pipeline/steps/errors.ts
|
|
580
|
+
function errorStatus(error, fallback) {
|
|
581
|
+
const status = error?.status;
|
|
582
|
+
return typeof status === "number" ? status : fallback;
|
|
583
|
+
}
|
|
584
|
+
function errorMessage(error, fallback) {
|
|
585
|
+
return error instanceof Error ? error.message : fallback;
|
|
586
|
+
}
|
|
587
|
+
function handlerFailureError(response) {
|
|
588
|
+
const message = response.statusText || `Handler returned HTTP ${response.status}`;
|
|
589
|
+
return Object.assign(new Error(message), { status: response.status });
|
|
590
|
+
}
|
|
627
591
|
|
|
628
|
-
// src/pipeline/
|
|
592
|
+
// src/pipeline/steps/fail.ts
|
|
593
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
629
594
|
function fail(ctx, status, message, requestBody) {
|
|
630
595
|
const response = NextResponse2.json({ success: false, error: message }, { status });
|
|
631
596
|
firePluginResponse(ctx, response, requestBody);
|
|
632
597
|
return response;
|
|
633
598
|
}
|
|
634
599
|
|
|
635
|
-
// src/pipeline/
|
|
600
|
+
// src/pipeline/steps/run-validate.ts
|
|
636
601
|
async function runValidate(ctx, body) {
|
|
637
602
|
if (!ctx.routeEntry.validateFn) return null;
|
|
638
603
|
try {
|
|
@@ -643,7 +608,7 @@ async function runValidate(ctx, body) {
|
|
|
643
608
|
}
|
|
644
609
|
}
|
|
645
610
|
|
|
646
|
-
// src/
|
|
611
|
+
// src/pipeline/flows/static/static-invoke.ts
|
|
647
612
|
import { NextResponse as NextResponse3 } from "next/server";
|
|
648
613
|
|
|
649
614
|
// src/types.ts
|
|
@@ -655,23 +620,23 @@ var HttpError = class extends Error {
|
|
|
655
620
|
}
|
|
656
621
|
};
|
|
657
622
|
|
|
658
|
-
// src/
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
} catch (error) {
|
|
665
|
-
options.onError?.(error);
|
|
666
|
-
const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
|
|
667
|
-
const message = error instanceof Error ? error.message : "Internal error";
|
|
668
|
-
return NextResponse3.json({ success: false, error: message }, { status });
|
|
669
|
-
}
|
|
623
|
+
// src/pipeline/steps/parse-query.ts
|
|
624
|
+
function parseQuery(request, routeEntry) {
|
|
625
|
+
if (!routeEntry.querySchema) return void 0;
|
|
626
|
+
const params = Object.fromEntries(request.nextUrl.searchParams.entries());
|
|
627
|
+
const result = routeEntry.querySchema.safeParse(params);
|
|
628
|
+
return result.success ? result.data : params;
|
|
670
629
|
}
|
|
671
630
|
|
|
672
|
-
// src/pipeline/
|
|
673
|
-
|
|
674
|
-
|
|
631
|
+
// src/pipeline/flows/static/static-invoke.ts
|
|
632
|
+
function invokePaidStatic(ctx, wallet, account, body, payment) {
|
|
633
|
+
return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
|
|
634
|
+
}
|
|
635
|
+
function invokeUnauthed(ctx, wallet, account, body) {
|
|
636
|
+
return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, null));
|
|
637
|
+
}
|
|
638
|
+
function buildHandlerCtx(ctx, wallet, account, body, payment) {
|
|
639
|
+
return {
|
|
675
640
|
body,
|
|
676
641
|
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
677
642
|
request: ctx.request,
|
|
@@ -680,85 +645,71 @@ async function invoke(ctx, wallet, account, body, payment) {
|
|
|
680
645
|
wallet,
|
|
681
646
|
payment,
|
|
682
647
|
account,
|
|
683
|
-
alert
|
|
684
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
685
|
-
level,
|
|
686
|
-
message,
|
|
687
|
-
route: ctx.routeEntry.key,
|
|
688
|
-
meta: alertMeta
|
|
689
|
-
});
|
|
690
|
-
},
|
|
648
|
+
alert: ctx.report,
|
|
691
649
|
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
692
650
|
};
|
|
693
|
-
let rawResult;
|
|
694
|
-
let handlerError;
|
|
695
|
-
const response = await safeCallHandler(
|
|
696
|
-
async (c) => {
|
|
697
|
-
rawResult = await ctx.handler(c);
|
|
698
|
-
return rawResult;
|
|
699
|
-
},
|
|
700
|
-
handlerCtx,
|
|
701
|
-
{
|
|
702
|
-
onError(error) {
|
|
703
|
-
handlerError = error;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
);
|
|
707
|
-
return { response, rawResult, handlerError };
|
|
708
651
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
function fireProviderQuota(ctx, response, handlerResult) {
|
|
712
|
-
const { providerName, providerConfig } = ctx.routeEntry;
|
|
713
|
-
if (!providerName || !providerConfig?.extractQuota) return;
|
|
714
|
-
if (response.status >= 400) return;
|
|
652
|
+
async function runHandler(ctx, handlerCtx) {
|
|
653
|
+
let returned;
|
|
715
654
|
try {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
} catch {
|
|
655
|
+
returned = ctx.handler(handlerCtx);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
return errorResult(error);
|
|
658
|
+
}
|
|
659
|
+
if (isAsyncIterable(returned) && !isThenable(returned)) {
|
|
660
|
+
return errorResult(
|
|
661
|
+
new HttpError(
|
|
662
|
+
`route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
|
|
663
|
+
500
|
|
664
|
+
)
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
let rawResult;
|
|
668
|
+
try {
|
|
669
|
+
rawResult = await returned;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
return errorResult(error);
|
|
732
672
|
}
|
|
673
|
+
const response = rawResult instanceof Response ? rawResult : NextResponse3.json(rawResult);
|
|
674
|
+
return { response, rawResult };
|
|
733
675
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
676
|
+
function errorResult(error) {
|
|
677
|
+
const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
|
|
678
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
679
|
+
return {
|
|
680
|
+
response: NextResponse3.json({ success: false, error: message }, { status }),
|
|
681
|
+
rawResult: void 0,
|
|
682
|
+
handlerError: error
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function isAsyncIterable(value) {
|
|
686
|
+
return value != null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
687
|
+
}
|
|
688
|
+
function isThenable(value) {
|
|
689
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
739
690
|
}
|
|
740
691
|
|
|
741
|
-
// src/pipeline/
|
|
692
|
+
// src/pipeline/steps/finalize/response.ts
|
|
742
693
|
function finalize(ctx, response, rawResult, requestBody) {
|
|
743
694
|
fireProviderQuota(ctx, response, rawResult);
|
|
744
695
|
firePluginResponse(ctx, response, requestBody, rawResult);
|
|
745
696
|
return response;
|
|
746
697
|
}
|
|
747
698
|
|
|
748
|
-
// src/pipeline/
|
|
749
|
-
async function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
699
|
+
// src/pipeline/steps/grant-entitlement.ts
|
|
700
|
+
async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
701
|
+
if (!ctx.routeEntry.siwxEnabled) return;
|
|
702
|
+
try {
|
|
703
|
+
await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
ctx.report(
|
|
706
|
+
"warn",
|
|
707
|
+
`Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`
|
|
708
|
+
);
|
|
754
709
|
}
|
|
755
|
-
const validateErr = await runValidate(ctx, body.data);
|
|
756
|
-
if (validateErr) return validateErr;
|
|
757
|
-
const result = await invoke(ctx, wallet, account, body.data, null);
|
|
758
|
-
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
759
710
|
}
|
|
760
711
|
|
|
761
|
-
// src/pipeline/
|
|
712
|
+
// src/pipeline/steps/settlement-context.ts
|
|
762
713
|
function settlementContext(ctx, scope) {
|
|
763
714
|
return {
|
|
764
715
|
route: ctx.routeEntry.key,
|
|
@@ -772,24 +723,7 @@ function settlementContext(ctx, scope) {
|
|
|
772
723
|
};
|
|
773
724
|
}
|
|
774
725
|
|
|
775
|
-
// src/pipeline/
|
|
776
|
-
async function runBeforeSettle(ctx, scope) {
|
|
777
|
-
const hook = ctx.routeEntry.settlement?.beforeSettle;
|
|
778
|
-
if (!hook) return null;
|
|
779
|
-
try {
|
|
780
|
-
await hook(settlementContext(ctx, scope));
|
|
781
|
-
return null;
|
|
782
|
-
} catch (error) {
|
|
783
|
-
return fail(
|
|
784
|
-
ctx,
|
|
785
|
-
errorStatus(error, 500),
|
|
786
|
-
errorMessage(error, "Pre-settlement validation failed"),
|
|
787
|
-
scope.body
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// src/pipeline/context/run-settlement-error.ts
|
|
726
|
+
// src/pipeline/steps/run-settlement-error.ts
|
|
793
727
|
async function runSettlementError(ctx, scope, error, phase) {
|
|
794
728
|
const hook = ctx.routeEntry.settlement?.onSettlementError;
|
|
795
729
|
if (!hook) return;
|
|
@@ -797,16 +731,11 @@ async function runSettlementError(ctx, scope, error, phase) {
|
|
|
797
731
|
await hook({ ...settlementContext(ctx, scope), error, phase });
|
|
798
732
|
} catch (hookError) {
|
|
799
733
|
const message = errorMessage(hookError, "Settlement error hook failed");
|
|
800
|
-
|
|
801
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
802
|
-
level: "error",
|
|
803
|
-
message: `Settlement error hook failed: ${message}`,
|
|
804
|
-
route: ctx.routeEntry.key
|
|
805
|
-
});
|
|
734
|
+
ctx.report("error", `Settlement error hook failed: ${message}`);
|
|
806
735
|
}
|
|
807
736
|
}
|
|
808
737
|
|
|
809
|
-
// src/pipeline/
|
|
738
|
+
// src/pipeline/steps/run-after-settle.ts
|
|
810
739
|
async function runAfterSettle(ctx, scope) {
|
|
811
740
|
const hook = ctx.routeEntry.settlement?.afterSettle;
|
|
812
741
|
if (!hook) return;
|
|
@@ -814,81 +743,145 @@ async function runAfterSettle(ctx, scope) {
|
|
|
814
743
|
await hook(settlementContext(ctx, scope));
|
|
815
744
|
} catch (error) {
|
|
816
745
|
const message = errorMessage(error, "Post-settlement hook failed");
|
|
817
|
-
|
|
818
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
819
|
-
level: "error",
|
|
820
|
-
message: `Post-settlement hook failed: ${message}`,
|
|
821
|
-
route: ctx.routeEntry.key
|
|
822
|
-
});
|
|
746
|
+
ctx.report("error", `Post-settlement hook failed: ${message}`);
|
|
823
747
|
await runSettlementError(ctx, scope, error, "afterSettle");
|
|
824
748
|
}
|
|
825
749
|
}
|
|
826
750
|
|
|
827
|
-
// src/pipeline/
|
|
828
|
-
async function
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
route: ctx.routeEntry.key
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// src/pipeline/context/grant-entitlement.ts
|
|
845
|
-
async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
846
|
-
if (!ctx.routeEntry.siwxEnabled) return;
|
|
847
|
-
try {
|
|
848
|
-
await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
|
|
849
|
-
} catch (error) {
|
|
850
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
851
|
-
level: "warn",
|
|
852
|
-
message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
853
|
-
route: ctx.routeEntry.key
|
|
854
|
-
});
|
|
855
|
-
}
|
|
751
|
+
// src/pipeline/steps/finalize/epilogue.ts
|
|
752
|
+
async function runPostSettleEpilogue(args) {
|
|
753
|
+
const { ctx, strategy, wallet, settle, afterSettleScope, rawResult, body } = args;
|
|
754
|
+
await grantEntitlementIfSiwx(ctx, wallet);
|
|
755
|
+
firePaymentSettled(ctx, {
|
|
756
|
+
protocol: strategy.protocol,
|
|
757
|
+
payer: wallet,
|
|
758
|
+
transaction: settle.settledPayment.transaction ?? "",
|
|
759
|
+
network: settle.settledPayment.network
|
|
760
|
+
});
|
|
761
|
+
await runAfterSettle(ctx, afterSettleScope);
|
|
762
|
+
return finalize(ctx, settle.response, rawResult, body);
|
|
856
763
|
}
|
|
857
764
|
|
|
858
|
-
// src/pipeline/
|
|
859
|
-
async function
|
|
860
|
-
const { ctx, strategy, verifyOutcome, scope, rawResult, body, onSettleError } = args;
|
|
861
|
-
const { request, routeEntry, deps } = ctx;
|
|
765
|
+
// src/pipeline/steps/finalize/request.ts
|
|
766
|
+
async function settleAndFinalizeRequest(args) {
|
|
767
|
+
const { ctx, strategy, verifyOutcome, scope, rawResult, body, billedAmount, onSettleError } = args;
|
|
768
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
862
769
|
const settle = await strategy.settle({
|
|
863
770
|
request,
|
|
864
771
|
response: scope.response,
|
|
865
772
|
payment: verifyOutcome.payment,
|
|
866
773
|
token: verifyOutcome.token,
|
|
867
774
|
routeEntry,
|
|
868
|
-
deps
|
|
775
|
+
deps,
|
|
776
|
+
billedAmount,
|
|
777
|
+
report
|
|
869
778
|
});
|
|
870
779
|
if (!settle.ok) {
|
|
871
780
|
if (onSettleError) await onSettleError(settle.error, settle.failMessage);
|
|
872
781
|
return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
|
|
873
782
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
783
|
+
return runPostSettleEpilogue({
|
|
784
|
+
ctx,
|
|
785
|
+
strategy,
|
|
786
|
+
wallet: verifyOutcome.wallet,
|
|
787
|
+
settle,
|
|
788
|
+
afterSettleScope: {
|
|
789
|
+
...scope,
|
|
790
|
+
payment: settle.settledPayment,
|
|
791
|
+
response: settle.response
|
|
792
|
+
},
|
|
793
|
+
rawResult,
|
|
794
|
+
body
|
|
885
795
|
});
|
|
886
|
-
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/pipeline/steps/finalize/stream.ts
|
|
799
|
+
async function settleAndFinalizeStream(args) {
|
|
800
|
+
const { ctx, strategy, verifyOutcome, source, account, body, bindChannelCharge } = args;
|
|
801
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
802
|
+
if (!strategy.settleStream) {
|
|
803
|
+
return fail(ctx, 500, `${strategy.protocol} does not support streaming handlers`, body);
|
|
804
|
+
}
|
|
805
|
+
const settle = await strategy.settleStream({
|
|
806
|
+
request,
|
|
807
|
+
source,
|
|
808
|
+
payment: verifyOutcome.payment,
|
|
809
|
+
token: verifyOutcome.token,
|
|
810
|
+
routeEntry,
|
|
811
|
+
deps,
|
|
812
|
+
bindChannelCharge,
|
|
813
|
+
report
|
|
814
|
+
});
|
|
815
|
+
if (!settle.ok) {
|
|
816
|
+
return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
|
|
817
|
+
}
|
|
818
|
+
return runPostSettleEpilogue({
|
|
819
|
+
ctx,
|
|
820
|
+
strategy,
|
|
821
|
+
wallet: verifyOutcome.wallet,
|
|
822
|
+
settle,
|
|
823
|
+
afterSettleScope: {
|
|
824
|
+
wallet: verifyOutcome.wallet,
|
|
825
|
+
account,
|
|
826
|
+
body,
|
|
827
|
+
payment: settle.settledPayment,
|
|
828
|
+
response: settle.response,
|
|
829
|
+
rawResult: void 0
|
|
830
|
+
},
|
|
831
|
+
rawResult: void 0,
|
|
832
|
+
body
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/pipeline/steps/run-handler-only.ts
|
|
837
|
+
async function runHandlerOnly(ctx, wallet, account) {
|
|
838
|
+
const body = await parseBody(ctx);
|
|
839
|
+
if (!body.ok) return body.response;
|
|
840
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
841
|
+
if (validateErr) return validateErr;
|
|
842
|
+
const result = await invokeUnauthed(ctx, wallet, account, body.data);
|
|
843
|
+
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/pipeline/steps/run-before-settle.ts
|
|
847
|
+
async function runBeforeSettle(ctx, scope) {
|
|
848
|
+
const hook = ctx.routeEntry.settlement?.beforeSettle;
|
|
849
|
+
if (!hook) return null;
|
|
850
|
+
try {
|
|
851
|
+
await hook(settlementContext(ctx, scope));
|
|
852
|
+
return null;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
return fail(
|
|
855
|
+
ctx,
|
|
856
|
+
errorStatus(error, 500),
|
|
857
|
+
errorMessage(error, "Pre-settlement validation failed"),
|
|
858
|
+
scope.body
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/pipeline/steps/run-settled-handler-error.ts
|
|
864
|
+
async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
|
|
865
|
+
const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
|
|
866
|
+
if (!hook) return;
|
|
867
|
+
try {
|
|
868
|
+
await hook({ ...settlementContext(ctx, scope), error });
|
|
869
|
+
} catch (hookError) {
|
|
870
|
+
const message = errorMessage(hookError, "Settled handler error hook failed");
|
|
871
|
+
ctx.report("error", `Settled handler error hook failed: ${message}`);
|
|
872
|
+
}
|
|
887
873
|
}
|
|
888
874
|
|
|
889
875
|
// src/auth/normalize-wallet.ts
|
|
890
876
|
function normalizeWalletAddress(address) {
|
|
891
|
-
|
|
877
|
+
const isEvm = /^0x/i.test(address);
|
|
878
|
+
return isEvm ? normalizeEvmWalletAddress(address) : normalizeSolanaWalletAddress(address);
|
|
879
|
+
}
|
|
880
|
+
function normalizeEvmWalletAddress(address) {
|
|
881
|
+
return address.toLowerCase();
|
|
882
|
+
}
|
|
883
|
+
function normalizeSolanaWalletAddress(address) {
|
|
884
|
+
return address;
|
|
892
885
|
}
|
|
893
886
|
|
|
894
887
|
// src/auth/siwx.ts
|
|
@@ -941,7 +934,7 @@ async function buildSIWXExtension() {
|
|
|
941
934
|
return declareSIWxExtension();
|
|
942
935
|
}
|
|
943
936
|
|
|
944
|
-
// src/pipeline/
|
|
937
|
+
// src/pipeline/steps/try-siwx-fast-path.ts
|
|
945
938
|
async function trySiwxFastPath(ctx, account) {
|
|
946
939
|
const { request, routeEntry, deps } = ctx;
|
|
947
940
|
if (!routeEntry.siwxEnabled) return null;
|
|
@@ -953,35 +946,29 @@ async function trySiwxFastPath(ctx, account) {
|
|
|
953
946
|
ctx.pluginCtx.setVerifiedWallet(wallet);
|
|
954
947
|
const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
|
|
955
948
|
if (!entitled) return null;
|
|
956
|
-
|
|
957
|
-
authMode: "siwx",
|
|
958
|
-
wallet,
|
|
959
|
-
route: routeEntry.key
|
|
960
|
-
});
|
|
949
|
+
fireAuthVerified(ctx, { authMode: "siwx", wallet });
|
|
961
950
|
return runHandlerOnly(ctx, wallet, account);
|
|
962
951
|
}
|
|
963
952
|
|
|
964
|
-
// src/pipeline/
|
|
953
|
+
// src/pipeline/steps/should-parse-body-early.ts
|
|
965
954
|
function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
|
|
966
955
|
if (incomingStrategy) return false;
|
|
967
956
|
if (!routeEntry.bodySchema) return false;
|
|
968
957
|
return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
|
|
969
958
|
}
|
|
970
959
|
|
|
971
|
-
// src/pipeline/
|
|
972
|
-
function
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
if (errors.length === 0) return null;
|
|
984
|
-
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
960
|
+
// src/pipeline/steps/resolve-early-body.ts
|
|
961
|
+
async function resolveEarlyBody(args) {
|
|
962
|
+
const { ctx, pricing, incomingStrategy } = args;
|
|
963
|
+
if (!shouldParseBodyEarly(incomingStrategy, ctx.routeEntry, pricing)) {
|
|
964
|
+
return { ok: true, earlyBody: void 0 };
|
|
965
|
+
}
|
|
966
|
+
const earlyClone = ctx.request.clone();
|
|
967
|
+
const earlyResult = await parseBody(ctx, earlyClone);
|
|
968
|
+
if (!earlyResult.ok) return { ok: false, response: earlyResult.response };
|
|
969
|
+
const validateErr = await runValidate(ctx, earlyResult.data);
|
|
970
|
+
if (validateErr) return { ok: false, response: validateErr };
|
|
971
|
+
return { ok: true, earlyBody: earlyResult.data };
|
|
985
972
|
}
|
|
986
973
|
|
|
987
974
|
// src/auth/api-key.ts
|
|
@@ -1002,6 +989,34 @@ function extractBearerToken(header) {
|
|
|
1002
989
|
return null;
|
|
1003
990
|
}
|
|
1004
991
|
|
|
992
|
+
// src/pipeline/steps/run-api-key-gate.ts
|
|
993
|
+
async function runApiKeyGate(ctx) {
|
|
994
|
+
const { request, routeEntry } = ctx;
|
|
995
|
+
if (!routeEntry.apiKeyResolver) return { ok: true, account: void 0 };
|
|
996
|
+
const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
|
|
997
|
+
if (!apiKeyResult.valid) {
|
|
998
|
+
return { ok: false, response: fail(ctx, 401, "Invalid or missing API key") };
|
|
999
|
+
}
|
|
1000
|
+
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: apiKeyResult.account });
|
|
1001
|
+
return { ok: true, account: apiKeyResult.account };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/pipeline/steps/protocol-init-error.ts
|
|
1005
|
+
function protocolInitError(routeEntry, deps) {
|
|
1006
|
+
if (!routeEntry.pricing) return null;
|
|
1007
|
+
const errors = [];
|
|
1008
|
+
for (const protocol of routeEntry.protocols) {
|
|
1009
|
+
if (protocol === "x402" && deps.x402InitError) {
|
|
1010
|
+
errors.push(`x402: ${deps.x402InitError}`);
|
|
1011
|
+
}
|
|
1012
|
+
if (protocol === "mpp" && deps.mppInitError) {
|
|
1013
|
+
errors.push(`mpp: ${deps.mppInitError}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (errors.length === 0) return null;
|
|
1017
|
+
return `Payment protocol initialization failed. ${errors.join("; ")}`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1005
1020
|
// src/pipeline/flows/api-key-only.ts
|
|
1006
1021
|
async function runApiKeyOnlyFlow(ctx) {
|
|
1007
1022
|
if (!ctx.routeEntry.apiKeyResolver) {
|
|
@@ -1009,12 +1024,7 @@ async function runApiKeyOnlyFlow(ctx) {
|
|
|
1009
1024
|
}
|
|
1010
1025
|
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
1011
1026
|
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
1012
|
-
|
|
1013
|
-
authMode: "apiKey",
|
|
1014
|
-
wallet: null,
|
|
1015
|
-
route: ctx.routeEntry.key,
|
|
1016
|
-
account: result.account
|
|
1017
|
-
});
|
|
1027
|
+
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
|
|
1018
1028
|
return runHandlerOnly(ctx, null, result.account);
|
|
1019
1029
|
}
|
|
1020
1030
|
|
|
@@ -1171,13 +1181,22 @@ function selectPricing(raw, deps = {}) {
|
|
|
1171
1181
|
// src/protocols/mpp/credential.ts
|
|
1172
1182
|
import { Credential } from "mppx";
|
|
1173
1183
|
import { getAddress, isAddress } from "viem";
|
|
1184
|
+
var SESSION_ACTIONS = /* @__PURE__ */ new Set(["open", "topUp", "voucher", "close"]);
|
|
1174
1185
|
function readMppCredential(request) {
|
|
1175
1186
|
const credential = Credential.fromRequest(request);
|
|
1176
1187
|
if (!credential) return null;
|
|
1177
1188
|
const wallet = walletFromDid(credential.source ?? "");
|
|
1178
|
-
const
|
|
1189
|
+
const payload = credential.payload;
|
|
1190
|
+
const rawType = payload?.type;
|
|
1179
1191
|
const payloadType = rawType === "transaction" ? "transaction" : rawType === "hash" ? "hash" : "unknown";
|
|
1180
|
-
|
|
1192
|
+
const rawAction = payload?.action;
|
|
1193
|
+
const sessionAction = typeof rawAction === "string" && SESSION_ACTIONS.has(rawAction) ? rawAction : void 0;
|
|
1194
|
+
return {
|
|
1195
|
+
credential,
|
|
1196
|
+
wallet,
|
|
1197
|
+
payloadType,
|
|
1198
|
+
...sessionAction ? { sessionAction } : {}
|
|
1199
|
+
};
|
|
1181
1200
|
}
|
|
1182
1201
|
function walletFromDid(rawSource) {
|
|
1183
1202
|
const parts = rawSource.split(":");
|
|
@@ -1185,6 +1204,168 @@ function walletFromDid(rawSource) {
|
|
|
1185
1204
|
return normalizeWalletAddress(isAddress(last) ? getAddress(last) : rawSource);
|
|
1186
1205
|
}
|
|
1187
1206
|
|
|
1207
|
+
// src/protocols/mpp/session-mode.ts
|
|
1208
|
+
async function verifySessionMode(args, info) {
|
|
1209
|
+
const { request, deps, price, routeEntry } = args;
|
|
1210
|
+
if (!deps.mppx?.sessionRequest || !deps.mppx?.sessionStream || !deps.mppSessionConfig) {
|
|
1211
|
+
return {
|
|
1212
|
+
ok: false,
|
|
1213
|
+
kind: "config",
|
|
1214
|
+
message: "MPP sessions not configured on this server (set RouterConfig.mpp.session)"
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
const tickCost = routeEntry.tickCost;
|
|
1218
|
+
const unitType = routeEntry.unitType;
|
|
1219
|
+
const streaming = routeEntry.streaming === true;
|
|
1220
|
+
const middleware = streaming ? deps.mppx.sessionStream : deps.mppx.sessionRequest;
|
|
1221
|
+
const middlewareRequest = isChannelOnlyAction(info, request) ? new Request(request.url, { method: request.method, headers: request.headers }) : request;
|
|
1222
|
+
let result;
|
|
1223
|
+
try {
|
|
1224
|
+
result = await middleware({
|
|
1225
|
+
amount: tickCost,
|
|
1226
|
+
unitType,
|
|
1227
|
+
suggestedDeposit: price,
|
|
1228
|
+
...streaming ? { meta: { streaming: "true" } } : {}
|
|
1229
|
+
})(middlewareRequest);
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1232
|
+
return {
|
|
1233
|
+
ok: false,
|
|
1234
|
+
kind: "config",
|
|
1235
|
+
message: `MPP session verify failed: ${message}`
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
if (result.status === 402) {
|
|
1239
|
+
const failure = await readMppxProblemDetails(result.challenge);
|
|
1240
|
+
return { ok: false, kind: "invalid", failure };
|
|
1241
|
+
}
|
|
1242
|
+
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1243
|
+
const payment = {
|
|
1244
|
+
protocol: "mpp",
|
|
1245
|
+
status: "verified",
|
|
1246
|
+
payer: info.wallet,
|
|
1247
|
+
amount: price,
|
|
1248
|
+
network: "tempo:4217",
|
|
1249
|
+
...mppRecipient ? { recipient: mppRecipient } : {}
|
|
1250
|
+
};
|
|
1251
|
+
const token = {
|
|
1252
|
+
mode: "session",
|
|
1253
|
+
streaming,
|
|
1254
|
+
sessionResult: result,
|
|
1255
|
+
info,
|
|
1256
|
+
tickCost
|
|
1257
|
+
};
|
|
1258
|
+
return {
|
|
1259
|
+
ok: true,
|
|
1260
|
+
wallet: info.wallet,
|
|
1261
|
+
payment,
|
|
1262
|
+
token,
|
|
1263
|
+
alreadySettled: false
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async function settleSessionMode(args) {
|
|
1267
|
+
const { request, response, payment, token, billedAmount } = args;
|
|
1268
|
+
const sessionToken = token;
|
|
1269
|
+
if (isChannelOnlyAction(sessionToken.info, request)) {
|
|
1270
|
+
const wrapped2 = sessionToken.sessionResult.withReceipt(
|
|
1271
|
+
new Response(null, { status: 200 })
|
|
1272
|
+
);
|
|
1273
|
+
return {
|
|
1274
|
+
ok: true,
|
|
1275
|
+
response: wrapped2,
|
|
1276
|
+
settledPayment: { ...payment, status: "settled", amount: billedAmount }
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
if (sessionToken.streaming) {
|
|
1280
|
+
return {
|
|
1281
|
+
ok: false,
|
|
1282
|
+
error: new Error("streaming session content settled via request path"),
|
|
1283
|
+
failMessage: "streaming session content settled via request path",
|
|
1284
|
+
failStatus: 500
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
const wrapped = sessionToken.sessionResult.withReceipt(
|
|
1288
|
+
response
|
|
1289
|
+
);
|
|
1290
|
+
wrapped.headers.set("Cache-Control", "private");
|
|
1291
|
+
const receiptHeader = wrapped.headers.get(HEADERS.MPP_PAYMENT_RECEIPT) ?? void 0;
|
|
1292
|
+
const settledPayment = {
|
|
1293
|
+
...payment,
|
|
1294
|
+
status: "settled",
|
|
1295
|
+
amount: billedAmount,
|
|
1296
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1297
|
+
};
|
|
1298
|
+
return { ok: true, response: wrapped, settledPayment };
|
|
1299
|
+
}
|
|
1300
|
+
async function buildSessionChallenge(args) {
|
|
1301
|
+
const { request, deps, suggestedDeposit, routeEntry, report } = args;
|
|
1302
|
+
if (!deps.mppSessionConfig) return {};
|
|
1303
|
+
const streaming = routeEntry.streaming === true;
|
|
1304
|
+
const middleware = streaming ? deps.mppx?.sessionStream : deps.mppx?.sessionRequest;
|
|
1305
|
+
if (!middleware) return {};
|
|
1306
|
+
const tickCost = routeEntry.tickCost;
|
|
1307
|
+
const unitType = routeEntry.unitType;
|
|
1308
|
+
try {
|
|
1309
|
+
const result = await middleware({
|
|
1310
|
+
amount: tickCost,
|
|
1311
|
+
unitType,
|
|
1312
|
+
suggestedDeposit,
|
|
1313
|
+
...streaming ? { meta: { streaming: "true" } } : {}
|
|
1314
|
+
})(request);
|
|
1315
|
+
if (result.status === 402) {
|
|
1316
|
+
const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
1317
|
+
if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
|
|
1318
|
+
}
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
report(
|
|
1321
|
+
"warn",
|
|
1322
|
+
`MPP session challenge build failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1323
|
+
);
|
|
1324
|
+
throw err;
|
|
1325
|
+
}
|
|
1326
|
+
return {};
|
|
1327
|
+
}
|
|
1328
|
+
function isChannelOnlyAction(info, request) {
|
|
1329
|
+
const action = info.sessionAction;
|
|
1330
|
+
if (!action) return false;
|
|
1331
|
+
if (action === "close" || action === "topUp") return true;
|
|
1332
|
+
if ((action === "open" || action === "voucher") && !hasRequestBody(request)) return true;
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
async function readMppxProblemDetails(challenge) {
|
|
1336
|
+
let body;
|
|
1337
|
+
try {
|
|
1338
|
+
body = await challenge.clone().text();
|
|
1339
|
+
} catch {
|
|
1340
|
+
return { reason: "mpp_session_invalid" };
|
|
1341
|
+
}
|
|
1342
|
+
if (!body) return { reason: "mpp_session_invalid" };
|
|
1343
|
+
let parsed;
|
|
1344
|
+
try {
|
|
1345
|
+
parsed = JSON.parse(body);
|
|
1346
|
+
} catch {
|
|
1347
|
+
return { reason: "mpp_session_invalid", message: body.slice(0, 500) };
|
|
1348
|
+
}
|
|
1349
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1350
|
+
return { reason: "mpp_session_invalid" };
|
|
1351
|
+
}
|
|
1352
|
+
const details = parsed;
|
|
1353
|
+
const typeUri = typeof details.type === "string" ? details.type : void 0;
|
|
1354
|
+
const slug = typeUri ? typeUri.split("/").pop() : void 0;
|
|
1355
|
+
const reason = slug ? slug.replace(/-/g, "_") : "mpp_session_invalid";
|
|
1356
|
+
const message = typeof details.detail === "string" && details.detail.length > 0 ? details.detail : typeof details.title === "string" ? details.title : void 0;
|
|
1357
|
+
return message ? { reason, message } : { reason };
|
|
1358
|
+
}
|
|
1359
|
+
function hasRequestBody(request) {
|
|
1360
|
+
const cl = request.headers.get("content-length");
|
|
1361
|
+
if (cl !== null) {
|
|
1362
|
+
const n = Number.parseInt(cl.trim(), 10);
|
|
1363
|
+
return Number.isFinite(n) && n > 0;
|
|
1364
|
+
}
|
|
1365
|
+
if (request.headers.get("transfer-encoding") !== null) return true;
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1188
1369
|
// src/protocols/mpp/transaction-mode.ts
|
|
1189
1370
|
import { Transaction as TempoTransaction } from "viem/tempo";
|
|
1190
1371
|
import { call as viemCall } from "viem/actions";
|
|
@@ -1212,7 +1393,7 @@ async function readChallengeReason(challenge) {
|
|
|
1212
1393
|
|
|
1213
1394
|
// src/protocols/mpp/transaction-mode.ts
|
|
1214
1395
|
async function verifyTxMode(args, info) {
|
|
1215
|
-
const { deps, price,
|
|
1396
|
+
const { deps, price, report } = args;
|
|
1216
1397
|
if (!deps.tempoClient) {
|
|
1217
1398
|
return {
|
|
1218
1399
|
ok: false,
|
|
@@ -1230,7 +1411,7 @@ async function verifyTxMode(args, info) {
|
|
|
1230
1411
|
});
|
|
1231
1412
|
} catch (err) {
|
|
1232
1413
|
const message = err instanceof Error ? err.message : String(err);
|
|
1233
|
-
|
|
1414
|
+
report("warn", `MPP simulation failed: ${message}`);
|
|
1234
1415
|
return { ok: false, kind: "invalid" };
|
|
1235
1416
|
}
|
|
1236
1417
|
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
@@ -1251,7 +1432,7 @@ async function verifyTxMode(args, info) {
|
|
|
1251
1432
|
};
|
|
1252
1433
|
}
|
|
1253
1434
|
async function settleTxMode(args) {
|
|
1254
|
-
const { request, response, payment, deps,
|
|
1435
|
+
const { request, response, payment, deps, report } = args;
|
|
1255
1436
|
if (!deps.mppx) {
|
|
1256
1437
|
return {
|
|
1257
1438
|
ok: false,
|
|
@@ -1265,7 +1446,7 @@ async function settleTxMode(args) {
|
|
|
1265
1446
|
result = await deps.mppx.charge({ amount: payment.amount })(request);
|
|
1266
1447
|
} catch (err) {
|
|
1267
1448
|
const message = err instanceof Error ? err.message : String(err);
|
|
1268
|
-
|
|
1449
|
+
report("error", `MPP broadcast failed after handler: ${message}`);
|
|
1269
1450
|
return {
|
|
1270
1451
|
ok: false,
|
|
1271
1452
|
error: err,
|
|
@@ -1282,7 +1463,7 @@ async function settleTxMode(args) {
|
|
|
1282
1463
|
mppResult: result,
|
|
1283
1464
|
challenge: result.challenge
|
|
1284
1465
|
});
|
|
1285
|
-
|
|
1466
|
+
report("error", `MPP payment failed after handler: ${detail}`);
|
|
1286
1467
|
return {
|
|
1287
1468
|
ok: false,
|
|
1288
1469
|
error: settlementError,
|
|
@@ -1305,10 +1486,10 @@ async function settleTxMode(args) {
|
|
|
1305
1486
|
|
|
1306
1487
|
// src/protocols/mpp/hash-mode.ts
|
|
1307
1488
|
async function verifyHashMode(args, info) {
|
|
1308
|
-
const { deps, price,
|
|
1489
|
+
const { deps, price, request, report } = args;
|
|
1309
1490
|
if (!deps.mppx) {
|
|
1310
1491
|
const reason = deps.mppInitError ? `MPP initialization failed: ${deps.mppInitError}` : "MPP not initialized \u2014 ensure mppx is installed and mpp config (secretKey, currency, recipient) is correct";
|
|
1311
|
-
|
|
1492
|
+
report("error", reason);
|
|
1312
1493
|
return { ok: false, kind: "config", message: reason };
|
|
1313
1494
|
}
|
|
1314
1495
|
let chargeResult;
|
|
@@ -1316,13 +1497,13 @@ async function verifyHashMode(args, info) {
|
|
|
1316
1497
|
chargeResult = await deps.mppx.charge({ amount: price })(request);
|
|
1317
1498
|
} catch (err) {
|
|
1318
1499
|
const message = err instanceof Error ? err.message : String(err);
|
|
1319
|
-
|
|
1500
|
+
report("error", `MPP charge failed: ${message}`);
|
|
1320
1501
|
return { ok: false, kind: "config", message: `MPP payment processing failed: ${message}` };
|
|
1321
1502
|
}
|
|
1322
1503
|
if (chargeResult.status === 402) {
|
|
1323
1504
|
const reason = await readChallengeReason(chargeResult.challenge);
|
|
1324
1505
|
const detail = reason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
|
|
1325
|
-
|
|
1506
|
+
report("warn", `MPP credential rejected: ${detail}`);
|
|
1326
1507
|
return { ok: false, kind: "invalid" };
|
|
1327
1508
|
}
|
|
1328
1509
|
const receiptHeader = chargeResult.withReceipt(new Response()).headers.get(
|
|
@@ -1367,9 +1548,20 @@ var mppStrategy = {
|
|
|
1367
1548
|
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
1368
1549
|
return Boolean(auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT));
|
|
1369
1550
|
},
|
|
1551
|
+
preflight(request, _routeEntry) {
|
|
1552
|
+
const info = readMppCredential(request);
|
|
1553
|
+
if (!info?.sessionAction) return null;
|
|
1554
|
+
if (!isChannelOnlyAction(info, request)) return null;
|
|
1555
|
+
return { skipBody: true, skipHandler: true };
|
|
1556
|
+
},
|
|
1370
1557
|
async verify(args) {
|
|
1371
1558
|
const info = readMppCredential(args.request);
|
|
1372
1559
|
if (!info) return { ok: false, kind: "invalid" };
|
|
1560
|
+
if (args.routeEntry.dynamicPrice) {
|
|
1561
|
+
if (!info.sessionAction) return { ok: false, kind: "invalid" };
|
|
1562
|
+
return verifySessionMode(args, info);
|
|
1563
|
+
}
|
|
1564
|
+
if (info.sessionAction) return { ok: false, kind: "invalid" };
|
|
1373
1565
|
if (info.payloadType === "transaction" && args.deps.tempoClient) {
|
|
1374
1566
|
return verifyTxMode(args, info);
|
|
1375
1567
|
}
|
|
@@ -1377,28 +1569,88 @@ var mppStrategy = {
|
|
|
1377
1569
|
},
|
|
1378
1570
|
async settle(args) {
|
|
1379
1571
|
const token = args.token;
|
|
1572
|
+
if (token.mode === "session") return settleSessionMode(args);
|
|
1380
1573
|
if (token.mode === "transaction") return settleTxMode(args);
|
|
1381
1574
|
return settleHashMode(args);
|
|
1382
1575
|
},
|
|
1576
|
+
async settleStream(args) {
|
|
1577
|
+
const token = args.token;
|
|
1578
|
+
if (token.mode !== "session" || !token.streaming) {
|
|
1579
|
+
return {
|
|
1580
|
+
ok: false,
|
|
1581
|
+
error: new Error("streaming requires a streaming-mode MPP session credential"),
|
|
1582
|
+
failMessage: "streaming requires a streaming-mode MPP session credential",
|
|
1583
|
+
failStatus: 400
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
const sessionToken = token;
|
|
1587
|
+
const sseResult = sessionToken.sessionResult;
|
|
1588
|
+
const { bindChannelCharge, source: handlerStream } = args;
|
|
1589
|
+
async function* forwardHandlerStreamWithChannelDebit(channel) {
|
|
1590
|
+
bindChannelCharge(channel.charge);
|
|
1591
|
+
try {
|
|
1592
|
+
for await (const chunk of handlerStream) {
|
|
1593
|
+
yield typeof chunk === "string" ? chunk : JSON.stringify(chunk);
|
|
1594
|
+
}
|
|
1595
|
+
} finally {
|
|
1596
|
+
bindChannelCharge(null);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const sse = sseResult.withReceipt(forwardHandlerStreamWithChannelDebit);
|
|
1600
|
+
sse.headers.set("Cache-Control", "private");
|
|
1601
|
+
const settledPayment = {
|
|
1602
|
+
...args.payment,
|
|
1603
|
+
status: "settled",
|
|
1604
|
+
amount: args.payment.amount
|
|
1605
|
+
};
|
|
1606
|
+
return { ok: true, response: sse, settledPayment };
|
|
1607
|
+
},
|
|
1383
1608
|
async buildChallenge(args) {
|
|
1384
1609
|
if (!args.deps.mppx) return {};
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
);
|
|
1395
|
-
throw err;
|
|
1610
|
+
const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
|
|
1611
|
+
if (args.routeEntry.dynamicPrice && sessionsConfigured) {
|
|
1612
|
+
const tickCost = args.routeEntry.tickCost;
|
|
1613
|
+
const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
|
|
1614
|
+
const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
|
|
1615
|
+
return buildSessionChallenge({
|
|
1616
|
+
...args,
|
|
1617
|
+
suggestedDeposit
|
|
1618
|
+
});
|
|
1396
1619
|
}
|
|
1397
|
-
return
|
|
1620
|
+
return buildChargeChallenge(args);
|
|
1398
1621
|
}
|
|
1399
1622
|
};
|
|
1623
|
+
function multiplyDecimal(decimal, factor) {
|
|
1624
|
+
if (!Number.isFinite(factor) || factor <= 0) return decimal;
|
|
1625
|
+
const [whole, fraction = ""] = decimal.split(".");
|
|
1626
|
+
const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
|
|
1627
|
+
const decimals = fraction.length;
|
|
1628
|
+
if (decimals === 0) return scaled;
|
|
1629
|
+
const padded = scaled.padStart(decimals + 1, "0");
|
|
1630
|
+
const intPart = padded.slice(0, padded.length - decimals);
|
|
1631
|
+
const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
|
|
1632
|
+
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
1633
|
+
}
|
|
1634
|
+
async function buildChargeChallenge(args) {
|
|
1635
|
+
if (!args.deps.mppx) return {};
|
|
1636
|
+
try {
|
|
1637
|
+
const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
|
|
1638
|
+
if (result.status === 402) {
|
|
1639
|
+
const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
|
|
1640
|
+
if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
|
|
1641
|
+
}
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
args.report(
|
|
1644
|
+
"warn",
|
|
1645
|
+
`MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1646
|
+
);
|
|
1647
|
+
throw err;
|
|
1648
|
+
}
|
|
1649
|
+
return {};
|
|
1650
|
+
}
|
|
1400
1651
|
|
|
1401
1652
|
// src/protocols/x402/strategy.ts
|
|
1653
|
+
import { PERMIT2_ADDRESS } from "@x402/evm";
|
|
1402
1654
|
init_accepts();
|
|
1403
1655
|
|
|
1404
1656
|
// src/protocols/x402/challenge.ts
|
|
@@ -1408,20 +1660,27 @@ init_solana();
|
|
|
1408
1660
|
// src/protocols/x402/requirements.ts
|
|
1409
1661
|
init_evm();
|
|
1410
1662
|
init_solana();
|
|
1411
|
-
async function buildExpectedRequirements(server, request, price, accepts) {
|
|
1412
|
-
const
|
|
1663
|
+
async function buildExpectedRequirements(server, request, price, accepts, report) {
|
|
1664
|
+
const sdkRequirements = await buildSdkHandledRequirements(
|
|
1665
|
+
server,
|
|
1666
|
+
request,
|
|
1667
|
+
price,
|
|
1668
|
+
accepts,
|
|
1669
|
+
report
|
|
1670
|
+
);
|
|
1413
1671
|
const customRequirements = buildCustomRequirements(price, accepts);
|
|
1414
|
-
return [...
|
|
1672
|
+
return [...sdkRequirements, ...customRequirements];
|
|
1415
1673
|
}
|
|
1416
|
-
async function
|
|
1417
|
-
const
|
|
1674
|
+
async function buildSdkHandledRequirements(server, request, price, accepts, report) {
|
|
1675
|
+
const groups = [
|
|
1418
1676
|
buildEvmExactOptions(accepts, price),
|
|
1677
|
+
buildEvmUptoOptions(accepts, price),
|
|
1419
1678
|
buildSolanaExactOptions(accepts, price)
|
|
1420
1679
|
].filter((options) => options.length > 0);
|
|
1421
|
-
if (
|
|
1680
|
+
if (groups.length === 0) return [];
|
|
1422
1681
|
const requirements = [];
|
|
1423
1682
|
const failures = [];
|
|
1424
|
-
for (const options of
|
|
1683
|
+
for (const options of groups) {
|
|
1425
1684
|
try {
|
|
1426
1685
|
requirements.push(
|
|
1427
1686
|
...await server.buildPaymentRequirementsFromOptions(options, { request })
|
|
@@ -1429,21 +1688,31 @@ async function buildExactRequirements(server, request, price, accepts) {
|
|
|
1429
1688
|
} catch (error) {
|
|
1430
1689
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1431
1690
|
failures.push(err);
|
|
1432
|
-
if (
|
|
1691
|
+
if (groups.length === 1) {
|
|
1433
1692
|
throw err;
|
|
1434
1693
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1694
|
+
report?.(
|
|
1695
|
+
"warn",
|
|
1696
|
+
`Failed to build x402 ${options[0]?.scheme} requirements for ${options[0]?.network}: ${err.message}`
|
|
1437
1697
|
);
|
|
1438
1698
|
}
|
|
1439
1699
|
}
|
|
1440
1700
|
if (requirements.length > 0) {
|
|
1441
1701
|
return requirements;
|
|
1442
1702
|
}
|
|
1443
|
-
throw failures[0] ?? new Error("Failed to build x402
|
|
1703
|
+
throw failures[0] ?? new Error("Failed to build x402 SDK-handled requirements");
|
|
1444
1704
|
}
|
|
1445
1705
|
function buildCustomRequirements(price, accepts) {
|
|
1446
|
-
return accepts.filter((accept) => accept
|
|
1706
|
+
return accepts.filter((accept) => !isSdkHandled(accept)).map((accept) => buildCustomRequirement(price, accept));
|
|
1707
|
+
}
|
|
1708
|
+
function isSdkHandled(accept) {
|
|
1709
|
+
if (isEvmNetwork(accept.network)) {
|
|
1710
|
+
return accept.scheme === "exact" || accept.scheme === "upto";
|
|
1711
|
+
}
|
|
1712
|
+
if (isSolanaRequirement({ network: accept.network })) {
|
|
1713
|
+
return accept.scheme === "exact";
|
|
1714
|
+
}
|
|
1715
|
+
return false;
|
|
1447
1716
|
}
|
|
1448
1717
|
function buildCustomRequirement(price, accept) {
|
|
1449
1718
|
if (!accept.asset) {
|
|
@@ -1477,7 +1746,7 @@ function decimalToAtomicUnits(amount, decimals) {
|
|
|
1477
1746
|
|
|
1478
1747
|
// src/protocols/x402/challenge.ts
|
|
1479
1748
|
async function buildX402Challenge(opts) {
|
|
1480
|
-
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
|
|
1749
|
+
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions, report } = opts;
|
|
1481
1750
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
1482
1751
|
const resource = buildChallengeResource(request, routeEntry);
|
|
1483
1752
|
const requirements = await buildChallengeRequirements(
|
|
@@ -1486,7 +1755,8 @@ async function buildX402Challenge(opts) {
|
|
|
1486
1755
|
price,
|
|
1487
1756
|
accepts,
|
|
1488
1757
|
resource,
|
|
1489
|
-
facilitatorsByNetwork
|
|
1758
|
+
facilitatorsByNetwork,
|
|
1759
|
+
report
|
|
1490
1760
|
);
|
|
1491
1761
|
const paymentRequired = await server.createPaymentRequiredResponse(
|
|
1492
1762
|
requirements,
|
|
@@ -1497,13 +1767,13 @@ async function buildX402Challenge(opts) {
|
|
|
1497
1767
|
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
1498
1768
|
return { encoded, requirements };
|
|
1499
1769
|
}
|
|
1500
|
-
async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
|
|
1501
|
-
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1770
|
+
async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork, report) {
|
|
1771
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
|
|
1502
1772
|
if (!needsFacilitatorEnrichment(accepts)) return requirements;
|
|
1503
|
-
return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
|
|
1773
|
+
return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report);
|
|
1504
1774
|
}
|
|
1505
1775
|
function needsFacilitatorEnrichment(accepts) {
|
|
1506
|
-
return
|
|
1776
|
+
return hasSolanaAccepts(accepts);
|
|
1507
1777
|
}
|
|
1508
1778
|
async function enrichGroup(group, resource) {
|
|
1509
1779
|
const accepted = await enrichRequirementsWithFacilitatorAccepts(
|
|
@@ -1518,7 +1788,7 @@ async function enrichGroup(group, resource) {
|
|
|
1518
1788
|
}
|
|
1519
1789
|
return accepted;
|
|
1520
1790
|
}
|
|
1521
|
-
async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
|
|
1791
|
+
async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report) {
|
|
1522
1792
|
const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
|
|
1523
1793
|
if (groups.length === 0) return requirements;
|
|
1524
1794
|
const results = await Promise.all(
|
|
@@ -1528,8 +1798,9 @@ async function enrichChallengeRequirements(requirements, resource, facilitatorsB
|
|
|
1528
1798
|
} catch (err) {
|
|
1529
1799
|
const label = group.facilitator.url ?? group.facilitator.network;
|
|
1530
1800
|
const reason = err instanceof Error ? err.message : String(err);
|
|
1531
|
-
|
|
1532
|
-
|
|
1801
|
+
report?.(
|
|
1802
|
+
"warn",
|
|
1803
|
+
`${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
|
|
1533
1804
|
);
|
|
1534
1805
|
return { success: false, group };
|
|
1535
1806
|
}
|
|
@@ -1582,7 +1853,7 @@ function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
|
|
|
1582
1853
|
return facilitator;
|
|
1583
1854
|
}
|
|
1584
1855
|
function requiresFacilitatorEnrichment(requirement) {
|
|
1585
|
-
return
|
|
1856
|
+
return isSolanaRequirement(requirement);
|
|
1586
1857
|
}
|
|
1587
1858
|
function buildChallengeResource(request, routeEntry) {
|
|
1588
1859
|
return {
|
|
@@ -1594,33 +1865,68 @@ function buildChallengeResource(request, routeEntry) {
|
|
|
1594
1865
|
}
|
|
1595
1866
|
|
|
1596
1867
|
// src/protocols/x402/settle.ts
|
|
1597
|
-
async function settleX402Payment(server, payload, requirements) {
|
|
1868
|
+
async function settleX402Payment(server, payload, requirements, amountOverride) {
|
|
1598
1869
|
const { encodePaymentResponseHeader } = await import("@x402/core/http");
|
|
1870
|
+
if (amountOverride?.amount !== void 0) {
|
|
1871
|
+
const upstreamTaggedAmount = tagBareDecimalAsDollars(amountOverride.amount);
|
|
1872
|
+
const result2 = await server.settlePayment(payload, requirements, void 0, void 0, {
|
|
1873
|
+
amount: upstreamTaggedAmount
|
|
1874
|
+
});
|
|
1875
|
+
return {
|
|
1876
|
+
encoded: encodePaymentResponseHeader(result2),
|
|
1877
|
+
result: result2
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1599
1880
|
const result = await server.settlePayment(payload, requirements);
|
|
1600
|
-
|
|
1601
|
-
|
|
1881
|
+
return {
|
|
1882
|
+
encoded: encodePaymentResponseHeader(result),
|
|
1883
|
+
result
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
function tagBareDecimalAsDollars(amount) {
|
|
1887
|
+
if (/^\d+\.\d+$/.test(amount)) return `$${amount}`;
|
|
1888
|
+
return amount;
|
|
1602
1889
|
}
|
|
1603
1890
|
|
|
1604
1891
|
// src/protocols/x402/verify.ts
|
|
1892
|
+
import { VerifyError } from "@x402/core/types";
|
|
1605
1893
|
async function verifyX402Payment(opts) {
|
|
1606
|
-
const { server, request, price, accepts } = opts;
|
|
1894
|
+
const { server, request, price, accepts, report } = opts;
|
|
1607
1895
|
const payload = await readPaymentPayload(request);
|
|
1608
1896
|
if (!payload) return null;
|
|
1609
|
-
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
1897
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
|
|
1610
1898
|
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
1899
|
+
const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
|
|
1611
1900
|
if (!matching) {
|
|
1612
|
-
return invalidPaymentVerification(
|
|
1901
|
+
return invalidPaymentVerification({
|
|
1902
|
+
reason: "requirements_mismatch",
|
|
1903
|
+
message: "Signed payment requirements did not match any server-built requirement",
|
|
1904
|
+
...accepted ? { accepted } : {}
|
|
1905
|
+
});
|
|
1613
1906
|
}
|
|
1614
1907
|
let verify;
|
|
1615
1908
|
try {
|
|
1616
1909
|
verify = await server.verifyPayment(payload, matching);
|
|
1617
1910
|
} catch (err) {
|
|
1618
|
-
|
|
1619
|
-
|
|
1911
|
+
if (err instanceof VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
|
|
1912
|
+
return invalidPaymentVerification({
|
|
1913
|
+
reason: err.invalidReason ?? "verify_error",
|
|
1914
|
+
...err.invalidMessage ? { message: err.invalidMessage } : {},
|
|
1915
|
+
...err.payer ? { payer: err.payer } : {},
|
|
1916
|
+
...accepted ? { accepted } : {}
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1620
1919
|
throw err;
|
|
1621
1920
|
}
|
|
1622
|
-
if (!verify.isValid)
|
|
1623
|
-
|
|
1921
|
+
if (!verify.isValid) {
|
|
1922
|
+
return invalidPaymentVerification({
|
|
1923
|
+
reason: verify.invalidReason ?? "unknown",
|
|
1924
|
+
...verify.invalidMessage ? { message: verify.invalidMessage } : {},
|
|
1925
|
+
...verify.payer ? { payer: verify.payer } : {},
|
|
1926
|
+
...accepted ? { accepted } : {}
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
if (!verify.payer) {
|
|
1624
1930
|
throw new Error("x402 verification succeeded without a payer address");
|
|
1625
1931
|
}
|
|
1626
1932
|
return {
|
|
@@ -1652,11 +1958,36 @@ async function readPaymentPayload(request) {
|
|
|
1652
1958
|
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
1653
1959
|
return decodePaymentSignatureHeader(paymentHeader);
|
|
1654
1960
|
}
|
|
1655
|
-
function invalidPaymentVerification() {
|
|
1656
|
-
return {
|
|
1961
|
+
function invalidPaymentVerification(failure) {
|
|
1962
|
+
return {
|
|
1963
|
+
valid: false,
|
|
1964
|
+
payload: null,
|
|
1965
|
+
requirements: null,
|
|
1966
|
+
payer: null,
|
|
1967
|
+
...failure ? { failure } : {}
|
|
1968
|
+
};
|
|
1657
1969
|
}
|
|
1658
1970
|
|
|
1659
1971
|
// src/protocols/x402/strategy.ts
|
|
1972
|
+
function formatVerifyFailureMessage(failure) {
|
|
1973
|
+
if (failure.reason === "permit2_allowance_required") {
|
|
1974
|
+
const wallet = failure.payer ?? "<the payer wallet>";
|
|
1975
|
+
const asset = failure.accepted?.asset ?? "<the asset>";
|
|
1976
|
+
const amount = failure.accepted?.amount ?? "<the required amount>";
|
|
1977
|
+
const network = failure.accepted?.network ?? "<the payment network>";
|
|
1978
|
+
return [
|
|
1979
|
+
`Payment rejected: In order for Upto to charge, the wallet ${wallet} MUST approve Permit2 to spend ${asset} on ${network}.`,
|
|
1980
|
+
`Required call (one-time, on-chain): ${asset}.approve(${PERMIT2_ADDRESS}, MAX_UINT256) from ${wallet}.`,
|
|
1981
|
+
`Permit2 contract address: ${PERMIT2_ADDRESS}.`,
|
|
1982
|
+
`Minimum allowance for this request: ${amount} (smallest units of ${asset}); use MAX_UINT256 to avoid re-approving on every future call.`,
|
|
1983
|
+
`Alternative without an on-chain transaction: the merchant can adopt the EIP-2612 gas-sponsoring extension (https://docs.x402.org/extensions/eip2612-gas-sponsoring).`
|
|
1984
|
+
].join(" ");
|
|
1985
|
+
}
|
|
1986
|
+
if (failure.message) {
|
|
1987
|
+
return `Payment rejected (${failure.reason}): ${failure.message}`;
|
|
1988
|
+
}
|
|
1989
|
+
return `Payment rejected: ${failure.reason}`;
|
|
1990
|
+
}
|
|
1660
1991
|
var x402Strategy = {
|
|
1661
1992
|
protocol: "x402",
|
|
1662
1993
|
detects(request) {
|
|
@@ -1664,129 +1995,123 @@ var x402Strategy = {
|
|
|
1664
1995
|
request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)
|
|
1665
1996
|
);
|
|
1666
1997
|
},
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
payment,
|
|
1703
|
-
token: {
|
|
1704
|
-
payload: verifyResult.payload,
|
|
1705
|
-
requirements: verifyResult.requirements
|
|
1706
|
-
}
|
|
1707
|
-
};
|
|
1708
|
-
},
|
|
1709
|
-
async settle(args) {
|
|
1710
|
-
const { response, payment, token, deps } = args;
|
|
1711
|
-
const x402Token = token;
|
|
1712
|
-
try {
|
|
1713
|
-
const settle = await settleX402Payment(
|
|
1714
|
-
deps.x402Server,
|
|
1715
|
-
x402Token.payload,
|
|
1716
|
-
x402Token.requirements
|
|
1717
|
-
);
|
|
1718
|
-
if (!settle.result?.success) {
|
|
1719
|
-
const reason = settle.result?.errorReason || "x402 settlement returned success=false";
|
|
1720
|
-
const error = new Error(reason);
|
|
1721
|
-
error.errorReason = reason;
|
|
1722
|
-
throw error;
|
|
1723
|
-
}
|
|
1724
|
-
response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
|
|
1725
|
-
response.headers.set("Cache-Control", "private");
|
|
1726
|
-
const transaction = String(settle.result?.transaction ?? "");
|
|
1727
|
-
const settledPayment = {
|
|
1728
|
-
...payment,
|
|
1729
|
-
status: "settled",
|
|
1730
|
-
...transaction ? { transaction } : {}
|
|
1998
|
+
verify: (args) => verifyX402(args),
|
|
1999
|
+
settle: (args) => settleX402(args),
|
|
2000
|
+
buildChallenge: (args) => buildX402ChallengeContribution(args)
|
|
2001
|
+
};
|
|
2002
|
+
async function verifyX402(args) {
|
|
2003
|
+
const { request, body, price, routeEntry, deps, report } = args;
|
|
2004
|
+
if (!deps.x402Server) {
|
|
2005
|
+
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";
|
|
2006
|
+
report("error", reason);
|
|
2007
|
+
return { ok: false, kind: "config", message: reason };
|
|
2008
|
+
}
|
|
2009
|
+
const accepts = await resolveX402Accepts(
|
|
2010
|
+
request,
|
|
2011
|
+
routeEntry,
|
|
2012
|
+
deps.x402Accepts,
|
|
2013
|
+
deps.payeeAddress,
|
|
2014
|
+
body
|
|
2015
|
+
);
|
|
2016
|
+
const verifyResult = await verifyX402Payment({
|
|
2017
|
+
server: deps.x402Server,
|
|
2018
|
+
request,
|
|
2019
|
+
price,
|
|
2020
|
+
accepts,
|
|
2021
|
+
report
|
|
2022
|
+
});
|
|
2023
|
+
if (!verifyResult?.valid) {
|
|
2024
|
+
const failure = verifyResult?.failure;
|
|
2025
|
+
if (failure) {
|
|
2026
|
+
return {
|
|
2027
|
+
ok: false,
|
|
2028
|
+
kind: "invalid",
|
|
2029
|
+
failure: {
|
|
2030
|
+
reason: failure.reason,
|
|
2031
|
+
message: formatVerifyFailureMessage(failure)
|
|
2032
|
+
}
|
|
1731
2033
|
};
|
|
1732
|
-
return { ok: true, response, settledPayment };
|
|
1733
|
-
} catch (err) {
|
|
1734
|
-
const errObj = err;
|
|
1735
|
-
console.error("Settlement failed", {
|
|
1736
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1737
|
-
route: args.routeEntry.key,
|
|
1738
|
-
network: payment.network,
|
|
1739
|
-
errorReason: errObj.errorReason,
|
|
1740
|
-
facilitatorStatus: errObj.response?.status,
|
|
1741
|
-
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
1742
|
-
});
|
|
1743
|
-
return { ok: false, error: err, failMessage: "Settlement failed" };
|
|
1744
2034
|
}
|
|
1745
|
-
|
|
1746
|
-
async buildChallenge(args) {
|
|
1747
|
-
const { request, routeEntry, body, price, extensions, deps } = args;
|
|
1748
|
-
if (!deps.x402Server) return {};
|
|
1749
|
-
const accepts = await resolveX402Accepts(
|
|
1750
|
-
request,
|
|
1751
|
-
routeEntry,
|
|
1752
|
-
deps.x402Accepts,
|
|
1753
|
-
deps.payeeAddress,
|
|
1754
|
-
body
|
|
1755
|
-
);
|
|
1756
|
-
const { encoded } = await buildX402Challenge({
|
|
1757
|
-
server: deps.x402Server,
|
|
1758
|
-
routeEntry,
|
|
1759
|
-
request,
|
|
1760
|
-
price,
|
|
1761
|
-
accepts,
|
|
1762
|
-
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
1763
|
-
extensions
|
|
1764
|
-
});
|
|
1765
|
-
return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
|
|
2035
|
+
return { ok: false, kind: "invalid" };
|
|
1766
2036
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
const
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
|
|
2037
|
+
const wallet = normalizeWalletAddress(verifyResult.payer);
|
|
2038
|
+
const { network, payTo } = verifyResult.requirements;
|
|
2039
|
+
const payment = {
|
|
2040
|
+
protocol: "x402",
|
|
2041
|
+
status: "verified",
|
|
2042
|
+
payer: wallet,
|
|
2043
|
+
amount: price,
|
|
2044
|
+
network,
|
|
2045
|
+
...payTo ? { recipient: payTo } : {}
|
|
2046
|
+
};
|
|
2047
|
+
return {
|
|
2048
|
+
ok: true,
|
|
2049
|
+
wallet,
|
|
2050
|
+
payment,
|
|
2051
|
+
token: {
|
|
2052
|
+
payload: verifyResult.payload,
|
|
2053
|
+
requirements: verifyResult.requirements
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
async function settleX402(args) {
|
|
2058
|
+
const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
|
|
2059
|
+
const { payload, requirements } = token;
|
|
2060
|
+
const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
|
|
2061
|
+
try {
|
|
2062
|
+
const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
|
|
2063
|
+
if (!settle.result?.success) {
|
|
2064
|
+
throw Object.assign(
|
|
2065
|
+
new Error(settle.result?.errorReason ?? "x402 settlement returned success=false"),
|
|
2066
|
+
{ errorReason: settle.result?.errorReason }
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
|
|
2070
|
+
response.headers.set("Cache-Control", "private");
|
|
2071
|
+
const transaction = String(settle.result.transaction ?? "");
|
|
2072
|
+
const settledPayment = {
|
|
2073
|
+
...payment,
|
|
2074
|
+
status: "settled",
|
|
2075
|
+
amount: billedAmount,
|
|
2076
|
+
...transaction ? { transaction } : {}
|
|
2077
|
+
};
|
|
2078
|
+
return { ok: true, response, settledPayment };
|
|
2079
|
+
} catch (err) {
|
|
2080
|
+
reportSettleFailure(report, err, payment.network);
|
|
2081
|
+
return { ok: false, error: err, failMessage: "Settlement failed" };
|
|
1788
2082
|
}
|
|
1789
|
-
|
|
2083
|
+
}
|
|
2084
|
+
async function buildX402ChallengeContribution(args) {
|
|
2085
|
+
const { request, routeEntry, body, price, extensions, deps, report } = args;
|
|
2086
|
+
if (!deps.x402Server) return {};
|
|
2087
|
+
const accepts = await resolveX402Accepts(
|
|
2088
|
+
request,
|
|
2089
|
+
routeEntry,
|
|
2090
|
+
deps.x402Accepts,
|
|
2091
|
+
deps.payeeAddress,
|
|
2092
|
+
body
|
|
2093
|
+
);
|
|
2094
|
+
const { encoded } = await buildX402Challenge({
|
|
2095
|
+
server: deps.x402Server,
|
|
2096
|
+
routeEntry,
|
|
2097
|
+
request,
|
|
2098
|
+
price,
|
|
2099
|
+
accepts,
|
|
2100
|
+
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
2101
|
+
extensions,
|
|
2102
|
+
report
|
|
2103
|
+
});
|
|
2104
|
+
return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
|
|
2105
|
+
}
|
|
2106
|
+
function reportSettleFailure(report, err, network) {
|
|
2107
|
+
const facilitator = err ?? {};
|
|
2108
|
+
report("error", "Settlement failed", {
|
|
2109
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2110
|
+
network,
|
|
2111
|
+
errorReason: facilitator.errorReason,
|
|
2112
|
+
facilitatorStatus: facilitator.response?.status,
|
|
2113
|
+
facilitatorBody: facilitator.response?.data ?? facilitator.response?.body
|
|
2114
|
+
});
|
|
1790
2115
|
}
|
|
1791
2116
|
|
|
1792
2117
|
// src/protocols/index.ts
|
|
@@ -1805,9 +2130,59 @@ function getAllowedStrategies(allowed) {
|
|
|
1805
2130
|
return allowed.map((name) => STRATEGIES[name]);
|
|
1806
2131
|
}
|
|
1807
2132
|
|
|
1808
|
-
// src/pipeline/
|
|
2133
|
+
// src/pipeline/flows/build402.ts
|
|
1809
2134
|
import { NextResponse as NextResponse4 } from "next/server";
|
|
1810
|
-
|
|
2135
|
+
|
|
2136
|
+
// src/pipeline/challenge-extensions.ts
|
|
2137
|
+
async function buildChallengeExtensions(ctx) {
|
|
2138
|
+
const { routeEntry } = ctx;
|
|
2139
|
+
let extensions;
|
|
2140
|
+
try {
|
|
2141
|
+
const { z: z2 } = await import("zod");
|
|
2142
|
+
const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
|
|
2143
|
+
const toJSON = (schema) => z2.toJSONSchema(schema, {
|
|
2144
|
+
target: "draft-2020-12",
|
|
2145
|
+
unrepresentable: "any"
|
|
2146
|
+
});
|
|
2147
|
+
const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
|
|
2148
|
+
const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
|
|
2149
|
+
if (inputSchema) {
|
|
2150
|
+
const config = {
|
|
2151
|
+
method: routeEntry.method,
|
|
2152
|
+
bodyType: routeEntry.bodySchema ? "json" : void 0,
|
|
2153
|
+
inputSchema
|
|
2154
|
+
};
|
|
2155
|
+
if (routeEntry.inputExample !== void 0) {
|
|
2156
|
+
config.input = routeEntry.inputExample;
|
|
2157
|
+
}
|
|
2158
|
+
if (outputSchema && routeEntry.outputExample !== void 0) {
|
|
2159
|
+
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
2160
|
+
}
|
|
2161
|
+
extensions = declareDiscoveryExtension(config);
|
|
2162
|
+
}
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
ctx.report(
|
|
2165
|
+
"warn",
|
|
2166
|
+
`Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
if (routeEntry.siwxEnabled) {
|
|
2170
|
+
try {
|
|
2171
|
+
const siwxExtension = await buildSIWXExtension();
|
|
2172
|
+
if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
|
|
2173
|
+
extensions = {
|
|
2174
|
+
...extensions ?? {},
|
|
2175
|
+
...siwxExtension
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
} catch {
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return extensions;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/pipeline/flows/build402.ts
|
|
2185
|
+
async function build402(ctx, pricing, body, failure) {
|
|
1811
2186
|
let challengePrice;
|
|
1812
2187
|
try {
|
|
1813
2188
|
challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
|
|
@@ -1821,7 +2196,8 @@ async function build402(ctx, pricing, body) {
|
|
|
1821
2196
|
return errorResponse;
|
|
1822
2197
|
}
|
|
1823
2198
|
const extensions = await buildChallengeExtensions(ctx);
|
|
1824
|
-
const
|
|
2199
|
+
const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
|
|
2200
|
+
const response = new NextResponse4(responseBody, {
|
|
1825
2201
|
status: 402,
|
|
1826
2202
|
headers: {
|
|
1827
2203
|
"Content-Type": "application/json",
|
|
@@ -1836,7 +2212,8 @@ async function build402(ctx, pricing, body) {
|
|
|
1836
2212
|
body,
|
|
1837
2213
|
price: challengePrice,
|
|
1838
2214
|
extensions,
|
|
1839
|
-
deps: ctx.deps
|
|
2215
|
+
deps: ctx.deps,
|
|
2216
|
+
report: ctx.report
|
|
1840
2217
|
});
|
|
1841
2218
|
if (contribution.headers) {
|
|
1842
2219
|
for (const [name, value] of Object.entries(contribution.headers)) {
|
|
@@ -1845,11 +2222,7 @@ async function build402(ctx, pricing, body) {
|
|
|
1845
2222
|
}
|
|
1846
2223
|
} catch (err) {
|
|
1847
2224
|
const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
|
|
1848
|
-
|
|
1849
|
-
level: "critical",
|
|
1850
|
-
message,
|
|
1851
|
-
route: ctx.routeEntry.key
|
|
1852
|
-
});
|
|
2225
|
+
ctx.report("critical", message);
|
|
1853
2226
|
if (strategy.protocol === "x402") {
|
|
1854
2227
|
const errorResponse = NextResponse4.json(
|
|
1855
2228
|
{ success: false, error: message },
|
|
@@ -1863,97 +2236,470 @@ async function build402(ctx, pricing, body) {
|
|
|
1863
2236
|
firePluginResponse(ctx, response);
|
|
1864
2237
|
return response;
|
|
1865
2238
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2239
|
+
|
|
2240
|
+
// src/pipeline/flows/dynamic/dynamic-body-and-price.ts
|
|
2241
|
+
async function resolveDynamicBodyAndPrice(args) {
|
|
2242
|
+
const { ctx, pricing, skipBody } = args;
|
|
2243
|
+
if (skipBody) {
|
|
2244
|
+
return {
|
|
2245
|
+
ok: true,
|
|
2246
|
+
parsedBody: void 0,
|
|
2247
|
+
price: surrogatePriceForSkippedBody(ctx.routeEntry)
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
const body = await parseBody(ctx);
|
|
2251
|
+
if (!body.ok) return { ok: false, response: body.response };
|
|
2252
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
2253
|
+
if (validateErr) {
|
|
2254
|
+
return { ok: false, response: validateErr };
|
|
2255
|
+
}
|
|
2256
|
+
if (!pricing) {
|
|
2257
|
+
return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
|
|
2258
|
+
}
|
|
1869
2259
|
try {
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
const toJSON = (schema) => z.toJSONSchema(schema, {
|
|
1873
|
-
target: "draft-2020-12",
|
|
1874
|
-
unrepresentable: "any"
|
|
1875
|
-
});
|
|
1876
|
-
const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
|
|
1877
|
-
const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
|
|
1878
|
-
if (inputSchema) {
|
|
1879
|
-
const config = {
|
|
1880
|
-
method: routeEntry.method,
|
|
1881
|
-
bodyType: routeEntry.bodySchema ? "json" : void 0,
|
|
1882
|
-
inputSchema
|
|
1883
|
-
};
|
|
1884
|
-
if (routeEntry.inputExample !== void 0) {
|
|
1885
|
-
config.input = routeEntry.inputExample;
|
|
1886
|
-
}
|
|
1887
|
-
if (outputSchema && routeEntry.outputExample !== void 0) {
|
|
1888
|
-
config.output = { schema: outputSchema, example: routeEntry.outputExample };
|
|
1889
|
-
}
|
|
1890
|
-
extensions = declareDiscoveryExtension(config);
|
|
1891
|
-
}
|
|
2260
|
+
const price = await pricing.quote(body.data);
|
|
2261
|
+
return { ok: true, parsedBody: body.data, price };
|
|
1892
2262
|
} catch (err) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2263
|
+
return {
|
|
2264
|
+
ok: false,
|
|
2265
|
+
response: fail(
|
|
2266
|
+
ctx,
|
|
2267
|
+
errorStatus(err, 500),
|
|
2268
|
+
errorMessage(err, "Price calculation failed"),
|
|
2269
|
+
body.data
|
|
2270
|
+
)
|
|
2271
|
+
};
|
|
1898
2272
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2273
|
+
}
|
|
2274
|
+
function surrogatePriceForSkippedBody(routeEntry) {
|
|
2275
|
+
return routeEntry.maxPrice ?? routeEntry.minPrice ?? "0";
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// src/pipeline/flows/dynamic/dynamic-channel-mgmt.ts
|
|
2279
|
+
import { NextResponse as NextResponse5 } from "next/server";
|
|
2280
|
+
async function runDynamicChannelMgmtFlow(args) {
|
|
2281
|
+
const { ctx, strategy, account, pricing, skipBody } = args;
|
|
2282
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
2283
|
+
const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
|
|
2284
|
+
if (!bodyAndPrice.ok) return bodyAndPrice.response;
|
|
2285
|
+
const { parsedBody, price } = bodyAndPrice;
|
|
2286
|
+
const verifyOutcome = await strategy.verify({
|
|
2287
|
+
request,
|
|
2288
|
+
body: parsedBody,
|
|
2289
|
+
price,
|
|
2290
|
+
routeEntry,
|
|
2291
|
+
deps,
|
|
2292
|
+
report
|
|
2293
|
+
});
|
|
2294
|
+
if (verifyOutcome.ok === false) {
|
|
2295
|
+
if (verifyOutcome.kind === "config") {
|
|
2296
|
+
return fail(ctx, 500, verifyOutcome.message, parsedBody);
|
|
1909
2297
|
}
|
|
2298
|
+
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
1910
2299
|
}
|
|
1911
|
-
|
|
2300
|
+
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2301
|
+
firePaymentVerified(ctx, {
|
|
2302
|
+
protocol: strategy.protocol,
|
|
2303
|
+
payer: verifyOutcome.wallet,
|
|
2304
|
+
amount: price,
|
|
2305
|
+
network: verifyOutcome.payment.network
|
|
2306
|
+
});
|
|
2307
|
+
const synthetic = new NextResponse5(null, { status: 200 });
|
|
2308
|
+
const settleScope = {
|
|
2309
|
+
wallet: verifyOutcome.wallet,
|
|
2310
|
+
account,
|
|
2311
|
+
body: parsedBody,
|
|
2312
|
+
payment: verifyOutcome.payment,
|
|
2313
|
+
response: synthetic,
|
|
2314
|
+
rawResult: void 0
|
|
2315
|
+
};
|
|
2316
|
+
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2317
|
+
if (beforeErr) return beforeErr;
|
|
2318
|
+
return settleAndFinalizeRequest({
|
|
2319
|
+
ctx,
|
|
2320
|
+
strategy,
|
|
2321
|
+
verifyOutcome,
|
|
2322
|
+
scope: settleScope,
|
|
2323
|
+
rawResult: void 0,
|
|
2324
|
+
body: parsedBody,
|
|
2325
|
+
billedAmount: "0",
|
|
2326
|
+
onSettleError: async (error, failMessage) => {
|
|
2327
|
+
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2328
|
+
report("critical", `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`);
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
1912
2331
|
}
|
|
1913
2332
|
|
|
1914
|
-
// src/pipeline/flows/
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
wallet: null,
|
|
1925
|
-
route: routeEntry.key,
|
|
1926
|
-
account
|
|
2333
|
+
// src/pipeline/flows/dynamic/dynamic-invoke.ts
|
|
2334
|
+
import { NextResponse as NextResponse6 } from "next/server";
|
|
2335
|
+
|
|
2336
|
+
// src/pricing/atomic.ts
|
|
2337
|
+
var USDC_DECIMALS = 6;
|
|
2338
|
+
function decimalToAtomic(amount) {
|
|
2339
|
+
const m = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
|
|
2340
|
+
if (!m) {
|
|
2341
|
+
throw Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
|
|
2342
|
+
status: 400
|
|
1927
2343
|
});
|
|
1928
2344
|
}
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2345
|
+
const whole = m[1];
|
|
2346
|
+
const fraction = (m[2] ?? "").slice(0, USDC_DECIMALS).padEnd(USDC_DECIMALS, "0");
|
|
2347
|
+
return BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0");
|
|
2348
|
+
}
|
|
2349
|
+
function atomicToDecimal(atomic) {
|
|
2350
|
+
const whole = atomic / 10n ** BigInt(USDC_DECIMALS);
|
|
2351
|
+
const fraction = atomic % 10n ** BigInt(USDC_DECIMALS);
|
|
2352
|
+
if (fraction === 0n) return whole.toString();
|
|
2353
|
+
const fractionStr = fraction.toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
|
|
2354
|
+
return `${whole}.${fractionStr}`;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// src/pricing/charge-context.ts
|
|
2358
|
+
function createChargeContext(args) {
|
|
2359
|
+
const { tickCost, maxPrice, route } = args;
|
|
2360
|
+
const tickAtomic = decimalToAtomic(tickCost);
|
|
2361
|
+
if (tickAtomic <= 0n) {
|
|
2362
|
+
throw new Error(`route '${route}': tickCost '${tickCost}' must be a positive decimal string`);
|
|
2363
|
+
}
|
|
2364
|
+
const capAtomic = maxPrice !== void 0 ? decimalToAtomic(maxPrice) : null;
|
|
2365
|
+
let ticks = 0;
|
|
2366
|
+
let atomic = 0n;
|
|
2367
|
+
let channelCharge = null;
|
|
2368
|
+
const charge = async () => {
|
|
2369
|
+
const nextAtomic = atomic + tickAtomic;
|
|
2370
|
+
if (capAtomic !== null && nextAtomic > capAtomic) {
|
|
2371
|
+
throw Object.assign(
|
|
2372
|
+
new Error(
|
|
2373
|
+
`route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
|
|
2374
|
+
),
|
|
2375
|
+
{ status: 400, code: "CHARGE_OVER_CAP" }
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
ticks += 1;
|
|
2379
|
+
atomic = nextAtomic;
|
|
2380
|
+
if (channelCharge) await channelCharge();
|
|
2381
|
+
};
|
|
2382
|
+
return {
|
|
2383
|
+
charge,
|
|
2384
|
+
bindChannelCharge: (fn) => {
|
|
2385
|
+
channelCharge = fn;
|
|
2386
|
+
},
|
|
2387
|
+
tickCount: () => ticks,
|
|
2388
|
+
atomicTotal: () => atomic
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// src/pipeline/flows/dynamic/dynamic-invoke.ts
|
|
2393
|
+
async function invokeDynamic(ctx, wallet, account, body, payment) {
|
|
2394
|
+
const streaming = ctx.routeEntry.streaming === true;
|
|
2395
|
+
const chargeContext = streaming ? createChargeContext({
|
|
2396
|
+
tickCost: ctx.routeEntry.tickCost,
|
|
2397
|
+
maxPrice: ctx.routeEntry.maxPrice,
|
|
2398
|
+
route: ctx.routeEntry.key
|
|
2399
|
+
}) : null;
|
|
2400
|
+
const baseHandlerCtx = {
|
|
2401
|
+
body,
|
|
2402
|
+
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
2403
|
+
request: ctx.request,
|
|
2404
|
+
requestId: ctx.meta.requestId,
|
|
2405
|
+
route: ctx.routeEntry.key,
|
|
2406
|
+
wallet,
|
|
2407
|
+
payment,
|
|
2408
|
+
account,
|
|
2409
|
+
alert: ctx.report,
|
|
2410
|
+
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
2411
|
+
};
|
|
2412
|
+
const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
|
|
2413
|
+
let returned;
|
|
2414
|
+
try {
|
|
2415
|
+
returned = ctx.handler(handlerCtx);
|
|
2416
|
+
} catch (error) {
|
|
2417
|
+
return errorResult2(error, chargeContext);
|
|
2418
|
+
}
|
|
2419
|
+
if (isAsyncIterable2(returned) && !isThenable2(returned)) {
|
|
2420
|
+
if (!chargeContext) {
|
|
2421
|
+
return errorResult2(
|
|
2422
|
+
new HttpError(
|
|
2423
|
+
"route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
|
|
2424
|
+
500
|
|
2425
|
+
),
|
|
2426
|
+
null
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
return {
|
|
2430
|
+
kind: "stream",
|
|
2431
|
+
source: returned,
|
|
2432
|
+
chargeContext
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
let rawResult;
|
|
2436
|
+
try {
|
|
2437
|
+
rawResult = await returned;
|
|
2438
|
+
} catch (error) {
|
|
2439
|
+
return errorResult2(error, chargeContext);
|
|
2440
|
+
}
|
|
2441
|
+
const response = rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
|
|
2442
|
+
return { kind: "request", response, rawResult };
|
|
2443
|
+
}
|
|
2444
|
+
function errorResult2(error, chargeContext) {
|
|
2445
|
+
const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
|
|
2446
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
2447
|
+
void chargeContext;
|
|
2448
|
+
return {
|
|
2449
|
+
kind: "request",
|
|
2450
|
+
response: NextResponse6.json({ success: false, error: message }, { status }),
|
|
2451
|
+
rawResult: void 0,
|
|
2452
|
+
handlerError: error
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
function isAsyncIterable2(value) {
|
|
2456
|
+
return value != null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
2457
|
+
}
|
|
2458
|
+
function isThenable2(value) {
|
|
2459
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// src/pipeline/flows/dynamic/dynamic-preflight.ts
|
|
2463
|
+
function resolveDynamicPreflight(strategy, request, routeEntry) {
|
|
2464
|
+
const outcome = strategy.preflight?.(request, routeEntry) ?? null;
|
|
2465
|
+
return {
|
|
2466
|
+
skipBody: outcome?.skipBody ?? false,
|
|
2467
|
+
skipHandler: outcome?.skipHandler ?? false
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// src/pipeline/flows/dynamic/dynamic-request.ts
|
|
2472
|
+
async function runDynamicRequestFlow(args) {
|
|
2473
|
+
const { ctx, strategy, verifyOutcome, account, body, result } = args;
|
|
2474
|
+
const { routeEntry } = ctx;
|
|
2475
|
+
const settleScope = {
|
|
2476
|
+
wallet: verifyOutcome.wallet,
|
|
2477
|
+
account,
|
|
2478
|
+
body,
|
|
2479
|
+
payment: verifyOutcome.payment,
|
|
2480
|
+
response: result.response,
|
|
2481
|
+
rawResult: result.rawResult,
|
|
2482
|
+
handlerError: result.handlerError
|
|
2483
|
+
};
|
|
2484
|
+
if (result.response.status >= 400) {
|
|
2485
|
+
return finalize(ctx, result.response, result.rawResult, body);
|
|
2486
|
+
}
|
|
2487
|
+
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2488
|
+
if (beforeErr) return beforeErr;
|
|
2489
|
+
const billedAmount = routeEntry.tickCost;
|
|
2490
|
+
return settleAndFinalizeRequest({
|
|
2491
|
+
ctx,
|
|
2492
|
+
strategy,
|
|
2493
|
+
verifyOutcome,
|
|
2494
|
+
scope: settleScope,
|
|
2495
|
+
rawResult: result.rawResult,
|
|
2496
|
+
body,
|
|
2497
|
+
billedAmount,
|
|
2498
|
+
onSettleError: async (error, failMessage) => {
|
|
2499
|
+
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2500
|
+
ctx.report(
|
|
2501
|
+
"critical",
|
|
2502
|
+
`${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// src/pipeline/flows/dynamic/dynamic-stream.ts
|
|
2509
|
+
async function runDynamicStreamFlow(args) {
|
|
2510
|
+
const { ctx, strategy, verifyOutcome, account, body, result } = args;
|
|
2511
|
+
return settleAndFinalizeStream({
|
|
2512
|
+
ctx,
|
|
2513
|
+
strategy,
|
|
2514
|
+
verifyOutcome,
|
|
2515
|
+
source: result.source,
|
|
2516
|
+
account,
|
|
2517
|
+
body,
|
|
2518
|
+
bindChannelCharge: result.chargeContext.bindChannelCharge
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// src/pipeline/flows/dynamic/dynamic-paid.ts
|
|
2523
|
+
async function runDynamicPaidFlow(ctx) {
|
|
2524
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
2525
|
+
const apiKeyGate = await runApiKeyGate(ctx);
|
|
2526
|
+
if (!apiKeyGate.ok) return apiKeyGate.response;
|
|
2527
|
+
const { account } = apiKeyGate;
|
|
2528
|
+
const pricing = selectPricing(routeEntry.pricing, {
|
|
2529
|
+
alert: report,
|
|
2530
|
+
maxPrice: routeEntry.maxPrice,
|
|
2531
|
+
minPrice: routeEntry.minPrice,
|
|
2532
|
+
route: routeEntry.key
|
|
2533
|
+
});
|
|
2534
|
+
const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
|
|
2535
|
+
const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
|
|
2536
|
+
if (!earlyResolution.ok) return earlyResolution.response;
|
|
2537
|
+
const { earlyBody } = earlyResolution;
|
|
2538
|
+
const siwxFastPath = await trySiwxFastPath(ctx, account);
|
|
2539
|
+
if (siwxFastPath) return siwxFastPath;
|
|
2540
|
+
if (!incomingStrategy) {
|
|
2541
|
+
const initError = protocolInitError(routeEntry, deps);
|
|
2542
|
+
if (initError) return fail(ctx, 500, initError);
|
|
2543
|
+
return build402(ctx, pricing, earlyBody);
|
|
2544
|
+
}
|
|
2545
|
+
const { skipBody, skipHandler } = resolveDynamicPreflight(incomingStrategy, request, routeEntry);
|
|
2546
|
+
if (skipHandler) {
|
|
2547
|
+
return runDynamicChannelMgmtFlow({
|
|
2548
|
+
ctx,
|
|
2549
|
+
strategy: incomingStrategy,
|
|
2550
|
+
account,
|
|
2551
|
+
pricing,
|
|
2552
|
+
skipBody
|
|
1935
2553
|
});
|
|
2554
|
+
}
|
|
2555
|
+
const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
|
|
2556
|
+
if (!bodyAndPrice.ok) return bodyAndPrice.response;
|
|
2557
|
+
const { parsedBody, price } = bodyAndPrice;
|
|
2558
|
+
const verifyOutcome = await incomingStrategy.verify({
|
|
2559
|
+
request,
|
|
2560
|
+
body: parsedBody,
|
|
2561
|
+
price,
|
|
2562
|
+
routeEntry,
|
|
2563
|
+
deps,
|
|
2564
|
+
report
|
|
2565
|
+
});
|
|
2566
|
+
if (verifyOutcome.ok === false) {
|
|
2567
|
+
if (verifyOutcome.kind === "config") {
|
|
2568
|
+
return fail(ctx, 500, verifyOutcome.message, parsedBody);
|
|
2569
|
+
}
|
|
2570
|
+
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
2571
|
+
}
|
|
2572
|
+
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2573
|
+
firePaymentVerified(ctx, {
|
|
2574
|
+
protocol: incomingStrategy.protocol,
|
|
2575
|
+
payer: verifyOutcome.wallet,
|
|
2576
|
+
amount: price,
|
|
2577
|
+
network: verifyOutcome.payment.network
|
|
2578
|
+
});
|
|
2579
|
+
const result = await invokeDynamic(
|
|
2580
|
+
ctx,
|
|
2581
|
+
verifyOutcome.wallet,
|
|
2582
|
+
account,
|
|
2583
|
+
parsedBody,
|
|
2584
|
+
verifyOutcome.payment
|
|
2585
|
+
);
|
|
2586
|
+
switch (result.kind) {
|
|
2587
|
+
case "stream":
|
|
2588
|
+
return runDynamicStreamFlow({
|
|
2589
|
+
ctx,
|
|
2590
|
+
strategy: incomingStrategy,
|
|
2591
|
+
verifyOutcome,
|
|
2592
|
+
account,
|
|
2593
|
+
body: parsedBody,
|
|
2594
|
+
result
|
|
2595
|
+
});
|
|
2596
|
+
case "request":
|
|
2597
|
+
return runDynamicRequestFlow({
|
|
2598
|
+
ctx,
|
|
2599
|
+
strategy: incomingStrategy,
|
|
2600
|
+
verifyOutcome,
|
|
2601
|
+
account,
|
|
2602
|
+
body: parsedBody,
|
|
2603
|
+
result
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
// src/pipeline/flows/static/static-body-and-price.ts
|
|
2609
|
+
async function resolveStaticBodyAndPrice(args) {
|
|
2610
|
+
const { ctx, pricing } = args;
|
|
2611
|
+
const body = await parseBody(ctx);
|
|
2612
|
+
if (!body.ok) return { ok: false, response: body.response };
|
|
2613
|
+
const validateErr = await runValidate(ctx, body.data);
|
|
2614
|
+
if (validateErr) {
|
|
2615
|
+
return { ok: false, response: validateErr };
|
|
2616
|
+
}
|
|
2617
|
+
if (!pricing) {
|
|
2618
|
+
return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
|
|
2619
|
+
}
|
|
2620
|
+
try {
|
|
2621
|
+
const price = await pricing.quote(body.data);
|
|
2622
|
+
return { ok: true, parsedBody: body.data, price };
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
return {
|
|
2625
|
+
ok: false,
|
|
2626
|
+
response: fail(
|
|
2627
|
+
ctx,
|
|
2628
|
+
errorStatus(err, 500),
|
|
2629
|
+
errorMessage(err, "Price calculation failed"),
|
|
2630
|
+
body.data
|
|
2631
|
+
)
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// src/pipeline/flows/static/static-request.ts
|
|
2637
|
+
async function runStaticRequestFlow(args) {
|
|
2638
|
+
const { ctx, strategy, verifyOutcome, account, body, price, result } = args;
|
|
2639
|
+
const settleScope = {
|
|
2640
|
+
wallet: verifyOutcome.wallet,
|
|
2641
|
+
account,
|
|
2642
|
+
body,
|
|
2643
|
+
payment: verifyOutcome.payment,
|
|
2644
|
+
response: result.response,
|
|
2645
|
+
rawResult: result.rawResult,
|
|
2646
|
+
handlerError: result.handlerError
|
|
1936
2647
|
};
|
|
2648
|
+
if (verifyOutcome.alreadySettled) {
|
|
2649
|
+
if (result.response.status >= 400) {
|
|
2650
|
+
const settledScope = settleScope;
|
|
2651
|
+
await runSettledHandlerError(ctx, settledScope);
|
|
2652
|
+
return finalize(ctx, result.response, result.rawResult, body);
|
|
2653
|
+
}
|
|
2654
|
+
return settleAndFinalizeRequest({
|
|
2655
|
+
ctx,
|
|
2656
|
+
strategy,
|
|
2657
|
+
verifyOutcome,
|
|
2658
|
+
scope: settleScope,
|
|
2659
|
+
rawResult: result.rawResult,
|
|
2660
|
+
body,
|
|
2661
|
+
billedAmount: price
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
if (result.response.status >= 400) {
|
|
2665
|
+
return finalize(ctx, result.response, result.rawResult, body);
|
|
2666
|
+
}
|
|
2667
|
+
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2668
|
+
if (beforeErr) return beforeErr;
|
|
2669
|
+
return settleAndFinalizeRequest({
|
|
2670
|
+
ctx,
|
|
2671
|
+
strategy,
|
|
2672
|
+
verifyOutcome,
|
|
2673
|
+
scope: settleScope,
|
|
2674
|
+
rawResult: result.rawResult,
|
|
2675
|
+
body,
|
|
2676
|
+
billedAmount: price,
|
|
2677
|
+
onSettleError: async (error, failMessage) => {
|
|
2678
|
+
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2679
|
+
ctx.report(
|
|
2680
|
+
"critical",
|
|
2681
|
+
`${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// src/pipeline/flows/static/static-paid.ts
|
|
2688
|
+
async function runStaticPaidFlow(ctx) {
|
|
2689
|
+
const { request, routeEntry, deps, report } = ctx;
|
|
2690
|
+
const apiKeyGate = await runApiKeyGate(ctx);
|
|
2691
|
+
if (!apiKeyGate.ok) return apiKeyGate.response;
|
|
2692
|
+
const { account } = apiKeyGate;
|
|
1937
2693
|
const pricing = selectPricing(routeEntry.pricing, {
|
|
1938
|
-
alert:
|
|
2694
|
+
alert: report,
|
|
1939
2695
|
maxPrice: routeEntry.maxPrice,
|
|
1940
2696
|
minPrice: routeEntry.minPrice,
|
|
1941
2697
|
route: routeEntry.key
|
|
1942
2698
|
});
|
|
1943
2699
|
const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
|
|
1944
|
-
|
|
1945
|
-
if (
|
|
1946
|
-
|
|
1947
|
-
const earlyResult = await parseBody(earlyClone, routeEntry);
|
|
1948
|
-
if (earlyResult.ok) {
|
|
1949
|
-
earlyBody = earlyResult.data;
|
|
1950
|
-
const validateErr2 = await runValidate(ctx, earlyBody);
|
|
1951
|
-
if (validateErr2) return validateErr2;
|
|
1952
|
-
} else {
|
|
1953
|
-
firePluginResponse(ctx, earlyResult.response);
|
|
1954
|
-
return earlyResult.response;
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
2700
|
+
const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
|
|
2701
|
+
if (!earlyResolution.ok) return earlyResolution.response;
|
|
2702
|
+
const { earlyBody } = earlyResolution;
|
|
1957
2703
|
const siwxFastPath = await trySiwxFastPath(ctx, account);
|
|
1958
2704
|
if (siwxFastPath) return siwxFastPath;
|
|
1959
2705
|
if (!incomingStrategy) {
|
|
@@ -1961,99 +2707,139 @@ async function runPaidFlow(ctx) {
|
|
|
1961
2707
|
if (initError) return fail(ctx, 500, initError);
|
|
1962
2708
|
return build402(ctx, pricing, earlyBody);
|
|
1963
2709
|
}
|
|
1964
|
-
const
|
|
1965
|
-
if (!
|
|
1966
|
-
|
|
1967
|
-
return body.response;
|
|
1968
|
-
}
|
|
1969
|
-
const validateErr = await runValidate(ctx, body.data);
|
|
1970
|
-
if (validateErr) return validateErr;
|
|
1971
|
-
if (!pricing) {
|
|
1972
|
-
return fail(ctx, 500, "Pricing not configured", body.data);
|
|
1973
|
-
}
|
|
1974
|
-
let price;
|
|
1975
|
-
try {
|
|
1976
|
-
price = await pricing.quote(body.data);
|
|
1977
|
-
} catch (err) {
|
|
1978
|
-
return fail(
|
|
1979
|
-
ctx,
|
|
1980
|
-
errorStatus(err, 500),
|
|
1981
|
-
errorMessage(err, "Price calculation failed"),
|
|
1982
|
-
body.data
|
|
1983
|
-
);
|
|
1984
|
-
}
|
|
2710
|
+
const bodyAndPrice = await resolveStaticBodyAndPrice({ ctx, pricing });
|
|
2711
|
+
if (!bodyAndPrice.ok) return bodyAndPrice.response;
|
|
2712
|
+
const { parsedBody, price } = bodyAndPrice;
|
|
1985
2713
|
const verifyOutcome = await incomingStrategy.verify({
|
|
1986
2714
|
request,
|
|
1987
|
-
body:
|
|
2715
|
+
body: parsedBody,
|
|
1988
2716
|
price,
|
|
1989
2717
|
routeEntry,
|
|
1990
|
-
deps
|
|
2718
|
+
deps,
|
|
2719
|
+
report
|
|
1991
2720
|
});
|
|
1992
2721
|
if (verifyOutcome.ok === false) {
|
|
1993
2722
|
if (verifyOutcome.kind === "config") {
|
|
1994
|
-
return fail(ctx, 500, verifyOutcome.message,
|
|
2723
|
+
return fail(ctx, 500, verifyOutcome.message, parsedBody);
|
|
1995
2724
|
}
|
|
1996
|
-
return build402(ctx, pricing,
|
|
2725
|
+
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
1997
2726
|
}
|
|
1998
2727
|
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
1999
|
-
|
|
2728
|
+
firePaymentVerified(ctx, {
|
|
2000
2729
|
protocol: incomingStrategy.protocol,
|
|
2001
2730
|
payer: verifyOutcome.wallet,
|
|
2002
2731
|
amount: price,
|
|
2003
2732
|
network: verifyOutcome.payment.network
|
|
2004
2733
|
});
|
|
2005
|
-
const result = await
|
|
2006
|
-
|
|
2007
|
-
|
|
2734
|
+
const result = await invokePaidStatic(
|
|
2735
|
+
ctx,
|
|
2736
|
+
verifyOutcome.wallet,
|
|
2008
2737
|
account,
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
handlerError: result.handlerError
|
|
2014
|
-
};
|
|
2015
|
-
if (verifyOutcome.alreadySettled) {
|
|
2016
|
-
if (result.response.status >= 400) {
|
|
2017
|
-
const settledScope = settleScope;
|
|
2018
|
-
await runSettledHandlerError(ctx, settledScope);
|
|
2019
|
-
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
2020
|
-
}
|
|
2021
|
-
return settleAndFinalize({
|
|
2022
|
-
ctx,
|
|
2023
|
-
strategy: incomingStrategy,
|
|
2024
|
-
verifyOutcome,
|
|
2025
|
-
scope: settleScope,
|
|
2026
|
-
rawResult: result.rawResult,
|
|
2027
|
-
body: body.data
|
|
2028
|
-
});
|
|
2029
|
-
}
|
|
2030
|
-
if (result.response.status >= 400) {
|
|
2031
|
-
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
2032
|
-
}
|
|
2033
|
-
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2034
|
-
if (beforeErr) return beforeErr;
|
|
2035
|
-
return settleAndFinalize({
|
|
2738
|
+
parsedBody,
|
|
2739
|
+
verifyOutcome.payment
|
|
2740
|
+
);
|
|
2741
|
+
return runStaticRequestFlow({
|
|
2036
2742
|
ctx,
|
|
2037
2743
|
strategy: incomingStrategy,
|
|
2038
2744
|
verifyOutcome,
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2044
|
-
firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2045
|
-
level: "critical",
|
|
2046
|
-
message: `${incomingStrategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
|
|
2047
|
-
route: routeEntry.key
|
|
2048
|
-
});
|
|
2049
|
-
}
|
|
2745
|
+
account,
|
|
2746
|
+
body: parsedBody,
|
|
2747
|
+
price,
|
|
2748
|
+
result
|
|
2050
2749
|
});
|
|
2051
2750
|
}
|
|
2052
2751
|
|
|
2752
|
+
// src/pipeline/flows/paid.ts
|
|
2753
|
+
async function runPaidFlow(ctx) {
|
|
2754
|
+
const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
|
|
2755
|
+
switch (dynamicPrice) {
|
|
2756
|
+
case true:
|
|
2757
|
+
return runDynamicPaidFlow(ctx);
|
|
2758
|
+
case false:
|
|
2759
|
+
return runStaticPaidFlow(ctx);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2053
2763
|
// src/pipeline/flows/siwx-only.ts
|
|
2054
|
-
import { NextResponse as
|
|
2764
|
+
import { NextResponse as NextResponse7 } from "next/server";
|
|
2765
|
+
|
|
2766
|
+
// src/kv-store/client.ts
|
|
2767
|
+
function restKvStore(url, token) {
|
|
2768
|
+
const base = url.replace(/\/+$/, "");
|
|
2769
|
+
const authHeader = { Authorization: `Bearer ${token}` };
|
|
2770
|
+
const jsonHeaders = { ...authHeader, "Content-Type": "application/json" };
|
|
2771
|
+
async function exec(command) {
|
|
2772
|
+
const res = await fetch(base, {
|
|
2773
|
+
method: "POST",
|
|
2774
|
+
headers: jsonHeaders,
|
|
2775
|
+
body: JSON.stringify(command)
|
|
2776
|
+
});
|
|
2777
|
+
if (!res.ok) {
|
|
2778
|
+
throw new Error(`[kv-store] ${command[0]} ${command[1] ?? ""}: ${res.status}`);
|
|
2779
|
+
}
|
|
2780
|
+
const body = await res.json();
|
|
2781
|
+
if (body.error) throw new Error(`[kv-store] ${command[0]}: ${body.error}`);
|
|
2782
|
+
return body.result ?? null;
|
|
2783
|
+
}
|
|
2784
|
+
async function get(key) {
|
|
2785
|
+
const res = await fetch(`${base}/get/${encodeURIComponent(key)}`, { headers: authHeader });
|
|
2786
|
+
if (!res.ok) throw new Error(`[kv-store] GET ${key}: ${res.status}`);
|
|
2787
|
+
const { result } = await res.json();
|
|
2788
|
+
return result ?? null;
|
|
2789
|
+
}
|
|
2790
|
+
async function set(key, value) {
|
|
2791
|
+
await exec(["SET", key, JSON.stringify(value)]);
|
|
2792
|
+
}
|
|
2793
|
+
async function del(key) {
|
|
2794
|
+
await exec(["DEL", key]);
|
|
2795
|
+
}
|
|
2796
|
+
async function setNxEx(key, value, ttlSeconds) {
|
|
2797
|
+
const result = await exec(["SET", key, JSON.stringify(value), "EX", ttlSeconds, "NX"]);
|
|
2798
|
+
return result === "OK";
|
|
2799
|
+
}
|
|
2800
|
+
async function sadd(key, member) {
|
|
2801
|
+
await exec(["SADD", key, member]);
|
|
2802
|
+
}
|
|
2803
|
+
async function sismember(key, member) {
|
|
2804
|
+
const result = await exec(["SISMEMBER", key, member]);
|
|
2805
|
+
return result === 1;
|
|
2806
|
+
}
|
|
2807
|
+
async function update(key, fn) {
|
|
2808
|
+
const current = await get(key);
|
|
2809
|
+
const change = fn(current);
|
|
2810
|
+
if (change.op === "set") await set(key, change.value);
|
|
2811
|
+
if (change.op === "delete") await del(key);
|
|
2812
|
+
return change.result;
|
|
2813
|
+
}
|
|
2814
|
+
return { get, set, del, setNxEx, sadd, sismember, update };
|
|
2815
|
+
}
|
|
2816
|
+
function isRestConfig(input) {
|
|
2817
|
+
return typeof input === "object" && input !== null && typeof input.url === "string" && typeof input.token === "string" && typeof input.get !== "function";
|
|
2818
|
+
}
|
|
2819
|
+
function resolveKvStore(input, env = process.env) {
|
|
2820
|
+
if (input) {
|
|
2821
|
+
if (isRestConfig(input)) return restKvStore(input.url, input.token);
|
|
2822
|
+
return input;
|
|
2823
|
+
}
|
|
2824
|
+
const url = env.KV_REST_API_URL;
|
|
2825
|
+
const token = env.KV_REST_API_TOKEN;
|
|
2826
|
+
if (url && token) return restKvStore(url, token);
|
|
2827
|
+
return void 0;
|
|
2828
|
+
}
|
|
2829
|
+
function withPrefix(kv, prefix) {
|
|
2830
|
+
const k = (key) => `${prefix}${key}`;
|
|
2831
|
+
return {
|
|
2832
|
+
get: (key) => kv.get(k(key)),
|
|
2833
|
+
set: (key, value) => kv.set(k(key), value),
|
|
2834
|
+
del: (key) => kv.del(k(key)),
|
|
2835
|
+
setNxEx: (key, value, ttl) => kv.setNxEx(k(key), value, ttl),
|
|
2836
|
+
sadd: (key, member) => kv.sadd(k(key), member),
|
|
2837
|
+
sismember: (key, member) => kv.sismember(k(key), member),
|
|
2838
|
+
update: (key, fn) => kv.update(k(key), fn)
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2055
2841
|
|
|
2056
|
-
// src/
|
|
2842
|
+
// src/kv-store/nonce.ts
|
|
2057
2843
|
var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
|
|
2058
2844
|
var MemoryNonceStore = class {
|
|
2059
2845
|
seen = /* @__PURE__ */ new Map();
|
|
@@ -2070,48 +2856,74 @@ var MemoryNonceStore = class {
|
|
|
2070
2856
|
}
|
|
2071
2857
|
}
|
|
2072
2858
|
};
|
|
2073
|
-
function
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
)
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2859
|
+
function createKvNonceStore(kv, options) {
|
|
2860
|
+
const prefix = options?.prefix ?? "siwx:nonce:";
|
|
2861
|
+
const ttlSeconds = Math.ceil((options?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
2862
|
+
return {
|
|
2863
|
+
async check(nonce) {
|
|
2864
|
+
return kv.setNxEx(`${prefix}${nonce}`, 1, ttlSeconds);
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// src/kv-store/entitlement.ts
|
|
2870
|
+
var MemoryEntitlementStore = class {
|
|
2871
|
+
routeToWallets = /* @__PURE__ */ new Map();
|
|
2872
|
+
async has(route, wallet) {
|
|
2873
|
+
const wallets = this.routeToWallets.get(route);
|
|
2874
|
+
if (!wallets) return false;
|
|
2875
|
+
return wallets.has(normalizeWalletAddress(wallet));
|
|
2085
2876
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2877
|
+
async grant(route, wallet) {
|
|
2878
|
+
const normalized = normalizeWalletAddress(wallet);
|
|
2879
|
+
let wallets = this.routeToWallets.get(route);
|
|
2880
|
+
if (!wallets) {
|
|
2881
|
+
wallets = /* @__PURE__ */ new Set();
|
|
2882
|
+
this.routeToWallets.set(route, wallets);
|
|
2883
|
+
}
|
|
2884
|
+
wallets.add(normalized);
|
|
2088
2885
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
}
|
|
2093
|
-
function createRedisNonceStore(client, opts) {
|
|
2094
|
-
const prefix = opts?.prefix ?? "siwx:nonce:";
|
|
2095
|
-
const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
2096
|
-
const clientType = detectRedisClientType(client);
|
|
2886
|
+
};
|
|
2887
|
+
function createKvEntitlementStore(kv, options) {
|
|
2888
|
+
const prefix = options?.prefix ?? "siwx:ent:";
|
|
2097
2889
|
return {
|
|
2098
|
-
async
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
return result !== null;
|
|
2104
|
-
}
|
|
2105
|
-
if (clientType === "ioredis") {
|
|
2106
|
-
const redis = client;
|
|
2107
|
-
const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
|
|
2108
|
-
return result === "OK";
|
|
2109
|
-
}
|
|
2110
|
-
throw new Error("Unknown Redis client type");
|
|
2890
|
+
async has(route, wallet) {
|
|
2891
|
+
return kv.sismember(`${prefix}${route}`, normalizeWalletAddress(wallet));
|
|
2892
|
+
},
|
|
2893
|
+
async grant(route, wallet) {
|
|
2894
|
+
await kv.sadd(`${prefix}${route}`, normalizeWalletAddress(wallet));
|
|
2111
2895
|
}
|
|
2112
2896
|
};
|
|
2113
2897
|
}
|
|
2114
2898
|
|
|
2899
|
+
// src/kv-store/mpp.ts
|
|
2900
|
+
async function createKvMppStore(kv, options) {
|
|
2901
|
+
const prefix = options?.prefix ?? "mpp:";
|
|
2902
|
+
const namespaced = withPrefix(kv, prefix);
|
|
2903
|
+
const { Store: StoreNs } = await import("mppx");
|
|
2904
|
+
return StoreNs.upstash({
|
|
2905
|
+
get: (key) => namespaced.get(key),
|
|
2906
|
+
set: (key, value) => namespaced.set(key, value),
|
|
2907
|
+
del: (key) => namespaced.del(key),
|
|
2908
|
+
update: (key, fn) => namespaced.update(key, fn)
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// src/protocols/detect.ts
|
|
2913
|
+
function detectProtocol(request) {
|
|
2914
|
+
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
2915
|
+
return "x402";
|
|
2916
|
+
}
|
|
2917
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
2918
|
+
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
2919
|
+
return "mpp";
|
|
2920
|
+
}
|
|
2921
|
+
if (request.headers.get(HEADERS.SIWX)) {
|
|
2922
|
+
return "siwx";
|
|
2923
|
+
}
|
|
2924
|
+
return null;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2115
2927
|
// src/protocols/mpp/siwx-mode.ts
|
|
2116
2928
|
import { Credential as Credential2 } from "mppx";
|
|
2117
2929
|
async function verifyMppSiwx(request, mppx) {
|
|
@@ -2130,12 +2942,11 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2130
2942
|
const { request, routeEntry, deps } = ctx;
|
|
2131
2943
|
if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get(HEADERS.SIWX)) {
|
|
2132
2944
|
const earlyClone = request.clone();
|
|
2133
|
-
const earlyBody = await parseBody(
|
|
2945
|
+
const earlyBody = await parseBody(ctx, earlyClone);
|
|
2134
2946
|
if (earlyBody.ok) {
|
|
2135
2947
|
const validateErr = await runValidate(ctx, earlyBody.data);
|
|
2136
2948
|
if (validateErr) return validateErr;
|
|
2137
2949
|
} else {
|
|
2138
|
-
firePluginResponse(ctx, earlyBody.response);
|
|
2139
2950
|
return earlyBody.response;
|
|
2140
2951
|
}
|
|
2141
2952
|
}
|
|
@@ -2147,20 +2958,12 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2147
2958
|
mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
|
|
2148
2959
|
} catch (err) {
|
|
2149
2960
|
const message = err instanceof Error ? err.message : String(err);
|
|
2150
|
-
|
|
2151
|
-
level: "critical",
|
|
2152
|
-
message: `MPP SIWX verification failed: ${message}`,
|
|
2153
|
-
route: routeEntry.key
|
|
2154
|
-
});
|
|
2961
|
+
ctx.report("critical", `MPP SIWX verification failed: ${message}`);
|
|
2155
2962
|
return fail(ctx, 500, `MPP SIWX verification failed: ${message}`);
|
|
2156
2963
|
}
|
|
2157
2964
|
if (mppSiwxResult.valid) {
|
|
2158
2965
|
ctx.pluginCtx.setVerifiedWallet(mppSiwxResult.wallet);
|
|
2159
|
-
|
|
2160
|
-
authMode: "siwx",
|
|
2161
|
-
wallet: mppSiwxResult.wallet,
|
|
2162
|
-
route: routeEntry.key
|
|
2163
|
-
});
|
|
2966
|
+
fireAuthVerified(ctx, { authMode: "siwx", wallet: mppSiwxResult.wallet });
|
|
2164
2967
|
const authResponse = await runHandlerOnly(ctx, mppSiwxResult.wallet, void 0);
|
|
2165
2968
|
if (authResponse.status < 400) {
|
|
2166
2969
|
return mppSiwxResult.withReceipt(authResponse);
|
|
@@ -2173,7 +2976,7 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2173
2976
|
}
|
|
2174
2977
|
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
2175
2978
|
if (!siwx.valid) {
|
|
2176
|
-
const response =
|
|
2979
|
+
const response = NextResponse7.json(
|
|
2177
2980
|
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
2178
2981
|
{ status: 402 }
|
|
2179
2982
|
);
|
|
@@ -2182,11 +2985,7 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
2182
2985
|
}
|
|
2183
2986
|
const wallet = normalizeWalletAddress(siwx.wallet);
|
|
2184
2987
|
ctx.pluginCtx.setVerifiedWallet(wallet);
|
|
2185
|
-
|
|
2186
|
-
authMode: "siwx",
|
|
2187
|
-
wallet,
|
|
2188
|
-
route: routeEntry.key
|
|
2189
|
-
});
|
|
2988
|
+
fireAuthVerified(ctx, { authMode: "siwx", wallet });
|
|
2190
2989
|
return runHandlerOnly(ctx, wallet, void 0);
|
|
2191
2990
|
}
|
|
2192
2991
|
async function buildSiwxChallenge(ctx) {
|
|
@@ -2223,7 +3022,6 @@ async function buildSiwxChallenge(ctx) {
|
|
|
2223
3022
|
extensions: {
|
|
2224
3023
|
"sign-in-with-x": {
|
|
2225
3024
|
info: siwxInfo,
|
|
2226
|
-
// Required by MCP tools at the top level for chain detection.
|
|
2227
3025
|
supportedChains,
|
|
2228
3026
|
...siwxSchema ? { schema: siwxSchema } : {}
|
|
2229
3027
|
}
|
|
@@ -2234,13 +3032,12 @@ async function buildSiwxChallenge(ctx) {
|
|
|
2234
3032
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
2235
3033
|
encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
2236
3034
|
} catch (err) {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
});
|
|
3035
|
+
ctx.report(
|
|
3036
|
+
"warn",
|
|
3037
|
+
`SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3038
|
+
);
|
|
2242
3039
|
}
|
|
2243
|
-
const response = new
|
|
3040
|
+
const response = new NextResponse7(JSON.stringify(paymentRequired), {
|
|
2244
3041
|
status: 402,
|
|
2245
3042
|
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
|
|
2246
3043
|
});
|
|
@@ -2281,7 +3078,7 @@ async function runUnprotectedFlow(ctx) {
|
|
|
2281
3078
|
return runHandlerOnly(ctx, null, void 0);
|
|
2282
3079
|
}
|
|
2283
3080
|
|
|
2284
|
-
// src/orchestrate.ts
|
|
3081
|
+
// src/pipeline/orchestrate.ts
|
|
2285
3082
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
2286
3083
|
return async (request) => {
|
|
2287
3084
|
await deps.initPromise;
|
|
@@ -2346,6 +3143,12 @@ var RouteBuilder = class {
|
|
|
2346
3143
|
/** @internal */
|
|
2347
3144
|
_minPrice;
|
|
2348
3145
|
/** @internal */
|
|
3146
|
+
_dynamicPrice = false;
|
|
3147
|
+
/** @internal */
|
|
3148
|
+
_tickCost;
|
|
3149
|
+
/** @internal */
|
|
3150
|
+
_unitType;
|
|
3151
|
+
/** @internal */
|
|
2349
3152
|
_payTo;
|
|
2350
3153
|
/** @internal */
|
|
2351
3154
|
_bodySchema;
|
|
@@ -2390,7 +3193,8 @@ var RouteBuilder = class {
|
|
|
2390
3193
|
next._protocols = [...this._protocols];
|
|
2391
3194
|
return next;
|
|
2392
3195
|
}
|
|
2393
|
-
paid(
|
|
3196
|
+
paid(pricingOrOptions, options) {
|
|
3197
|
+
const { pricing, resolvedOptions } = resolvePaidArgs(this._key, pricingOrOptions, options);
|
|
2394
3198
|
if (this._authMode === "unprotected") {
|
|
2395
3199
|
throw new Error(
|
|
2396
3200
|
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
@@ -2404,16 +3208,24 @@ var RouteBuilder = class {
|
|
|
2404
3208
|
const next = this.fork();
|
|
2405
3209
|
next._authMode = "paid";
|
|
2406
3210
|
next._pricing = pricing;
|
|
2407
|
-
if (
|
|
2408
|
-
next._protocols = [...
|
|
3211
|
+
if (resolvedOptions?.protocols) {
|
|
3212
|
+
next._protocols = [...resolvedOptions.protocols];
|
|
2409
3213
|
} else if (next._protocols.length === 0) {
|
|
2410
3214
|
next._protocols = ["x402"];
|
|
2411
3215
|
}
|
|
2412
|
-
if (
|
|
2413
|
-
if (
|
|
2414
|
-
if (
|
|
2415
|
-
if (
|
|
3216
|
+
if (resolvedOptions?.maxPrice) next._maxPrice = resolvedOptions.maxPrice;
|
|
3217
|
+
if (resolvedOptions?.minPrice) next._minPrice = resolvedOptions.minPrice;
|
|
3218
|
+
if (resolvedOptions?.payTo) next._payTo = resolvedOptions.payTo;
|
|
3219
|
+
if (resolvedOptions?.mpp) next._mppInfo = resolvedOptions.mpp;
|
|
3220
|
+
if (resolvedOptions?.dynamic) next._dynamicPrice = true;
|
|
3221
|
+
if (resolvedOptions?.tickCost) next._tickCost = resolvedOptions.tickCost;
|
|
3222
|
+
if (resolvedOptions?.unitType) next._unitType = resolvedOptions.unitType;
|
|
2416
3223
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
3224
|
+
if (next._dynamicPrice) {
|
|
3225
|
+
throw new Error(
|
|
3226
|
+
`route '${this._key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
2417
3229
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
2418
3230
|
if (!tierKey) {
|
|
2419
3231
|
throw new Error(`route '${this._key}': tier key cannot be empty`);
|
|
@@ -2426,16 +3238,40 @@ var RouteBuilder = class {
|
|
|
2426
3238
|
}
|
|
2427
3239
|
}
|
|
2428
3240
|
}
|
|
2429
|
-
if (
|
|
2430
|
-
const parsed = parseFloat(
|
|
3241
|
+
if (resolvedOptions?.maxPrice !== void 0) {
|
|
3242
|
+
const parsed = parseFloat(resolvedOptions.maxPrice);
|
|
3243
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
3244
|
+
throw new Error(
|
|
3245
|
+
`route '${this._key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (resolvedOptions?.tickCost !== void 0) {
|
|
3250
|
+
const parsed = parseFloat(resolvedOptions.tickCost);
|
|
2431
3251
|
if (isNaN(parsed) || parsed <= 0) {
|
|
2432
3252
|
throw new Error(
|
|
2433
|
-
`route '${this._key}':
|
|
3253
|
+
`route '${this._key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
|
|
2434
3254
|
);
|
|
2435
3255
|
}
|
|
2436
3256
|
}
|
|
3257
|
+
if (next._dynamicPrice && !next._maxPrice) {
|
|
3258
|
+
throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires maxPrice`);
|
|
3259
|
+
}
|
|
3260
|
+
if (next._dynamicPrice && !next._tickCost) {
|
|
3261
|
+
throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires tickCost`);
|
|
3262
|
+
}
|
|
2437
3263
|
return next;
|
|
2438
3264
|
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Require Sign-In-with-X wallet identity on this route — clients prove
|
|
3267
|
+
* control of a wallet via a signed challenge. Combine with `.paid()` to gate
|
|
3268
|
+
* a paid route on a verified wallet identity.
|
|
3269
|
+
*
|
|
3270
|
+
* @example
|
|
3271
|
+
* ```ts
|
|
3272
|
+
* router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
|
|
3273
|
+
* ```
|
|
3274
|
+
*/
|
|
2439
3275
|
siwx() {
|
|
2440
3276
|
if (this._authMode === "unprotected") {
|
|
2441
3277
|
throw new Error(
|
|
@@ -2458,6 +3294,19 @@ var RouteBuilder = class {
|
|
|
2458
3294
|
next._protocols = [];
|
|
2459
3295
|
return next;
|
|
2460
3296
|
}
|
|
3297
|
+
/**
|
|
3298
|
+
* Require an `X-API-Key` header (or `Authorization: Bearer <key>`); the
|
|
3299
|
+
* resolver returns the account record, or `null` for 401. Composes with
|
|
3300
|
+
* `.paid()` — key is checked first, payment second.
|
|
3301
|
+
*
|
|
3302
|
+
* @example
|
|
3303
|
+
* ```ts
|
|
3304
|
+
* router
|
|
3305
|
+
* .route('admin/users')
|
|
3306
|
+
* .apiKey(async (key) => db.admin.findByKey(key))
|
|
3307
|
+
* .handler(async ({ account }) => db.user.list(account.orgId));
|
|
3308
|
+
* ```
|
|
3309
|
+
*/
|
|
2461
3310
|
apiKey(resolver) {
|
|
2462
3311
|
if (this._siwxEnabled) {
|
|
2463
3312
|
throw new Error(
|
|
@@ -2469,6 +3318,15 @@ var RouteBuilder = class {
|
|
|
2469
3318
|
next._apiKeyResolver = resolver;
|
|
2470
3319
|
return next;
|
|
2471
3320
|
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Mark the route as public — no auth, no payment, no SIWX. The handler
|
|
3323
|
+
* receives `null` for `wallet`, `payment`, and `account`.
|
|
3324
|
+
*
|
|
3325
|
+
* @example
|
|
3326
|
+
* ```ts
|
|
3327
|
+
* router.route('health').unprotected().handler(async () => ({ status: 'ok' }));
|
|
3328
|
+
* ```
|
|
3329
|
+
*/
|
|
2472
3330
|
unprotected() {
|
|
2473
3331
|
if (this._authMode && this._authMode !== "unprotected") {
|
|
2474
3332
|
throw new Error(
|
|
@@ -2485,60 +3343,82 @@ var RouteBuilder = class {
|
|
|
2485
3343
|
next._protocols = [];
|
|
2486
3344
|
return next;
|
|
2487
3345
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
3346
|
+
/**
|
|
3347
|
+
* Tag the route with an upstream provider for discovery and provider-side
|
|
3348
|
+
* monitoring. The provider name and config surface in `well-known` and
|
|
3349
|
+
* OpenAPI output.
|
|
3350
|
+
*
|
|
3351
|
+
* @example
|
|
3352
|
+
* ```ts
|
|
3353
|
+
* router
|
|
3354
|
+
* .route('search')
|
|
3355
|
+
* .paid('0.01')
|
|
3356
|
+
* .provider('exa', { quotaPerMonth: 1000 })
|
|
3357
|
+
* .handler(handler);
|
|
3358
|
+
* ```
|
|
3359
|
+
*/
|
|
2491
3360
|
provider(name, config) {
|
|
2492
3361
|
const next = this.fork();
|
|
2493
3362
|
next._providerName = name;
|
|
2494
3363
|
next._providerConfig = config ?? {};
|
|
2495
3364
|
return next;
|
|
2496
3365
|
}
|
|
2497
|
-
|
|
3366
|
+
/**
|
|
3367
|
+
* Declare the request body's Zod schema. Parsed body is typed as `ctx.body`
|
|
3368
|
+
* in the handler. Use `.inputExample()` to attach a discovery example.
|
|
3369
|
+
*
|
|
3370
|
+
* @example
|
|
3371
|
+
* ```ts
|
|
3372
|
+
* .body(z.object({ query: z.string() }))
|
|
3373
|
+
* .handler(async ({ body }) => search(body.query));
|
|
3374
|
+
* ```
|
|
3375
|
+
*/
|
|
3376
|
+
body(schema) {
|
|
2498
3377
|
const next = this.fork();
|
|
2499
3378
|
next._bodySchema = schema;
|
|
2500
|
-
if (example !== void 0) {
|
|
2501
|
-
next._inputExample = example;
|
|
2502
|
-
next._hasInputExample = true;
|
|
2503
|
-
}
|
|
2504
3379
|
return next;
|
|
2505
3380
|
}
|
|
2506
|
-
|
|
3381
|
+
/**
|
|
3382
|
+
* Declare a query-string Zod schema and switch the route to `GET`. Parsed
|
|
3383
|
+
* query is typed as `ctx.query` in the handler. Use `.inputExample()` to
|
|
3384
|
+
* attach a discovery example.
|
|
3385
|
+
*
|
|
3386
|
+
* @example
|
|
3387
|
+
* ```ts
|
|
3388
|
+
* .query(z.object({ id: z.string() }))
|
|
3389
|
+
* .handler(async ({ query }) => getById(query.id));
|
|
3390
|
+
* ```
|
|
3391
|
+
*/
|
|
3392
|
+
query(schema) {
|
|
2507
3393
|
const next = this.fork();
|
|
2508
3394
|
next._querySchema = schema;
|
|
2509
|
-
if (example !== void 0) {
|
|
2510
|
-
next._inputExample = example;
|
|
2511
|
-
next._hasInputExample = true;
|
|
2512
|
-
}
|
|
2513
3395
|
next._method = "GET";
|
|
2514
3396
|
return next;
|
|
2515
3397
|
}
|
|
2516
|
-
|
|
3398
|
+
/**
|
|
3399
|
+
* Declare the response output's Zod schema for OpenAPI generation. The
|
|
3400
|
+
* runtime does not validate handler return values — use Zod's `.parse()`
|
|
3401
|
+
* inside the handler if strict output validation is required. Use
|
|
3402
|
+
* `.outputExample()` to attach a discovery example.
|
|
3403
|
+
*
|
|
3404
|
+
* @example
|
|
3405
|
+
* ```ts
|
|
3406
|
+
* .output(z.object({ result: z.string() }))
|
|
3407
|
+
* .handler(async () => ({ result: 'ok' }));
|
|
3408
|
+
* ```
|
|
3409
|
+
*/
|
|
3410
|
+
output(schema) {
|
|
2517
3411
|
const next = this.fork();
|
|
2518
3412
|
next._outputSchema = schema;
|
|
2519
|
-
if (example !== void 0) {
|
|
2520
|
-
next._outputExample = example;
|
|
2521
|
-
next._hasOutputExample = true;
|
|
2522
|
-
}
|
|
2523
3413
|
return next;
|
|
2524
3414
|
}
|
|
2525
3415
|
/**
|
|
2526
|
-
*
|
|
2527
|
-
*
|
|
2528
|
-
* Optional. When provided, the example is validated against the request schema
|
|
2529
|
-
* at route registration and embedded in the bazaar discovery extension so
|
|
2530
|
-
* indexers can advertise a working sample call.
|
|
2531
|
-
*
|
|
2532
|
-
* For the common case, pass the example directly to `.body(schema, example)` or
|
|
2533
|
-
* `.query(schema, example)` instead.
|
|
3416
|
+
* Attach an example of the request body or query for discovery output,
|
|
3417
|
+
* validated against the registered schema at registration.
|
|
2534
3418
|
*
|
|
2535
3419
|
* @example
|
|
2536
3420
|
* ```ts
|
|
2537
|
-
*
|
|
2538
|
-
* .paid('0.01')
|
|
2539
|
-
* .body(z.object({ q: z.string() }))
|
|
2540
|
-
* .inputExample({ q: 'hello world' })
|
|
2541
|
-
* .handler(async ({ body }) => { ... });
|
|
3421
|
+
* .body(searchSchema).inputExample({ query: 'cats' });
|
|
2542
3422
|
* ```
|
|
2543
3423
|
*/
|
|
2544
3424
|
inputExample(example) {
|
|
@@ -2548,32 +3428,12 @@ var RouteBuilder = class {
|
|
|
2548
3428
|
return next;
|
|
2549
3429
|
}
|
|
2550
3430
|
/**
|
|
2551
|
-
*
|
|
2552
|
-
*
|
|
2553
|
-
* Optional. When provided, the example is validated against the output schema
|
|
2554
|
-
* at route registration and embedded in the bazaar discovery extension so
|
|
2555
|
-
* indexers can advertise the response shape.
|
|
2556
|
-
*
|
|
2557
|
-
* For the common case, pass the example directly to `.output(schema, example)` instead.
|
|
2558
|
-
*
|
|
2559
|
-
* Accepts any JSON value (objects, arrays, or primitives) — top-level array
|
|
2560
|
-
* or primitive responses (e.g. `z.array(...)`) are supported alongside the
|
|
2561
|
-
* common object case.
|
|
3431
|
+
* Attach an example response for discovery output, validated against the
|
|
3432
|
+
* registered output schema at registration.
|
|
2562
3433
|
*
|
|
2563
3434
|
* @example
|
|
2564
3435
|
* ```ts
|
|
2565
|
-
*
|
|
2566
|
-
* .paid('0.01')
|
|
2567
|
-
* .output(z.object({ results: z.array(z.string()) }))
|
|
2568
|
-
* .outputExample({ results: ['a', 'b'] })
|
|
2569
|
-
* .handler(async () => { ... });
|
|
2570
|
-
*
|
|
2571
|
-
* // Top-level array response
|
|
2572
|
-
* router.route('chains')
|
|
2573
|
-
* .paid('0.01')
|
|
2574
|
-
* .output(z.array(z.object({ name: z.string() })))
|
|
2575
|
-
* .outputExample([{ name: 'Ethereum' }])
|
|
2576
|
-
* .handler(async () => { ... });
|
|
3436
|
+
* .output(resultSchema).outputExample({ result: 'ok' });
|
|
2577
3437
|
* ```
|
|
2578
3438
|
*/
|
|
2579
3439
|
outputExample(example) {
|
|
@@ -2582,71 +3442,125 @@ var RouteBuilder = class {
|
|
|
2582
3442
|
next._hasOutputExample = true;
|
|
2583
3443
|
return next;
|
|
2584
3444
|
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Set a human-readable summary of the route. Surfaces in OpenAPI,
|
|
3447
|
+
* `well-known`, and `llms.txt` discovery output.
|
|
3448
|
+
*
|
|
3449
|
+
* @example
|
|
3450
|
+
* ```ts
|
|
3451
|
+
* .description('Search indexed web pages by full-text query');
|
|
3452
|
+
* ```
|
|
3453
|
+
*/
|
|
2585
3454
|
description(text) {
|
|
2586
3455
|
const next = this.fork();
|
|
2587
3456
|
next._description = text;
|
|
2588
3457
|
return next;
|
|
2589
3458
|
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Override the URL path advertised in discovery output. Defaults to the
|
|
3461
|
+
* registry key passed to `.route()`.
|
|
3462
|
+
*
|
|
3463
|
+
* @example
|
|
3464
|
+
* ```ts
|
|
3465
|
+
* router.route('search').path('/v2/search').handler(handler);
|
|
3466
|
+
* ```
|
|
3467
|
+
*/
|
|
2590
3468
|
path(p) {
|
|
2591
3469
|
const next = this.fork();
|
|
2592
3470
|
next._path = p;
|
|
2593
3471
|
return next;
|
|
2594
3472
|
}
|
|
3473
|
+
/**
|
|
3474
|
+
* Override the HTTP method advertised in discovery. Defaults to `POST`, or
|
|
3475
|
+
* `GET` when `.query()` has been called.
|
|
3476
|
+
*
|
|
3477
|
+
* @example
|
|
3478
|
+
* ```ts
|
|
3479
|
+
* router.route('items/delete').method('DELETE').handler(handler);
|
|
3480
|
+
* ```
|
|
3481
|
+
*/
|
|
2595
3482
|
method(m) {
|
|
2596
3483
|
const next = this.fork();
|
|
2597
3484
|
next._method = m;
|
|
2598
3485
|
return next;
|
|
2599
3486
|
}
|
|
2600
|
-
// -------------------------------------------------------------------------
|
|
2601
|
-
// Pre-payment validation
|
|
2602
|
-
// -------------------------------------------------------------------------
|
|
2603
3487
|
/**
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
3488
|
+
* Run validation against the parsed body before the 402 challenge. Throw
|
|
3489
|
+
* `Object.assign(new Error('...'), { status })` to reject with a custom
|
|
3490
|
+
* status code; defaults to 400. Requires `.body()` to be called first.
|
|
3491
|
+
*
|
|
3492
|
+
* @example
|
|
3493
|
+
* ```ts
|
|
3494
|
+
* .body(RegisterSchema).validate(async (body) => {
|
|
3495
|
+
* if (await isTaken(body.name)) {
|
|
3496
|
+
* throw Object.assign(new Error('taken'), { status: 409 });
|
|
3497
|
+
* }
|
|
3498
|
+
* });
|
|
3499
|
+
* ```
|
|
3500
|
+
*/
|
|
3501
|
+
validate(fn) {
|
|
3502
|
+
const next = this.fork();
|
|
3503
|
+
next._validateFn = fn;
|
|
3504
|
+
return next;
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Hook into the settlement lifecycle. `beforeSettle` runs after the handler
|
|
3508
|
+
* succeeds but before on-chain settlement and can cancel the charge;
|
|
3509
|
+
* `afterSettle` runs after settlement completes (success or failure).
|
|
2607
3510
|
*
|
|
2608
|
-
*
|
|
3511
|
+
* @example
|
|
3512
|
+
* ```ts
|
|
3513
|
+
* .settlement({
|
|
3514
|
+
* beforeSettle: ({ result }) => (result.refund ? 'skip' : 'continue'),
|
|
3515
|
+
* afterSettle: ({ tx }) => analytics.track('settled', { tx }),
|
|
3516
|
+
* });
|
|
3517
|
+
* ```
|
|
3518
|
+
*/
|
|
3519
|
+
settlement(lifecycle) {
|
|
3520
|
+
const next = this.fork();
|
|
3521
|
+
next._settlement = lifecycle;
|
|
3522
|
+
return next;
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Register the request handler and return the Next.js route function. The
|
|
3526
|
+
* handler receives a typed context and may return a value (serialized to
|
|
3527
|
+
* JSON), a raw `Response`, or throw an `HttpError` for a non-2xx status.
|
|
2609
3528
|
*
|
|
2610
3529
|
* @example
|
|
2611
|
-
* ```
|
|
2612
|
-
* router
|
|
2613
|
-
* .route('
|
|
2614
|
-
* .paid(
|
|
2615
|
-
* .body(
|
|
2616
|
-
* .
|
|
2617
|
-
* if (await isDomainTaken(body.domain)) {
|
|
2618
|
-
* throw Object.assign(new Error('Domain taken'), { status: 409 });
|
|
2619
|
-
* }
|
|
2620
|
-
* })
|
|
2621
|
-
* .handler(async ({ body }) => { ... });
|
|
3530
|
+
* ```ts
|
|
3531
|
+
* export const POST = router
|
|
3532
|
+
* .route('search')
|
|
3533
|
+
* .paid('0.01')
|
|
3534
|
+
* .body(schema)
|
|
3535
|
+
* .handler(async ({ body, wallet }) => searchService(body, wallet));
|
|
2622
3536
|
* ```
|
|
2623
3537
|
*/
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
next._validateFn = fn;
|
|
2627
|
-
return next;
|
|
3538
|
+
handler(fn) {
|
|
3539
|
+
return this.register(fn, false);
|
|
2628
3540
|
}
|
|
2629
|
-
// -------------------------------------------------------------------------
|
|
2630
|
-
// Settlement lifecycle
|
|
2631
|
-
// -------------------------------------------------------------------------
|
|
2632
3541
|
/**
|
|
2633
|
-
*
|
|
3542
|
+
* Register a streaming handler (`async function*`) and return the Next.js
|
|
3543
|
+
* route function. Each `charge()` call bills one tick (`tickCost` USDC) up
|
|
3544
|
+
* to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
|
|
2634
3545
|
*
|
|
2635
|
-
*
|
|
2636
|
-
*
|
|
2637
|
-
*
|
|
2638
|
-
*
|
|
3546
|
+
* @example
|
|
3547
|
+
* ```ts
|
|
3548
|
+
* export const POST = router
|
|
3549
|
+
* .route('llm/stream')
|
|
3550
|
+
* .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
|
|
3551
|
+
* .body(schema)
|
|
3552
|
+
* .stream(async function* ({ body, charge }) {
|
|
3553
|
+
* for await (const token of streamLLM(body.prompt)) {
|
|
3554
|
+
* await charge();
|
|
3555
|
+
* yield token;
|
|
3556
|
+
* }
|
|
3557
|
+
* });
|
|
3558
|
+
* ```
|
|
2639
3559
|
*/
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
next._settlement = lifecycle;
|
|
2643
|
-
return next;
|
|
3560
|
+
stream(fn) {
|
|
3561
|
+
return this.register(fn, true);
|
|
2644
3562
|
}
|
|
2645
|
-
|
|
2646
|
-
// Terminal method
|
|
2647
|
-
// -------------------------------------------------------------------------
|
|
2648
|
-
handler(fn) {
|
|
2649
|
-
const handlerFn = fn;
|
|
3563
|
+
register(handlerFn, streaming) {
|
|
2650
3564
|
if (!this._authMode) {
|
|
2651
3565
|
throw new Error(
|
|
2652
3566
|
`route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
@@ -2660,6 +3574,26 @@ var RouteBuilder = class {
|
|
|
2660
3574
|
if (this._settlement && !this._pricing) {
|
|
2661
3575
|
throw new Error(`route '${this._key}': .settlement() requires a paid route`);
|
|
2662
3576
|
}
|
|
3577
|
+
if (this._dynamicPrice && this._protocols.includes("x402")) {
|
|
3578
|
+
const hasUpto = this._deps.x402Accepts.some((accept) => accept.scheme === "upto");
|
|
3579
|
+
if (!hasUpto) {
|
|
3580
|
+
throw new Error(
|
|
3581
|
+
`route '${this._key}': .paid({ dynamic: true }) on an x402 route requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
if (this._dynamicPrice && this._protocols.includes("mpp")) {
|
|
3586
|
+
if (!this._deps.mppSessionConfig) {
|
|
3587
|
+
throw new Error(
|
|
3588
|
+
`route '${this._key}': .paid({ dynamic: true }) on an MPP route requires session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
if (streaming && !this._dynamicPrice) {
|
|
3593
|
+
throw new Error(
|
|
3594
|
+
`route '${this._key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
|
|
3595
|
+
);
|
|
3596
|
+
}
|
|
2663
3597
|
validateExamples(
|
|
2664
3598
|
this._key,
|
|
2665
3599
|
this._bodySchema,
|
|
@@ -2675,6 +3609,8 @@ var RouteBuilder = class {
|
|
|
2675
3609
|
authMode: this._authMode,
|
|
2676
3610
|
siwxEnabled: this._siwxEnabled,
|
|
2677
3611
|
pricing: this._pricing,
|
|
3612
|
+
dynamicPrice: this._dynamicPrice ? true : void 0,
|
|
3613
|
+
streaming: streaming ? true : void 0,
|
|
2678
3614
|
protocols: this._protocols,
|
|
2679
3615
|
bodySchema: this._bodySchema,
|
|
2680
3616
|
querySchema: this._querySchema,
|
|
@@ -2692,81 +3628,28 @@ var RouteBuilder = class {
|
|
|
2692
3628
|
providerConfig: this._providerConfig,
|
|
2693
3629
|
validateFn: this._validateFn,
|
|
2694
3630
|
settlement: this._settlement,
|
|
2695
|
-
mppInfo: this._mppInfo
|
|
3631
|
+
mppInfo: this._mppInfo,
|
|
3632
|
+
tickCost: this._tickCost,
|
|
3633
|
+
unitType: this._unitType
|
|
2696
3634
|
};
|
|
2697
3635
|
this._registry.register(entry);
|
|
2698
|
-
return createRequestHandler(
|
|
2699
|
-
entry,
|
|
2700
|
-
handlerFn,
|
|
2701
|
-
this._deps
|
|
2702
|
-
);
|
|
3636
|
+
return createRequestHandler(entry, handlerFn, this._deps);
|
|
2703
3637
|
}
|
|
2704
3638
|
};
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
if (!wallets) return false;
|
|
2712
|
-
return wallets.has(normalizeWalletAddress(wallet));
|
|
2713
|
-
}
|
|
2714
|
-
async grant(route, wallet) {
|
|
2715
|
-
const normalized = normalizeWalletAddress(wallet);
|
|
2716
|
-
let wallets = this.routeToWallets.get(route);
|
|
2717
|
-
if (!wallets) {
|
|
2718
|
-
wallets = /* @__PURE__ */ new Set();
|
|
2719
|
-
this.routeToWallets.set(route, wallets);
|
|
3639
|
+
function resolvePaidArgs(routeKey, pricingOrOptions, options) {
|
|
3640
|
+
const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
|
|
3641
|
+
if (isHandlerDynamicShape) {
|
|
3642
|
+
const opts = pricingOrOptions;
|
|
3643
|
+
if (!opts.maxPrice) {
|
|
3644
|
+
throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
|
|
2720
3645
|
}
|
|
2721
|
-
|
|
2722
|
-
}
|
|
2723
|
-
};
|
|
2724
|
-
function detectRedisClientType2(client) {
|
|
2725
|
-
if (!client || typeof client !== "object") {
|
|
2726
|
-
throw new Error(
|
|
2727
|
-
"createRedisEntitlementStore requires a Redis client. Supported: @upstash/redis, ioredis."
|
|
2728
|
-
);
|
|
3646
|
+
return { pricing: opts.maxPrice, resolvedOptions: opts };
|
|
2729
3647
|
}
|
|
2730
|
-
|
|
2731
|
-
const constructor = client.constructor?.name;
|
|
2732
|
-
if (constructor === "Redis" && "url" in client) return "upstash";
|
|
2733
|
-
if (typeof client.sadd === "function" && typeof client.sismember === "function") {
|
|
2734
|
-
return "upstash";
|
|
2735
|
-
}
|
|
2736
|
-
throw new Error("Unrecognized Redis client for entitlement store.");
|
|
2737
|
-
}
|
|
2738
|
-
function createRedisEntitlementStore(client, options) {
|
|
2739
|
-
const clientType = detectRedisClientType2(client);
|
|
2740
|
-
const prefix = options?.prefix ?? "siwx:entitlement:";
|
|
2741
|
-
return {
|
|
2742
|
-
async has(route, wallet) {
|
|
2743
|
-
const key = `${prefix}${route}`;
|
|
2744
|
-
const normalized = normalizeWalletAddress(wallet);
|
|
2745
|
-
if (clientType === "upstash") {
|
|
2746
|
-
const redis2 = client;
|
|
2747
|
-
const result2 = await redis2.sismember(key, normalized);
|
|
2748
|
-
return result2 === 1 || result2 === true;
|
|
2749
|
-
}
|
|
2750
|
-
const redis = client;
|
|
2751
|
-
const result = await redis.sismember(key, normalized);
|
|
2752
|
-
return result === 1;
|
|
2753
|
-
},
|
|
2754
|
-
async grant(route, wallet) {
|
|
2755
|
-
const key = `${prefix}${route}`;
|
|
2756
|
-
const normalized = normalizeWalletAddress(wallet);
|
|
2757
|
-
if (clientType === "upstash") {
|
|
2758
|
-
const redis2 = client;
|
|
2759
|
-
await redis2.sadd(key, normalized);
|
|
2760
|
-
return;
|
|
2761
|
-
}
|
|
2762
|
-
const redis = client;
|
|
2763
|
-
await redis.sadd(key, normalized);
|
|
2764
|
-
}
|
|
2765
|
-
};
|
|
3648
|
+
return { pricing: pricingOrOptions, resolvedOptions: options };
|
|
2766
3649
|
}
|
|
2767
3650
|
|
|
2768
3651
|
// src/discovery/well-known.ts
|
|
2769
|
-
import { NextResponse as
|
|
3652
|
+
import { NextResponse as NextResponse8 } from "next/server";
|
|
2770
3653
|
|
|
2771
3654
|
// src/discovery/utils/guidance.ts
|
|
2772
3655
|
async function resolveGuidance(discovery) {
|
|
@@ -2810,7 +3693,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2810
3693
|
if (instructions) {
|
|
2811
3694
|
body.instructions = instructions;
|
|
2812
3695
|
}
|
|
2813
|
-
return
|
|
3696
|
+
return NextResponse8.json(body, {
|
|
2814
3697
|
headers: {
|
|
2815
3698
|
"Access-Control-Allow-Origin": "*",
|
|
2816
3699
|
"Access-Control-Allow-Methods": "GET",
|
|
@@ -2828,13 +3711,13 @@ function toDiscoveryResource(method, url, mode) {
|
|
|
2828
3711
|
|
|
2829
3712
|
// src/discovery/openapi.ts
|
|
2830
3713
|
init_constants();
|
|
2831
|
-
import { NextResponse as
|
|
3714
|
+
import { NextResponse as NextResponse9 } from "next/server";
|
|
2832
3715
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
2833
3716
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
2834
3717
|
let cached = null;
|
|
2835
3718
|
let validated = false;
|
|
2836
3719
|
return async (_request) => {
|
|
2837
|
-
if (cached) return
|
|
3720
|
+
if (cached) return NextResponse9.json(cached);
|
|
2838
3721
|
if (!validated && pricesKeys) {
|
|
2839
3722
|
registry.validate(pricesKeys);
|
|
2840
3723
|
validated = true;
|
|
@@ -2897,7 +3780,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
2897
3780
|
paths
|
|
2898
3781
|
};
|
|
2899
3782
|
cached = createDocument(openApiDocument);
|
|
2900
|
-
return
|
|
3783
|
+
return NextResponse9.json(cached);
|
|
2901
3784
|
};
|
|
2902
3785
|
}
|
|
2903
3786
|
function deriveTag(routeKey) {
|
|
@@ -2970,7 +3853,7 @@ function toProtocolObject(protocol, mppInfo) {
|
|
|
2970
3853
|
mpp: {
|
|
2971
3854
|
method: mppInfo?.method ?? "tempo",
|
|
2972
3855
|
intent: mppInfo?.intent ?? "charge",
|
|
2973
|
-
currency: mppInfo?.currency ??
|
|
3856
|
+
currency: mppInfo?.currency ?? TEMPO_USDC_ADDRESS
|
|
2974
3857
|
}
|
|
2975
3858
|
};
|
|
2976
3859
|
}
|
|
@@ -3020,11 +3903,11 @@ function buildPricingInfo(entry) {
|
|
|
3020
3903
|
}
|
|
3021
3904
|
|
|
3022
3905
|
// src/discovery/llms-txt.ts
|
|
3023
|
-
import { NextResponse as
|
|
3906
|
+
import { NextResponse as NextResponse10 } from "next/server";
|
|
3024
3907
|
function createLlmsTxtHandler(discovery) {
|
|
3025
3908
|
return async (_request) => {
|
|
3026
3909
|
const guidance = await resolveGuidance(discovery) ?? "";
|
|
3027
|
-
return new
|
|
3910
|
+
return new NextResponse10(guidance, {
|
|
3028
3911
|
headers: {
|
|
3029
3912
|
"Content-Type": "text/plain; charset=utf-8",
|
|
3030
3913
|
"Access-Control-Allow-Origin": "*"
|
|
@@ -3037,11 +3920,7 @@ function createLlmsTxtHandler(discovery) {
|
|
|
3037
3920
|
init_accepts();
|
|
3038
3921
|
init_constants();
|
|
3039
3922
|
|
|
3040
|
-
// src/config.ts
|
|
3041
|
-
init_constants();
|
|
3042
|
-
init_evm();
|
|
3043
|
-
init_solana();
|
|
3044
|
-
init_accepts();
|
|
3923
|
+
// src/config/error.ts
|
|
3045
3924
|
var RouterConfigError = class extends Error {
|
|
3046
3925
|
issues;
|
|
3047
3926
|
constructor(issues) {
|
|
@@ -3050,184 +3929,260 @@ var RouterConfigError = class extends Error {
|
|
|
3050
3929
|
this.issues = issues;
|
|
3051
3930
|
}
|
|
3052
3931
|
};
|
|
3053
|
-
function
|
|
3054
|
-
|
|
3055
|
-
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
3932
|
+
function formatRouterConfigIssues(issues) {
|
|
3933
|
+
return issues.map((issue) => issue.message).join("\n");
|
|
3056
3934
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3935
|
+
|
|
3936
|
+
// src/config/schema.ts
|
|
3937
|
+
init_constants();
|
|
3938
|
+
import { z } from "zod";
|
|
3939
|
+
|
|
3940
|
+
// src/config/utils.ts
|
|
3941
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
3942
|
+
var EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
3943
|
+
var EVM_PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
|
|
3944
|
+
var SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
3945
|
+
var ZERO_EVM_ADDRESS_RE = /^0x0{40}$/i;
|
|
3946
|
+
function isUrl(value) {
|
|
3947
|
+
try {
|
|
3948
|
+
new URL(value);
|
|
3949
|
+
return true;
|
|
3950
|
+
} catch {
|
|
3951
|
+
return false;
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
var isEvmAddress = (v) => EVM_ADDRESS_RE.test(v);
|
|
3955
|
+
var isEvmPrivateKey = (v) => EVM_PRIVATE_KEY_RE.test(v);
|
|
3956
|
+
var isPlaceholderEvm = (v) => ZERO_EVM_ADDRESS_RE.test(v);
|
|
3957
|
+
var isSolanaAddress = (v) => SOLANA_ADDRESS_RE.test(v);
|
|
3958
|
+
var isX402Network = (v) => v.startsWith("eip155:") || v.startsWith("solana:");
|
|
3959
|
+
var canonicalizeEvm = (addr) => addr.toLowerCase();
|
|
3960
|
+
function operatorAddressesCollide(opKey, fpKey) {
|
|
3961
|
+
if (!opKey || !fpKey || !isEvmPrivateKey(opKey) || !isEvmPrivateKey(fpKey)) return null;
|
|
3962
|
+
const op = privateKeyToAccount(opKey).address.toLowerCase();
|
|
3963
|
+
const fp = privateKeyToAccount(fpKey).address.toLowerCase();
|
|
3964
|
+
return op === fp ? op : null;
|
|
3965
|
+
}
|
|
3966
|
+
function trimAll(raw) {
|
|
3967
|
+
const out = {};
|
|
3968
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
3969
|
+
if (typeof v !== "string") {
|
|
3970
|
+
out[k] = void 0;
|
|
3971
|
+
continue;
|
|
3972
|
+
}
|
|
3973
|
+
const trimmed = v.trim();
|
|
3974
|
+
out[k] = trimmed.length > 0 ? trimmed : void 0;
|
|
3066
3975
|
}
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3976
|
+
return out;
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
// src/config/schema.ts
|
|
3980
|
+
function addIssue(ctx, params, message, path = []) {
|
|
3981
|
+
ctx.addIssue({ code: "custom", path, params, message });
|
|
3982
|
+
}
|
|
3983
|
+
var x402 = { protocol: "x402" };
|
|
3984
|
+
var mpp = { protocol: "mpp" };
|
|
3985
|
+
var envShape = {
|
|
3986
|
+
BASE_URL: z.string().refine(isUrl, {
|
|
3987
|
+
params: { code: "invalid_base_url" },
|
|
3988
|
+
message: "BASE_URL must be a valid URL \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain."
|
|
3989
|
+
}).optional(),
|
|
3990
|
+
EVM_PAYEE_ADDRESS: z.string().refine(isEvmAddress, {
|
|
3991
|
+
params: { code: "invalid_x402_payee", ...x402 },
|
|
3992
|
+
message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
|
|
3993
|
+
}).refine((v) => !isPlaceholderEvm(v), {
|
|
3994
|
+
params: { code: "placeholder_payee", ...x402 },
|
|
3995
|
+
message: "EVM_PAYEE_ADDRESS is the zero address (0x000\u2026000) \u2014 payments to this address are unrecoverable. Set it to a wallet you control."
|
|
3996
|
+
}).optional(),
|
|
3997
|
+
CDP_API_KEY_ID: z.string().optional(),
|
|
3998
|
+
CDP_API_KEY_SECRET: z.string().optional(),
|
|
3999
|
+
SOLANA_PAYEE_ADDRESS: z.string().refine(isSolanaAddress, {
|
|
4000
|
+
params: { code: "invalid_solana_payee", ...x402 },
|
|
4001
|
+
message: "SOLANA_PAYEE_ADDRESS must be a base58 Solana address (32\u201344 chars). When set, the router also accepts Solana payments."
|
|
4002
|
+
}).optional(),
|
|
4003
|
+
SOLANA_FACILITATOR_URL: z.string().refine(isUrl, {
|
|
4004
|
+
params: { code: "invalid_solana_facilitator_url", ...x402 },
|
|
4005
|
+
message: "SOLANA_FACILITATOR_URL must be a valid URL \u2014 override for the Solana x402 facilitator. Defaults to DEFAULT_SOLANA_FACILITATOR_URL."
|
|
4006
|
+
}).optional(),
|
|
4007
|
+
MPP_SECRET_KEY: z.string().optional(),
|
|
4008
|
+
MPP_CURRENCY: z.string().refine(isEvmAddress, {
|
|
4009
|
+
params: { code: "invalid_mpp_currency", ...mpp },
|
|
4010
|
+
message: "MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address \u2014 the token contract MPP charges in. Use TEMPO_USDC_ADDRESS for Tempo USDC."
|
|
4011
|
+
}).optional(),
|
|
4012
|
+
TEMPO_RPC_URL: z.string().refine(isUrl, {
|
|
4013
|
+
params: { code: "invalid_mpp_rpc_url", ...mpp },
|
|
4014
|
+
message: "TEMPO_RPC_URL must be a valid URL \u2014 authenticated Tempo JSON-RPC endpoint. Public rpc.tempo.xyz returns 401."
|
|
4015
|
+
}).optional(),
|
|
4016
|
+
MPP_OPERATOR_KEY: z.string().refine(isEvmPrivateKey, {
|
|
4017
|
+
params: { code: "invalid_mpp_operator_key", ...mpp },
|
|
4018
|
+
message: "MPP_OPERATOR_KEY must be a 0x-prefixed 32-byte EVM private key \u2014 signs server-side close/settle; presence enables MPP session mode."
|
|
4019
|
+
}).optional(),
|
|
4020
|
+
MPP_FEE_PAYER_KEY: z.string().refine(isEvmPrivateKey, {
|
|
4021
|
+
params: { code: "invalid_mpp_fee_payer_key", ...mpp },
|
|
4022
|
+
message: "MPP_FEE_PAYER_KEY must be a 0x-prefixed 32-byte EVM private key \u2014 sponsors client gas for channel open/topUp. Must resolve to a different address than MPP_OPERATOR_KEY."
|
|
4023
|
+
}).optional(),
|
|
4024
|
+
KV_REST_API_URL: z.string().optional(),
|
|
4025
|
+
KV_REST_API_TOKEN: z.string().optional(),
|
|
4026
|
+
NODE_ENV: z.string().optional()
|
|
4027
|
+
};
|
|
4028
|
+
var ENV_KEYS = Object.keys(envShape);
|
|
4029
|
+
var EnvInputSchema = z.object(envShape).passthrough().superRefine((env, ctx) => {
|
|
4030
|
+
if (env.BASE_URL === void 0) {
|
|
4031
|
+
addIssue(
|
|
4032
|
+
ctx,
|
|
4033
|
+
{ code: "missing_base_url" },
|
|
4034
|
+
"BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain.",
|
|
4035
|
+
["BASE_URL"]
|
|
4036
|
+
);
|
|
4037
|
+
}
|
|
4038
|
+
if (env.EVM_PAYEE_ADDRESS === void 0) {
|
|
4039
|
+
addIssue(
|
|
4040
|
+
ctx,
|
|
4041
|
+
{ code: "missing_x402_payee", ...x402 },
|
|
4042
|
+
"EVM_PAYEE_ADDRESS is required \u2014 the EVM address that receives x402 and MPP payments.",
|
|
4043
|
+
["EVM_PAYEE_ADDRESS"]
|
|
4044
|
+
);
|
|
3072
4045
|
}
|
|
3073
|
-
if (
|
|
3074
|
-
|
|
4046
|
+
if (env.MPP_SECRET_KEY) {
|
|
4047
|
+
if (env.MPP_CURRENCY === void 0) {
|
|
4048
|
+
addIssue(
|
|
4049
|
+
ctx,
|
|
4050
|
+
{ code: "missing_mpp_currency", ...mpp },
|
|
4051
|
+
"MPP_CURRENCY is required when MPP is enabled \u2014 the Tempo currency address MPP charges in. Use TEMPO_USDC_ADDRESS for Tempo USDC.",
|
|
4052
|
+
["MPP_CURRENCY"]
|
|
4053
|
+
);
|
|
4054
|
+
}
|
|
4055
|
+
if (env.TEMPO_RPC_URL === void 0) {
|
|
4056
|
+
addIssue(
|
|
4057
|
+
ctx,
|
|
4058
|
+
{ code: "missing_mpp_rpc_url", ...mpp },
|
|
4059
|
+
"TEMPO_RPC_URL is required when MPP is enabled \u2014 authenticated Tempo JSON-RPC endpoint. Public rpc.tempo.xyz returns 401.",
|
|
4060
|
+
["TEMPO_RPC_URL"]
|
|
4061
|
+
);
|
|
4062
|
+
}
|
|
3075
4063
|
}
|
|
3076
|
-
|
|
3077
|
-
|
|
4064
|
+
const collision = operatorAddressesCollide(env.MPP_OPERATOR_KEY, env.MPP_FEE_PAYER_KEY);
|
|
4065
|
+
if (collision) {
|
|
4066
|
+
addIssue(
|
|
4067
|
+
ctx,
|
|
4068
|
+
{ code: "mpp_operator_equals_fee_payer", ...mpp },
|
|
4069
|
+
`MPP_OPERATOR_KEY and MPP_FEE_PAYER_KEY resolve to the same address (${collision}). Tempo rejects fee-delegated txs with sender === feePayer. Use two distinct wallets, or unset MPP_FEE_PAYER_KEY to let clients pay their own gas.`,
|
|
4070
|
+
["MPP_FEE_PAYER_KEY"]
|
|
4071
|
+
);
|
|
3078
4072
|
}
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
currency ? null : "MPP_CURRENCY",
|
|
3095
|
-
rpcUrl ? null : "TEMPO_RPC_URL"
|
|
3096
|
-
].filter(Boolean);
|
|
3097
|
-
if (missing.length > 0) {
|
|
3098
|
-
throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
|
|
3099
|
-
}
|
|
3100
|
-
if (!isEvmAddress(currency)) {
|
|
3101
|
-
throw new Error("MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address");
|
|
3102
|
-
}
|
|
3103
|
-
if (options.recipient && !isEvmAddress(options.recipient)) {
|
|
3104
|
-
throw new Error("MPP recipient must be a 0x-prefixed EVM address");
|
|
3105
|
-
}
|
|
3106
|
-
if (feePayerKey && !isEvmPrivateKey(feePayerKey)) {
|
|
3107
|
-
throw new Error(`${feePayerKeySource} must be a 0x-prefixed 32-byte EVM private key`);
|
|
4073
|
+
});
|
|
4074
|
+
function collectKvWarnings(env, kvStoreOptionProvided) {
|
|
4075
|
+
if (kvStoreOptionProvided) return [];
|
|
4076
|
+
const warn = (code, message) => ({
|
|
4077
|
+
code,
|
|
4078
|
+
severity: "warning",
|
|
4079
|
+
message
|
|
4080
|
+
});
|
|
4081
|
+
if (env.KV_REST_API_URL && !env.KV_REST_API_TOKEN) {
|
|
4082
|
+
return [
|
|
4083
|
+
warn(
|
|
4084
|
+
"kv_url_without_token",
|
|
4085
|
+
"KV_REST_API_URL is set but KV_REST_API_TOKEN is missing \u2014 falling back to in-memory KV (unsafe in serverless production)."
|
|
4086
|
+
)
|
|
4087
|
+
];
|
|
3108
4088
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
};
|
|
3117
|
-
}
|
|
3118
|
-
function x402AcceptsFromEnv(env, options = {}) {
|
|
3119
|
-
const payeeEnv = options.payeeEnv ?? "X402_WALLET_ADDRESS";
|
|
3120
|
-
const solanaPayeeEnv = options.solanaPayeeEnv ?? "SOLANA_PAYEE_ADDRESS";
|
|
3121
|
-
const payeeAddress = options.payeeAddress ?? env[payeeEnv];
|
|
3122
|
-
if (!payeeAddress) {
|
|
3123
|
-
throw new Error(`${payeeEnv} is required to build x402 accepts`);
|
|
4089
|
+
if (env.KV_REST_API_TOKEN && !env.KV_REST_API_URL) {
|
|
4090
|
+
return [
|
|
4091
|
+
warn(
|
|
4092
|
+
"kv_token_without_url",
|
|
4093
|
+
"KV_REST_API_TOKEN is set but KV_REST_API_URL is missing \u2014 falling back to in-memory KV (unsafe in serverless production)."
|
|
4094
|
+
)
|
|
4095
|
+
];
|
|
3124
4096
|
}
|
|
3125
|
-
|
|
4097
|
+
if (env.KV_REST_API_URL && env.KV_REST_API_TOKEN && !isUrl(env.KV_REST_API_URL)) {
|
|
4098
|
+
return [
|
|
4099
|
+
warn(
|
|
4100
|
+
"invalid_kv_url",
|
|
4101
|
+
`KV_REST_API_URL is not a valid URL \u2014 KV calls will fail at request time. Got: ${env.KV_REST_API_URL}`
|
|
4102
|
+
)
|
|
4103
|
+
];
|
|
4104
|
+
}
|
|
4105
|
+
if (!env.KV_REST_API_URL && !env.KV_REST_API_TOKEN && env.NODE_ENV === "production") {
|
|
4106
|
+
return [
|
|
4107
|
+
warn(
|
|
4108
|
+
"missing_kv_in_production",
|
|
4109
|
+
"No KV_REST_API_URL/KV_REST_API_TOKEN set in production \u2014 using the in-memory KV store. SIWX nonce, SIWX entitlement, and MPP replay state will be lost across instances. Configure Upstash/Vercel KV or pass a custom kvStore."
|
|
4110
|
+
)
|
|
4111
|
+
];
|
|
4112
|
+
}
|
|
4113
|
+
return [];
|
|
4114
|
+
}
|
|
4115
|
+
function getConfiguredX402Accepts2(config) {
|
|
4116
|
+
if (config.x402?.accepts?.length) return [...config.x402.accepts];
|
|
4117
|
+
return [
|
|
3126
4118
|
{
|
|
3127
4119
|
scheme: "exact",
|
|
3128
|
-
network:
|
|
3129
|
-
payTo: payeeAddress
|
|
4120
|
+
network: config.network ?? BASE_MAINNET_NETWORK,
|
|
4121
|
+
payTo: config.payeeAddress
|
|
3130
4122
|
}
|
|
3131
4123
|
];
|
|
3132
|
-
const solanaPayeeAddress = options.solanaPayeeAddress ?? env[solanaPayeeEnv];
|
|
3133
|
-
if (solanaPayeeAddress) {
|
|
3134
|
-
accepts.push({
|
|
3135
|
-
scheme: "exact",
|
|
3136
|
-
network: SOLANA_MAINNET_NETWORK,
|
|
3137
|
-
payTo: solanaPayeeAddress
|
|
3138
|
-
});
|
|
3139
|
-
}
|
|
3140
|
-
return accepts;
|
|
3141
|
-
}
|
|
3142
|
-
function paidOptionsForProtocols(protocols) {
|
|
3143
|
-
return { protocols: [...protocols] };
|
|
3144
4124
|
}
|
|
3145
4125
|
function validateX402Config(config, env, options) {
|
|
4126
|
+
const accepts = getConfiguredX402Accepts2(config);
|
|
3146
4127
|
const issues = [];
|
|
3147
|
-
const
|
|
4128
|
+
const push = (code, message) => issues.push({ code, protocol: "x402", message });
|
|
3148
4129
|
if (accepts.length === 0) {
|
|
3149
|
-
|
|
3150
|
-
code: "missing_x402_accepts",
|
|
3151
|
-
protocol: "x402",
|
|
3152
|
-
message: "x402 requires at least one accept configuration."
|
|
3153
|
-
});
|
|
4130
|
+
push("missing_x402_accepts", "x402 requires at least one accept configuration.");
|
|
3154
4131
|
return issues;
|
|
3155
4132
|
}
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
issues.push({
|
|
3159
|
-
code: "missing_x402_network",
|
|
3160
|
-
protocol: "x402",
|
|
3161
|
-
message: "x402 accepts require a network."
|
|
3162
|
-
});
|
|
4133
|
+
if (accepts.some((a) => !a.network)) {
|
|
4134
|
+
push("missing_x402_network", "x402 accepts require a network.");
|
|
3163
4135
|
}
|
|
3164
|
-
const unsupported = accepts.find(
|
|
3165
|
-
(accept) => accept.network && !isSupportedX402Network(accept.network)
|
|
3166
|
-
);
|
|
4136
|
+
const unsupported = accepts.find((a) => a.network && !isX402Network(a.network));
|
|
3167
4137
|
if (unsupported) {
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
});
|
|
4138
|
+
push(
|
|
4139
|
+
"unsupported_x402_network",
|
|
4140
|
+
`unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
4141
|
+
);
|
|
3173
4142
|
}
|
|
3174
|
-
|
|
3175
|
-
(
|
|
3176
|
-
);
|
|
3177
|
-
if (missingAsset) {
|
|
3178
|
-
issues.push({
|
|
3179
|
-
code: "missing_x402_asset",
|
|
3180
|
-
protocol: "x402",
|
|
3181
|
-
message: "non-exact x402 accepts require an asset."
|
|
3182
|
-
});
|
|
4143
|
+
if (accepts.some((a) => (a.scheme ?? "exact") !== "exact" && !a.asset)) {
|
|
4144
|
+
push("missing_x402_asset", "non-exact x402 accepts require an asset.");
|
|
3183
4145
|
}
|
|
3184
|
-
|
|
3185
|
-
(
|
|
3186
|
-
)
|
|
3187
|
-
|
|
3188
|
-
issues.push({
|
|
3189
|
-
code: "invalid_x402_decimals",
|
|
3190
|
-
protocol: "x402",
|
|
3191
|
-
message: "x402 accept decimals must be a non-negative integer."
|
|
3192
|
-
});
|
|
4146
|
+
if (accepts.some(
|
|
4147
|
+
(a) => a.decimals !== void 0 && (!Number.isInteger(a.decimals) || a.decimals < 0)
|
|
4148
|
+
)) {
|
|
4149
|
+
push("invalid_x402_decimals", "x402 accept decimals must be a non-negative integer.");
|
|
3193
4150
|
}
|
|
3194
|
-
if (accepts.some((
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
});
|
|
4151
|
+
if (!config.payeeAddress && accepts.some((a) => !a.payTo)) {
|
|
4152
|
+
push(
|
|
4153
|
+
"missing_x402_payee",
|
|
4154
|
+
"x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
4155
|
+
);
|
|
3200
4156
|
}
|
|
3201
|
-
const placeholder =
|
|
4157
|
+
const placeholder = [
|
|
3202
4158
|
config.payeeAddress,
|
|
3203
|
-
...accepts.map((
|
|
3204
|
-
]);
|
|
4159
|
+
...accepts.map((a) => typeof a.payTo === "string" ? a.payTo : void 0)
|
|
4160
|
+
].find((v) => v !== void 0 && isPlaceholderEvm(v));
|
|
3205
4161
|
if (placeholder) {
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
});
|
|
4162
|
+
push(
|
|
4163
|
+
"placeholder_payee",
|
|
4164
|
+
`x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
4165
|
+
);
|
|
3211
4166
|
}
|
|
3212
|
-
if (options.requireCdpKeys !== false
|
|
3213
|
-
const
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
4167
|
+
if (options.requireCdpKeys !== false) {
|
|
4168
|
+
const hasEvm = accepts.some(
|
|
4169
|
+
(a) => typeof a.network === "string" && a.network.startsWith("eip155:")
|
|
4170
|
+
);
|
|
4171
|
+
if (hasEvm) {
|
|
4172
|
+
const missing = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET"].filter((k) => !env[k]);
|
|
4173
|
+
if (missing.length > 0) {
|
|
4174
|
+
push(
|
|
4175
|
+
"missing_cdp_keys",
|
|
4176
|
+
`x402 EVM facilitator (Coinbase) requires ${missing.join(" and ")}.`
|
|
4177
|
+
);
|
|
4178
|
+
}
|
|
3223
4179
|
}
|
|
3224
4180
|
}
|
|
3225
4181
|
return issues;
|
|
3226
4182
|
}
|
|
3227
|
-
function validateMppConfig(config
|
|
3228
|
-
const
|
|
3229
|
-
|
|
3230
|
-
if (!mpp) {
|
|
4183
|
+
function validateMppConfig(config) {
|
|
4184
|
+
const m = config.mpp;
|
|
4185
|
+
if (!m) {
|
|
3231
4186
|
return [
|
|
3232
4187
|
{
|
|
3233
4188
|
code: "missing_mpp_config",
|
|
@@ -3236,98 +4191,343 @@ function validateMppConfig(config, env) {
|
|
|
3236
4191
|
}
|
|
3237
4192
|
];
|
|
3238
4193
|
}
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
4194
|
+
const issues = [];
|
|
4195
|
+
const push = (code, message) => issues.push({ code, protocol: "mpp", message });
|
|
4196
|
+
if (!m.secretKey) {
|
|
4197
|
+
push(
|
|
4198
|
+
"missing_mpp_secret_key",
|
|
4199
|
+
"MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
4200
|
+
);
|
|
3245
4201
|
}
|
|
3246
|
-
if (!
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
4202
|
+
if (!m.currency) {
|
|
4203
|
+
push("missing_mpp_currency", "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency.");
|
|
4204
|
+
} else if (!isEvmAddress(m.currency)) {
|
|
4205
|
+
push(
|
|
4206
|
+
"invalid_mpp_currency",
|
|
4207
|
+
"MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_ADDRESS for Tempo USDC."
|
|
4208
|
+
);
|
|
4209
|
+
}
|
|
4210
|
+
const recipient = m.recipient ?? config.payeeAddress;
|
|
4211
|
+
if (!recipient) {
|
|
4212
|
+
push(
|
|
4213
|
+
"missing_mpp_recipient",
|
|
4214
|
+
"MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
4215
|
+
);
|
|
4216
|
+
} else if (!isEvmAddress(recipient)) {
|
|
4217
|
+
push(
|
|
4218
|
+
"invalid_mpp_recipient",
|
|
4219
|
+
"MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
4220
|
+
);
|
|
4221
|
+
}
|
|
4222
|
+
const placeholder = [m.recipient, config.payeeAddress].find(
|
|
4223
|
+
(v) => typeof v === "string" && isPlaceholderEvm(v)
|
|
4224
|
+
);
|
|
4225
|
+
if (placeholder) {
|
|
4226
|
+
push(
|
|
4227
|
+
"placeholder_payee",
|
|
4228
|
+
`MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
4229
|
+
);
|
|
4230
|
+
}
|
|
4231
|
+
if (!m.rpcUrl) {
|
|
4232
|
+
push(
|
|
4233
|
+
"missing_mpp_rpc_url",
|
|
4234
|
+
"MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
4237
|
+
if (m.feePayerKey && !isEvmPrivateKey(m.feePayerKey)) {
|
|
4238
|
+
push(
|
|
4239
|
+
"invalid_mpp_fee_payer_key",
|
|
4240
|
+
"MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
4241
|
+
);
|
|
4242
|
+
}
|
|
4243
|
+
if (m.operatorKey && !isEvmPrivateKey(m.operatorKey)) {
|
|
4244
|
+
push(
|
|
4245
|
+
"invalid_mpp_operator_key",
|
|
4246
|
+
"MPP operatorKey must be a 0x-prefixed 32-byte EVM private key."
|
|
4247
|
+
);
|
|
4248
|
+
}
|
|
4249
|
+
const collision = operatorAddressesCollide(m.operatorKey, m.feePayerKey);
|
|
4250
|
+
if (collision) {
|
|
4251
|
+
push(
|
|
4252
|
+
"mpp_operator_equals_fee_payer",
|
|
4253
|
+
`MPP operatorKey and feePayerKey resolve to the same address (${collision}). Tempo rejects fee-delegated txs with sender === feePayer, so channel close/settle would fail at runtime. Either use two distinct wallets, or omit feePayerKey to disable gas sponsorship (clients then pay their own gas).`
|
|
4254
|
+
);
|
|
4255
|
+
}
|
|
4256
|
+
return issues;
|
|
4257
|
+
}
|
|
4258
|
+
function translateZodIssues(error) {
|
|
4259
|
+
return error.issues.map((issue) => {
|
|
4260
|
+
const params = issue.params;
|
|
4261
|
+
if (!params?.code) {
|
|
4262
|
+
throw new Error(
|
|
4263
|
+
`[router] schema issue missing params.code (path=${issue.path.join(".")}, message=${issue.message}). Every refinement / addIssue call must set params.code.`
|
|
4264
|
+
);
|
|
4265
|
+
}
|
|
4266
|
+
return {
|
|
4267
|
+
code: params.code,
|
|
4268
|
+
message: issue.message,
|
|
4269
|
+
...params.protocol ? { protocol: params.protocol } : {},
|
|
4270
|
+
...params.severity ? { severity: params.severity } : {}
|
|
4271
|
+
};
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
function routerConfigFromEnv(options) {
|
|
4275
|
+
const rawEnv = options.env ?? process.env;
|
|
4276
|
+
const env = trimAll(rawEnv);
|
|
4277
|
+
const optionIssues = [];
|
|
4278
|
+
if (!options.title?.trim()) {
|
|
4279
|
+
optionIssues.push({
|
|
4280
|
+
code: "missing_discovery_title",
|
|
4281
|
+
message: "discovery `title` is required. Pass a short product name."
|
|
3257
4282
|
});
|
|
3258
4283
|
}
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
protocol: "mpp",
|
|
3264
|
-
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
4284
|
+
if (!options.description?.trim()) {
|
|
4285
|
+
optionIssues.push({
|
|
4286
|
+
code: "missing_discovery_description",
|
|
4287
|
+
message: "discovery `description` is required. One sentence is enough."
|
|
3265
4288
|
});
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
message: "
|
|
4289
|
+
}
|
|
4290
|
+
if (options.guidance === void 0) {
|
|
4291
|
+
optionIssues.push({
|
|
4292
|
+
code: "missing_discovery_guidance",
|
|
4293
|
+
message: "discovery `guidance` is required. Provide an empty string to opt out of `/llms.txt`."
|
|
3271
4294
|
});
|
|
3272
4295
|
}
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
protocol: "mpp",
|
|
3278
|
-
message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
4296
|
+
if (options.serverUrl !== void 0 && !isUrl(options.serverUrl)) {
|
|
4297
|
+
optionIssues.push({
|
|
4298
|
+
code: "invalid_server_url",
|
|
4299
|
+
message: `discovery \`serverUrl\` must be a valid URL. Got: ${options.serverUrl}`
|
|
3279
4300
|
});
|
|
3280
4301
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
4302
|
+
const parsed = EnvInputSchema.safeParse(env);
|
|
4303
|
+
const envIssues = parsed.success ? [] : translateZodIssues(parsed.error);
|
|
4304
|
+
const issues = [...envIssues, ...optionIssues];
|
|
4305
|
+
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
4306
|
+
for (const warning of collectKvWarnings(env, options.kvStore !== void 0)) {
|
|
4307
|
+
console.warn(`[router] ${warning.message}`);
|
|
4308
|
+
}
|
|
4309
|
+
const payeeAddress = canonicalizeEvm(env.EVM_PAYEE_ADDRESS);
|
|
4310
|
+
const accepts = [
|
|
4311
|
+
{ scheme: "exact", network: BASE_MAINNET_NETWORK, payTo: payeeAddress },
|
|
4312
|
+
{
|
|
4313
|
+
scheme: "upto",
|
|
4314
|
+
network: BASE_MAINNET_NETWORK,
|
|
4315
|
+
payTo: payeeAddress,
|
|
4316
|
+
asset: BASE_USDC_ADDRESS,
|
|
4317
|
+
decimals: BASE_USDC_DECIMALS
|
|
4318
|
+
}
|
|
4319
|
+
];
|
|
4320
|
+
if (env.SOLANA_PAYEE_ADDRESS) {
|
|
4321
|
+
accepts.push({
|
|
4322
|
+
scheme: "exact",
|
|
4323
|
+
network: SOLANA_MAINNET_NETWORK,
|
|
4324
|
+
payTo: env.SOLANA_PAYEE_ADDRESS
|
|
3286
4325
|
});
|
|
3287
4326
|
}
|
|
3288
|
-
|
|
4327
|
+
const configuredSolanaFacilitator = options.x402Facilitators?.solana;
|
|
4328
|
+
const solanaFacilitator = typeof configuredSolanaFacilitator === "string" ? configuredSolanaFacilitator : configuredSolanaFacilitator ?? env.SOLANA_FACILITATOR_URL ?? DEFAULT_SOLANA_FACILITATOR_URL;
|
|
4329
|
+
const mppEnabled = options.protocols?.includes("mpp") ?? Boolean(env.MPP_SECRET_KEY);
|
|
4330
|
+
const protocols = options.protocols ? [...options.protocols] : mppEnabled ? ["x402", "mpp"] : ["x402"];
|
|
4331
|
+
const mppConfig = mppEnabled ? {
|
|
4332
|
+
secretKey: env.MPP_SECRET_KEY,
|
|
4333
|
+
currency: canonicalizeEvm(env.MPP_CURRENCY),
|
|
4334
|
+
rpcUrl: env.TEMPO_RPC_URL,
|
|
4335
|
+
recipient: payeeAddress,
|
|
4336
|
+
...env.MPP_FEE_PAYER_KEY ? { feePayerKey: env.MPP_FEE_PAYER_KEY } : {},
|
|
4337
|
+
...env.MPP_OPERATOR_KEY ? { operatorKey: env.MPP_OPERATOR_KEY, session: {} } : {}
|
|
4338
|
+
} : void 0;
|
|
4339
|
+
return {
|
|
4340
|
+
payeeAddress,
|
|
4341
|
+
baseUrl: env.BASE_URL,
|
|
4342
|
+
network: BASE_MAINNET_NETWORK,
|
|
4343
|
+
protocols,
|
|
4344
|
+
x402: {
|
|
4345
|
+
accepts,
|
|
4346
|
+
facilitators: {
|
|
4347
|
+
...options.x402Facilitators,
|
|
4348
|
+
solana: solanaFacilitator
|
|
4349
|
+
}
|
|
4350
|
+
},
|
|
4351
|
+
...mppConfig ? { mpp: mppConfig } : {},
|
|
4352
|
+
discovery: {
|
|
4353
|
+
title: options.title,
|
|
4354
|
+
version: options.version ?? "1.0.0",
|
|
4355
|
+
description: options.description,
|
|
4356
|
+
guidance: options.guidance,
|
|
4357
|
+
...options.contact ? { contact: options.contact } : {},
|
|
4358
|
+
...options.ownershipProofs ? { ownershipProofs: options.ownershipProofs } : {},
|
|
4359
|
+
...options.methodHints ? { methodHints: options.methodHints } : {},
|
|
4360
|
+
...options.serverUrl ? { serverUrl: options.serverUrl } : {}
|
|
4361
|
+
},
|
|
4362
|
+
...options.prices ? { prices: options.prices } : {},
|
|
4363
|
+
...options.plugin ? { plugin: options.plugin } : {},
|
|
4364
|
+
...options.kvStore ? { kvStore: options.kvStore } : {},
|
|
4365
|
+
strictRoutes: options.strictRoutes ?? false
|
|
4366
|
+
};
|
|
4367
|
+
}
|
|
4368
|
+
function getRouterConfigIssues(config, options = {}) {
|
|
4369
|
+
const env = options.env ?? {};
|
|
4370
|
+
const protocols = config.protocols ?? ["x402"];
|
|
4371
|
+
const issues = [];
|
|
4372
|
+
if (!config.baseUrl) {
|
|
3289
4373
|
issues.push({
|
|
3290
|
-
code: "
|
|
3291
|
-
|
|
3292
|
-
message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
4374
|
+
code: "missing_base_url",
|
|
4375
|
+
message: '[router] baseUrl is required in RouterConfig. Set it to your production domain (e.g., "https://api.example.com"). The realm is used for payment matching and must be correct.'
|
|
3293
4376
|
});
|
|
3294
4377
|
}
|
|
3295
|
-
if (
|
|
4378
|
+
if (config.protocols && config.protocols.length === 0) {
|
|
3296
4379
|
issues.push({
|
|
3297
|
-
code: "
|
|
3298
|
-
|
|
3299
|
-
message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
4380
|
+
code: "empty_protocols",
|
|
4381
|
+
message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
3300
4382
|
});
|
|
3301
4383
|
}
|
|
4384
|
+
if (protocols.includes("x402")) issues.push(...validateX402Config(config, env, options));
|
|
4385
|
+
if (protocols.includes("mpp")) issues.push(...validateMppConfig(config));
|
|
3302
4386
|
return issues;
|
|
3303
4387
|
}
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
)
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
4388
|
+
|
|
4389
|
+
// src/init/x402.ts
|
|
4390
|
+
async function initX402(config, configError) {
|
|
4391
|
+
if (configError) return { initError: configError };
|
|
4392
|
+
try {
|
|
4393
|
+
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_x402_server(), x402_server_exports));
|
|
4394
|
+
const result = await createX402Server2(config);
|
|
4395
|
+
await result.initPromise;
|
|
4396
|
+
return {
|
|
4397
|
+
server: result.server,
|
|
4398
|
+
facilitatorsByNetwork: result.facilitatorsByNetwork
|
|
4399
|
+
};
|
|
4400
|
+
} catch (err) {
|
|
4401
|
+
return { initError: err instanceof Error ? err.message : String(err) };
|
|
4402
|
+
}
|
|
3311
4403
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
4404
|
+
|
|
4405
|
+
// src/init/mppx.ts
|
|
4406
|
+
function getMppxRequestContext(args) {
|
|
4407
|
+
const {
|
|
4408
|
+
Mppx,
|
|
4409
|
+
tempo,
|
|
4410
|
+
mppConfig,
|
|
4411
|
+
payeeAddress,
|
|
4412
|
+
getClient,
|
|
4413
|
+
feePayerAccount,
|
|
4414
|
+
resolvedStore,
|
|
4415
|
+
sessionEnabled,
|
|
4416
|
+
sharedSessionParams,
|
|
4417
|
+
realm
|
|
4418
|
+
} = args;
|
|
4419
|
+
const instance = Mppx.create({
|
|
4420
|
+
methods: [
|
|
4421
|
+
tempo.charge({
|
|
4422
|
+
currency: mppConfig.currency,
|
|
4423
|
+
recipient: mppConfig.recipient ?? payeeAddress,
|
|
4424
|
+
getClient,
|
|
4425
|
+
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
4426
|
+
...resolvedStore ? { store: resolvedStore } : {}
|
|
4427
|
+
}),
|
|
4428
|
+
...sessionEnabled ? [
|
|
4429
|
+
tempo.session({
|
|
4430
|
+
...sharedSessionParams,
|
|
4431
|
+
sse: false
|
|
4432
|
+
})
|
|
4433
|
+
] : []
|
|
4434
|
+
],
|
|
4435
|
+
secretKey: mppConfig.secretKey,
|
|
4436
|
+
realm
|
|
4437
|
+
});
|
|
4438
|
+
return instance;
|
|
4439
|
+
}
|
|
4440
|
+
function getMppxStreamingContext(args) {
|
|
4441
|
+
if (!args.sessionEnabled) return null;
|
|
4442
|
+
const { Mppx, tempo, mppConfig, sharedSessionParams, realm } = args;
|
|
4443
|
+
const instance = Mppx.create({
|
|
4444
|
+
methods: [
|
|
4445
|
+
tempo.session({
|
|
4446
|
+
...sharedSessionParams,
|
|
4447
|
+
sse: true
|
|
4448
|
+
})
|
|
4449
|
+
],
|
|
4450
|
+
secretKey: mppConfig.secretKey,
|
|
4451
|
+
realm
|
|
4452
|
+
});
|
|
4453
|
+
return instance;
|
|
3314
4454
|
}
|
|
3315
|
-
|
|
3316
|
-
|
|
4455
|
+
|
|
4456
|
+
// src/init/mpp.ts
|
|
4457
|
+
async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
|
|
4458
|
+
if (configError) return { initError: configError };
|
|
4459
|
+
if (!config.mpp) return {};
|
|
4460
|
+
try {
|
|
4461
|
+
const { Mppx, tempo } = await import("mppx/server");
|
|
4462
|
+
const { createClient, http } = await import("viem");
|
|
4463
|
+
const { tempo: tempoChain } = await import("viem/chains");
|
|
4464
|
+
const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
|
|
4465
|
+
const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
4466
|
+
const tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
|
|
4467
|
+
const getClient = async () => tempoClient;
|
|
4468
|
+
const operatorAccount = config.mpp.operatorKey ? privateKeyToAccount2(config.mpp.operatorKey) : void 0;
|
|
4469
|
+
const feePayerAccount = config.mpp.feePayerKey ? privateKeyToAccount2(config.mpp.feePayerKey) : void 0;
|
|
4470
|
+
if (config.mpp.session && operatorAccount) {
|
|
4471
|
+
assertOperatorMatchesRecipient(config, operatorAccount.address);
|
|
4472
|
+
}
|
|
4473
|
+
const resolvedStore = kvStore ? await createKvMppStore(kvStore) : void 0;
|
|
4474
|
+
const realm = new URL(resolvedBaseUrl).host;
|
|
4475
|
+
const mppConfig = config.mpp;
|
|
4476
|
+
const sessionEnabled = !!(mppConfig.session && operatorAccount);
|
|
4477
|
+
const sharedSessionParams = {
|
|
4478
|
+
currency: mppConfig.currency,
|
|
4479
|
+
decimals: 6,
|
|
4480
|
+
recipient: mppConfig.recipient ?? config.payeeAddress,
|
|
4481
|
+
getClient,
|
|
4482
|
+
...operatorAccount ? { account: operatorAccount } : {},
|
|
4483
|
+
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
4484
|
+
...resolvedStore ? { store: resolvedStore } : {}
|
|
4485
|
+
};
|
|
4486
|
+
const mppxArgs = {
|
|
4487
|
+
Mppx,
|
|
4488
|
+
tempo,
|
|
4489
|
+
mppConfig,
|
|
4490
|
+
payeeAddress: config.payeeAddress ?? "",
|
|
4491
|
+
getClient,
|
|
4492
|
+
feePayerAccount,
|
|
4493
|
+
resolvedStore,
|
|
4494
|
+
sessionEnabled,
|
|
4495
|
+
sharedSessionParams,
|
|
4496
|
+
realm
|
|
4497
|
+
};
|
|
4498
|
+
const primary = getMppxRequestContext(mppxArgs);
|
|
4499
|
+
const streaming = getMppxStreamingContext(mppxArgs);
|
|
4500
|
+
const mppx = {
|
|
4501
|
+
charge: primary.charge,
|
|
4502
|
+
...primary.session ? { sessionRequest: primary.session } : {},
|
|
4503
|
+
...streaming?.session ? { sessionStream: streaming.session } : {}
|
|
4504
|
+
};
|
|
4505
|
+
return { mppx, tempoClient };
|
|
4506
|
+
} catch (err) {
|
|
4507
|
+
return { initError: err instanceof Error ? err.message : String(err) };
|
|
4508
|
+
}
|
|
3317
4509
|
}
|
|
3318
|
-
function
|
|
3319
|
-
|
|
4510
|
+
function assertOperatorMatchesRecipient(config, operatorAddress) {
|
|
4511
|
+
const recipient = (config.mpp?.recipient ?? config.payeeAddress)?.toLowerCase();
|
|
4512
|
+
const opAddr = operatorAddress.toLowerCase();
|
|
4513
|
+
if (recipient && opAddr !== recipient) {
|
|
4514
|
+
throw new Error(
|
|
4515
|
+
`MPP session config mismatch: operator address ${operatorAddress} must equal recipient/payee ${recipient}. mppx's channel-close handler asserts sender === payee. Set mpp.operatorKey to the private key for ${recipient}, or set mpp.recipient/payeeAddress to ${operatorAddress}.`
|
|
4516
|
+
);
|
|
4517
|
+
}
|
|
3320
4518
|
}
|
|
3321
4519
|
|
|
3322
4520
|
// src/index.ts
|
|
3323
4521
|
init_constants();
|
|
3324
4522
|
function createRouter(config) {
|
|
3325
4523
|
const registry = new RouteRegistry();
|
|
3326
|
-
const
|
|
3327
|
-
const
|
|
3328
|
-
const
|
|
4524
|
+
const kvStore = resolveKvStore(config.kvStore);
|
|
4525
|
+
const nonceStore = kvStore ? createKvNonceStore(kvStore) : new MemoryNonceStore();
|
|
4526
|
+
const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
|
|
4527
|
+
const network = config.network ?? BASE_MAINNET_NETWORK;
|
|
3329
4528
|
const x402Accepts = getConfiguredX402Accepts(config);
|
|
3330
4529
|
const configIssues = getRouterConfigIssues(config, {
|
|
4530
|
+
env: process.env,
|
|
3331
4531
|
requireCdpKeys: process.env.NODE_ENV === "production"
|
|
3332
4532
|
});
|
|
3333
4533
|
const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
|
|
@@ -3370,69 +4570,20 @@ function createRouter(config) {
|
|
|
3370
4570
|
x402FacilitatorsByNetwork: void 0,
|
|
3371
4571
|
x402Accepts,
|
|
3372
4572
|
mppx: null,
|
|
3373
|
-
tempoClient: null
|
|
4573
|
+
tempoClient: null,
|
|
4574
|
+
mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
|
|
3374
4575
|
};
|
|
3375
4576
|
deps.initPromise = (async () => {
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
deps.x402Server = null;
|
|
3387
|
-
deps.x402InitError = err instanceof Error ? err.message : String(err);
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
if (mppConfigError) {
|
|
3391
|
-
deps.mppInitError = mppConfigError;
|
|
3392
|
-
} else if (config.mpp) {
|
|
3393
|
-
try {
|
|
3394
|
-
const { Mppx, tempo } = await import("mppx/server");
|
|
3395
|
-
const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
3396
|
-
const { createClient, http } = await import("viem");
|
|
3397
|
-
const { tempo: tempoChain } = await import("viem/chains");
|
|
3398
|
-
deps.tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
|
|
3399
|
-
const getClient = async () => deps.tempoClient;
|
|
3400
|
-
let feePayerAccount;
|
|
3401
|
-
if (config.mpp.feePayerKey) {
|
|
3402
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
3403
|
-
feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
|
|
3404
|
-
}
|
|
3405
|
-
let resolvedStore = config.mpp.store;
|
|
3406
|
-
if (!resolvedStore && config.mpp.useDefaultStore) {
|
|
3407
|
-
const kvUrl = process.env.KV_REST_API_URL;
|
|
3408
|
-
const kvToken = process.env.KV_REST_API_TOKEN;
|
|
3409
|
-
if (!kvUrl || !kvToken) {
|
|
3410
|
-
throw new Error(
|
|
3411
|
-
"mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
3412
|
-
);
|
|
3413
|
-
}
|
|
3414
|
-
const { Store } = await import("mppx");
|
|
3415
|
-
const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
|
|
3416
|
-
resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
|
|
3417
|
-
}
|
|
3418
|
-
deps.mppx = Mppx.create({
|
|
3419
|
-
methods: [
|
|
3420
|
-
tempo.charge({
|
|
3421
|
-
currency: config.mpp.currency,
|
|
3422
|
-
recipient: config.mpp.recipient ?? config.payeeAddress,
|
|
3423
|
-
getClient,
|
|
3424
|
-
...feePayerAccount ? { feePayer: feePayerAccount } : {},
|
|
3425
|
-
...resolvedStore ? { store: resolvedStore } : {}
|
|
3426
|
-
})
|
|
3427
|
-
],
|
|
3428
|
-
secretKey: config.mpp.secretKey,
|
|
3429
|
-
realm: new URL(resolvedBaseUrl).host
|
|
3430
|
-
});
|
|
3431
|
-
} catch (err) {
|
|
3432
|
-
deps.mppx = null;
|
|
3433
|
-
deps.mppInitError = err instanceof Error ? err.message : String(err);
|
|
3434
|
-
console.error(`[router] MPP initialization failed: ${deps.mppInitError}`);
|
|
3435
|
-
}
|
|
4577
|
+
const x402Result = await initX402(config, x402ConfigError);
|
|
4578
|
+
deps.x402Server = x402Result.server ?? null;
|
|
4579
|
+
deps.x402FacilitatorsByNetwork = x402Result.facilitatorsByNetwork;
|
|
4580
|
+
if (x402Result.initError) deps.x402InitError = x402Result.initError;
|
|
4581
|
+
const mppResult = await initMpp(config, resolvedBaseUrl, kvStore, mppConfigError);
|
|
4582
|
+
deps.mppx = mppResult.mppx ?? null;
|
|
4583
|
+
deps.tempoClient = mppResult.tempoClient ?? null;
|
|
4584
|
+
if (mppResult.initError) {
|
|
4585
|
+
deps.mppInitError = mppResult.initError;
|
|
4586
|
+
console.error(`[router] MPP initialization failed: ${mppResult.initError}`);
|
|
3436
4587
|
}
|
|
3437
4588
|
})();
|
|
3438
4589
|
const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
|
|
@@ -3499,27 +4650,21 @@ function normalizePath(path) {
|
|
|
3499
4650
|
normalized = normalized.replace(/^api\/+/, "");
|
|
3500
4651
|
return normalized.replace(/\/+$/, "");
|
|
3501
4652
|
}
|
|
4653
|
+
function createRouterFromEnv(options) {
|
|
4654
|
+
return createRouter(routerConfigFromEnv(options));
|
|
4655
|
+
}
|
|
3502
4656
|
export {
|
|
3503
|
-
|
|
4657
|
+
BASE_MAINNET_NETWORK,
|
|
4658
|
+
BASE_USDC_ADDRESS,
|
|
4659
|
+
BASE_USDC_DECIMALS,
|
|
4660
|
+
DEFAULT_SOLANA_FACILITATOR_URL,
|
|
3504
4661
|
HttpError,
|
|
3505
|
-
MemoryEntitlementStore,
|
|
3506
|
-
MemoryNonceStore,
|
|
3507
|
-
RouteBuilder,
|
|
3508
|
-
RouteRegistry,
|
|
3509
4662
|
RouterConfigError,
|
|
3510
|
-
SIWX_CHALLENGE_EXPIRY_MS,
|
|
3511
|
-
SIWX_ERROR_MESSAGES,
|
|
3512
4663
|
SOLANA_MAINNET_NETWORK,
|
|
3513
|
-
|
|
4664
|
+
TEMPO_USDC_ADDRESS,
|
|
4665
|
+
TEMPO_USDC_DECIMALS,
|
|
3514
4666
|
ZERO_EVM_ADDRESS,
|
|
3515
|
-
consolePlugin,
|
|
3516
|
-
createRedisEntitlementStore,
|
|
3517
|
-
createRedisNonceStore,
|
|
3518
4667
|
createRouter,
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
mppFromEnv,
|
|
3522
|
-
paidOptionsForProtocols,
|
|
3523
|
-
validateRouterConfig,
|
|
3524
|
-
x402AcceptsFromEnv
|
|
4668
|
+
createRouterFromEnv,
|
|
4669
|
+
routerConfigFromEnv
|
|
3525
4670
|
};
|