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