@agentcash/router 1.6.0 → 1.7.1
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 +133 -550
- package/dist/index.cjs +993 -824
- package/dist/index.d.cts +96 -101
- package/dist/index.d.ts +96 -101
- package/dist/index.js +985 -805
- package/package.json +6 -14
- package/.claude/CLAUDE.md +0 -343
- package/.claude/skills/router-guide/SKILL.md +0 -585
package/dist/index.js
CHANGED
|
@@ -9,14 +9,18 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/constants.ts
|
|
12
|
-
var
|
|
12
|
+
var BASE_MAINNET_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_ADDRESS, TEMPO_USDC_DECIMALS, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, ZERO_EVM_ADDRESS, DEFAULT_SOLANA_FACILITATOR_URL;
|
|
13
13
|
var init_constants = __esm({
|
|
14
14
|
"src/constants.ts"() {
|
|
15
15
|
"use strict";
|
|
16
|
-
|
|
16
|
+
BASE_MAINNET_NETWORK = "eip155:8453";
|
|
17
17
|
SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
18
|
-
|
|
18
|
+
TEMPO_USDC_ADDRESS = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
19
|
+
TEMPO_USDC_DECIMALS = 6;
|
|
20
|
+
BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
21
|
+
BASE_USDC_DECIMALS = 6;
|
|
19
22
|
ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
23
|
+
DEFAULT_SOLANA_FACILITATOR_URL = "https://facilitator.corbits.dev";
|
|
20
24
|
}
|
|
21
25
|
});
|
|
22
26
|
|
|
@@ -33,7 +37,7 @@ function getConfiguredX402Accepts(config) {
|
|
|
33
37
|
return [
|
|
34
38
|
{
|
|
35
39
|
scheme: "exact",
|
|
36
|
-
network: config.network ??
|
|
40
|
+
network: config.network ?? BASE_MAINNET_NETWORK,
|
|
37
41
|
payTo: config.payeeAddress
|
|
38
42
|
}
|
|
39
43
|
];
|
|
@@ -213,7 +217,10 @@ async function getAcceptsHeadersForFacilitator(facilitator) {
|
|
|
213
217
|
return {};
|
|
214
218
|
}
|
|
215
219
|
function resolveX402FacilitatorTarget(config, network, defaultEvmFacilitator) {
|
|
216
|
-
|
|
220
|
+
if (isSolanaNetwork(network)) {
|
|
221
|
+
return config.x402?.facilitators?.solana ?? DEFAULT_SOLANA_FACILITATOR_URL;
|
|
222
|
+
}
|
|
223
|
+
return defaultEvmFacilitator;
|
|
217
224
|
}
|
|
218
225
|
function normalizeFacilitatorTarget(target) {
|
|
219
226
|
return typeof target === "string" ? { url: target } : target;
|
|
@@ -226,22 +233,148 @@ function getNetworkFamily(network) {
|
|
|
226
233
|
function sameFacilitatorConfig(a, b) {
|
|
227
234
|
return a.url === b.url && a.createAuthHeaders === b.createAuthHeaders && a.createAcceptsHeaders === b.createAcceptsHeaders;
|
|
228
235
|
}
|
|
229
|
-
var DEFAULT_SOLANA_FACILITATOR_URL;
|
|
230
236
|
var init_facilitators = __esm({
|
|
231
237
|
"src/protocols/x402/facilitators.ts"() {
|
|
232
238
|
"use strict";
|
|
233
239
|
init_evm();
|
|
234
240
|
init_solana();
|
|
235
|
-
|
|
241
|
+
init_constants();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// src/kv-store/facilitator-supported.ts
|
|
246
|
+
function withCachedSupported(inner, options = {}) {
|
|
247
|
+
const { kv, cacheKey, ttlSeconds = FACILITATOR_SUPPORTED_TTL_SECONDS, fallback } = options;
|
|
248
|
+
const kvKey = kv && cacheKey ? `${FACILITATOR_SUPPORTED_KV_PREFIX}${cacheKey}` : void 0;
|
|
249
|
+
let inflight;
|
|
250
|
+
return {
|
|
251
|
+
verify: inner.verify.bind(inner),
|
|
252
|
+
settle: inner.settle.bind(inner),
|
|
253
|
+
getSupported: () => {
|
|
254
|
+
if (inflight) return inflight;
|
|
255
|
+
const attempt = fetchSupported(inner, kv, kvKey, ttlSeconds, fallback);
|
|
256
|
+
inflight = attempt;
|
|
257
|
+
attempt.catch(() => {
|
|
258
|
+
if (inflight === attempt) inflight = void 0;
|
|
259
|
+
});
|
|
260
|
+
return attempt;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function fetchSupported(inner, kv, kvKey, ttlSeconds, fallback) {
|
|
265
|
+
if (kv && kvKey) {
|
|
266
|
+
const cached = await readKvCache(kv, kvKey);
|
|
267
|
+
if (cached) return cached;
|
|
268
|
+
}
|
|
269
|
+
const fresh = await tryFetchLive(inner, fallback);
|
|
270
|
+
if (fresh === null) return fallback();
|
|
271
|
+
if (kv && kvKey) await writeKvCache(kv, kvKey, fresh, ttlSeconds);
|
|
272
|
+
return fresh;
|
|
273
|
+
}
|
|
274
|
+
async function tryFetchLive(inner, fallback) {
|
|
275
|
+
try {
|
|
276
|
+
return await inner.getSupported();
|
|
277
|
+
} catch (err) {
|
|
278
|
+
if (!fallback) throw err;
|
|
279
|
+
console.warn(
|
|
280
|
+
`[x402] facilitator /supported failed, using hardcoded baseline: ${err instanceof Error ? err.message : String(err)}`
|
|
281
|
+
);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function readKvCache(kv, key) {
|
|
286
|
+
try {
|
|
287
|
+
const cached = await kv.get(key);
|
|
288
|
+
return isSupportedResponse(cached) ? cached : void 0;
|
|
289
|
+
} catch {
|
|
290
|
+
return void 0;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function writeKvCache(kv, key, value, ttlSeconds) {
|
|
294
|
+
try {
|
|
295
|
+
await kv.setNxEx(key, value, ttlSeconds);
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function isSupportedResponse(value) {
|
|
300
|
+
return typeof value === "object" && value !== null && Array.isArray(value.kinds);
|
|
301
|
+
}
|
|
302
|
+
var FACILITATOR_SUPPORTED_TTL_SECONDS, FACILITATOR_SUPPORTED_KV_PREFIX;
|
|
303
|
+
var init_facilitator_supported = __esm({
|
|
304
|
+
"src/kv-store/facilitator-supported.ts"() {
|
|
305
|
+
"use strict";
|
|
306
|
+
FACILITATOR_SUPPORTED_TTL_SECONDS = 60 * 60;
|
|
307
|
+
FACILITATOR_SUPPORTED_KV_PREFIX = "x402:facilitator-supported:";
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// src/protocols/x402/facilitator-clients.ts
|
|
312
|
+
function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient, kvStore) {
|
|
313
|
+
return getResolvedX402FacilitatorGroups(facilitatorsByNetwork).map((group) => {
|
|
314
|
+
const inner = new HTTPFacilitatorClient(group.config);
|
|
315
|
+
const kinds = buildSupportedKinds(group);
|
|
316
|
+
const baseline = () => ({
|
|
317
|
+
kinds,
|
|
318
|
+
extensions: [],
|
|
319
|
+
signers: {}
|
|
320
|
+
});
|
|
321
|
+
if (group.family === "solana") {
|
|
322
|
+
return hardcodedSupportedClient(inner, baseline);
|
|
323
|
+
}
|
|
324
|
+
const cached = withCachedSupported(inner, {
|
|
325
|
+
kv: kvStore,
|
|
326
|
+
cacheKey: group.config.url,
|
|
327
|
+
fallback: baseline
|
|
328
|
+
});
|
|
329
|
+
return withScopedKinds(cached, kinds);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function hardcodedSupportedClient(inner, build) {
|
|
333
|
+
return {
|
|
334
|
+
verify: inner.verify.bind(inner),
|
|
335
|
+
settle: inner.settle.bind(inner),
|
|
336
|
+
getSupported: async () => build()
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function withScopedKinds(client, kinds) {
|
|
340
|
+
return {
|
|
341
|
+
verify: client.verify.bind(client),
|
|
342
|
+
settle: client.settle.bind(client),
|
|
343
|
+
getSupported: async () => ({ ...await client.getSupported(), kinds })
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function buildSupportedKinds(group) {
|
|
347
|
+
return group.networks.flatMap((network) => {
|
|
348
|
+
if (group.family === "solana") {
|
|
349
|
+
return [
|
|
350
|
+
{
|
|
351
|
+
x402Version: 2,
|
|
352
|
+
scheme: "exact",
|
|
353
|
+
network,
|
|
354
|
+
extra: { features: { xSettlementAccountSupported: true } }
|
|
355
|
+
}
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
return [
|
|
359
|
+
{ x402Version: 2, scheme: "exact", network },
|
|
360
|
+
{ x402Version: 2, scheme: "upto", network }
|
|
361
|
+
];
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
var init_facilitator_clients = __esm({
|
|
365
|
+
"src/protocols/x402/facilitator-clients.ts"() {
|
|
366
|
+
"use strict";
|
|
367
|
+
init_facilitator_supported();
|
|
368
|
+
init_facilitators();
|
|
236
369
|
}
|
|
237
370
|
});
|
|
238
371
|
|
|
239
|
-
// src/server.ts
|
|
240
|
-
var
|
|
241
|
-
__export(
|
|
372
|
+
// src/init/x402-server.ts
|
|
373
|
+
var x402_server_exports = {};
|
|
374
|
+
__export(x402_server_exports, {
|
|
242
375
|
createX402Server: () => createX402Server
|
|
243
376
|
});
|
|
244
|
-
async function createX402Server(config) {
|
|
377
|
+
async function createX402Server(config, kvStore) {
|
|
245
378
|
const { x402ResourceServer, HTTPFacilitatorClient } = await import("@x402/core/server");
|
|
246
379
|
const { registerExactEvmScheme } = await import("@x402/evm/exact/server");
|
|
247
380
|
const { bazaarResourceServerExtension } = await import("@x402/extensions/bazaar");
|
|
@@ -255,7 +388,11 @@ async function createX402Server(config) {
|
|
|
255
388
|
);
|
|
256
389
|
const evmNetworks = filterEvmNetworks(configuredNetworks);
|
|
257
390
|
const svmNetworks = filterSolanaNetworks(configuredNetworks);
|
|
258
|
-
const facilitatorClients = createFacilitatorClients(
|
|
391
|
+
const facilitatorClients = createFacilitatorClients(
|
|
392
|
+
facilitatorsByNetwork,
|
|
393
|
+
HTTPFacilitatorClient,
|
|
394
|
+
kvStore
|
|
395
|
+
);
|
|
259
396
|
const server = new x402ResourceServer(
|
|
260
397
|
facilitatorClients.length === 1 ? facilitatorClients[0] : facilitatorClients
|
|
261
398
|
);
|
|
@@ -279,48 +416,13 @@ async function createX402Server(config) {
|
|
|
279
416
|
facilitatorsByNetwork
|
|
280
417
|
};
|
|
281
418
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
return groups.map((group) => {
|
|
285
|
-
const inner = new HTTPFacilitatorClient(group.config);
|
|
286
|
-
const kinds = buildSupportedKinds(group);
|
|
287
|
-
return hardcodedSupportedClient(inner, kinds);
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
function hardcodedSupportedClient(inner, kinds) {
|
|
291
|
-
return {
|
|
292
|
-
verify: inner.verify.bind(inner),
|
|
293
|
-
settle: inner.settle.bind(inner),
|
|
294
|
-
getSupported: async () => ({ kinds, extensions: [], signers: {} })
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
function buildSupportedKinds(group) {
|
|
298
|
-
return group.networks.flatMap((network) => {
|
|
299
|
-
const exactKind = {
|
|
300
|
-
x402Version: 2,
|
|
301
|
-
scheme: "exact",
|
|
302
|
-
network,
|
|
303
|
-
...group.family === "solana" ? {
|
|
304
|
-
extra: {
|
|
305
|
-
features: {
|
|
306
|
-
xSettlementAccountSupported: true
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
} : {}
|
|
310
|
-
};
|
|
311
|
-
const uptoKind = { x402Version: 2, scheme: "upto", network };
|
|
312
|
-
if (group.family === "evm") {
|
|
313
|
-
return [exactKind, uptoKind];
|
|
314
|
-
}
|
|
315
|
-
return [exactKind, uptoKind];
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
var init_server = __esm({
|
|
319
|
-
"src/server.ts"() {
|
|
419
|
+
var init_x402_server = __esm({
|
|
420
|
+
"src/init/x402-server.ts"() {
|
|
320
421
|
"use strict";
|
|
321
422
|
init_evm();
|
|
322
423
|
init_solana();
|
|
323
424
|
init_facilitators();
|
|
425
|
+
init_facilitator_clients();
|
|
324
426
|
init_accepts();
|
|
325
427
|
}
|
|
326
428
|
});
|
|
@@ -389,7 +491,7 @@ var AUTH_SCHEME = {
|
|
|
389
491
|
MPP_PAYMENT: "Payment "
|
|
390
492
|
};
|
|
391
493
|
|
|
392
|
-
// src/plugin.ts
|
|
494
|
+
// src/plugin/index.ts
|
|
393
495
|
function createDefaultContext(meta) {
|
|
394
496
|
const ctx = {
|
|
395
497
|
requestId: meta.requestId,
|
|
@@ -425,46 +527,8 @@ function firePluginHook(plugin, method, ...args) {
|
|
|
425
527
|
return void 0;
|
|
426
528
|
}
|
|
427
529
|
}
|
|
428
|
-
function consolePlugin() {
|
|
429
|
-
return {
|
|
430
|
-
onRequest(meta) {
|
|
431
|
-
const ctx = createDefaultContext(meta);
|
|
432
|
-
return ctx;
|
|
433
|
-
},
|
|
434
|
-
onAuthVerified(_ctx, auth) {
|
|
435
|
-
const wallet = auth.wallet ? ` wallet=${auth.wallet}` : "";
|
|
436
|
-
console.log(`[router] AUTH ${auth.authMode} ${auth.route}${wallet}`);
|
|
437
|
-
},
|
|
438
|
-
onPaymentVerified(_ctx, payment) {
|
|
439
|
-
console.log(`[router] VERIFIED ${payment.protocol} ${payment.payer} ${payment.amount}`);
|
|
440
|
-
},
|
|
441
|
-
onPaymentSettled(_ctx, settlement) {
|
|
442
|
-
console.log(`[router] SETTLED ${settlement.protocol} tx=${settlement.transaction}`);
|
|
443
|
-
},
|
|
444
|
-
onResponse(ctx, response) {
|
|
445
|
-
const wallet = ctx.verifiedWallet ? ` wallet=${ctx.verifiedWallet}` : "";
|
|
446
|
-
console.log(
|
|
447
|
-
`[router] ${ctx.route} \u2192 ${response.statusCode} (${response.duration}ms)${wallet}`
|
|
448
|
-
);
|
|
449
|
-
},
|
|
450
|
-
onError(_ctx, error) {
|
|
451
|
-
console.error(`[router] ERROR ${error.status}: ${error.message}`);
|
|
452
|
-
},
|
|
453
|
-
onAlert(_ctx, alert) {
|
|
454
|
-
const logFn = alert.level === "critical" || alert.level === "error" ? console.error : alert.level === "warn" ? console.warn : console.log;
|
|
455
|
-
logFn(
|
|
456
|
-
`[router] ${alert.level.toUpperCase()} ${alert.route}: ${alert.message}`,
|
|
457
|
-
alert.meta ?? ""
|
|
458
|
-
);
|
|
459
|
-
},
|
|
460
|
-
onProviderQuota(_ctx, event) {
|
|
461
|
-
const logFn = event.level === "critical" ? console.error : event.level === "warn" ? console.warn : console.log;
|
|
462
|
-
logFn(`[router] QUOTA ${event.level.toUpperCase()} ${event.provider}: ${event.message}`);
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
530
|
|
|
467
|
-
// src/
|
|
531
|
+
// src/plugin/reporter.ts
|
|
468
532
|
function createReporter(plugin, pluginCtx, route) {
|
|
469
533
|
return (level, message, meta) => {
|
|
470
534
|
firePluginHook(plugin, "onAlert", pluginCtx, {
|
|
@@ -476,7 +540,7 @@ function createReporter(plugin, pluginCtx, route) {
|
|
|
476
540
|
};
|
|
477
541
|
}
|
|
478
542
|
|
|
479
|
-
// src/pipeline/
|
|
543
|
+
// src/pipeline/steps/preflight.ts
|
|
480
544
|
function preflight(routeEntry, handler, deps, request) {
|
|
481
545
|
const meta = buildMeta(request, routeEntry);
|
|
482
546
|
const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
|
|
@@ -506,10 +570,10 @@ function buildMeta(request, routeEntry) {
|
|
|
506
570
|
};
|
|
507
571
|
}
|
|
508
572
|
|
|
509
|
-
// src/pipeline/
|
|
573
|
+
// src/pipeline/steps/parse-body.ts
|
|
510
574
|
import { NextResponse } from "next/server";
|
|
511
575
|
|
|
512
|
-
// src/body.ts
|
|
576
|
+
// src/pipeline/body.ts
|
|
513
577
|
async function bufferBody(request) {
|
|
514
578
|
const text = await request.text();
|
|
515
579
|
if (!text) return void 0;
|
|
@@ -533,7 +597,19 @@ function validateBody(parsed, schema) {
|
|
|
533
597
|
};
|
|
534
598
|
}
|
|
535
599
|
|
|
536
|
-
// src/
|
|
600
|
+
// src/plugin/events.ts
|
|
601
|
+
function fireAuthVerified(ctx, event) {
|
|
602
|
+
firePluginHook(ctx.deps.plugin, "onAuthVerified", ctx.pluginCtx, {
|
|
603
|
+
...event,
|
|
604
|
+
route: ctx.routeEntry.key
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
function firePaymentVerified(ctx, event) {
|
|
608
|
+
firePluginHook(ctx.deps.plugin, "onPaymentVerified", ctx.pluginCtx, event);
|
|
609
|
+
}
|
|
610
|
+
function firePaymentSettled(ctx, event) {
|
|
611
|
+
firePluginHook(ctx.deps.plugin, "onPaymentSettled", ctx.pluginCtx, event);
|
|
612
|
+
}
|
|
537
613
|
function firePluginResponse(ctx, response, requestBody, responseBody) {
|
|
538
614
|
firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
|
|
539
615
|
statusCode: response.status,
|
|
@@ -552,8 +628,37 @@ function firePluginResponse(ctx, response, requestBody, responseBody) {
|
|
|
552
628
|
});
|
|
553
629
|
}
|
|
554
630
|
}
|
|
631
|
+
function fireProviderQuota(ctx, response, handlerResult) {
|
|
632
|
+
const { providerName, providerConfig } = ctx.routeEntry;
|
|
633
|
+
if (!providerName || !providerConfig?.extractQuota) return;
|
|
634
|
+
if (response.status >= 400) return;
|
|
635
|
+
try {
|
|
636
|
+
const quota = providerConfig.extractQuota(handlerResult, response.headers);
|
|
637
|
+
if (!quota) return;
|
|
638
|
+
const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
|
|
639
|
+
const overage = providerConfig.overage ?? "same-rate";
|
|
640
|
+
const event = {
|
|
641
|
+
provider: providerName,
|
|
642
|
+
route: ctx.routeEntry.key,
|
|
643
|
+
remaining: quota.remaining,
|
|
644
|
+
limit: quota.limit,
|
|
645
|
+
spend: quota.spend,
|
|
646
|
+
level,
|
|
647
|
+
overage,
|
|
648
|
+
message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
|
|
649
|
+
};
|
|
650
|
+
firePluginHook(ctx.deps.plugin, "onProviderQuota", ctx.pluginCtx, event);
|
|
651
|
+
} catch {
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function computeQuotaLevel(remaining, warn, critical) {
|
|
655
|
+
if (remaining === null) return "healthy";
|
|
656
|
+
if (critical !== void 0 && remaining <= critical) return "critical";
|
|
657
|
+
if (warn !== void 0 && remaining <= warn) return "warn";
|
|
658
|
+
return "healthy";
|
|
659
|
+
}
|
|
555
660
|
|
|
556
|
-
// src/pipeline/
|
|
661
|
+
// src/pipeline/steps/parse-body.ts
|
|
557
662
|
async function parseBody(ctx, request = ctx.request) {
|
|
558
663
|
if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
|
|
559
664
|
const raw = await bufferBody(request);
|
|
@@ -567,15 +672,7 @@ async function parseBody(ctx, request = ctx.request) {
|
|
|
567
672
|
return { ok: false, response };
|
|
568
673
|
}
|
|
569
674
|
|
|
570
|
-
// src/pipeline/
|
|
571
|
-
function parseQuery(request, routeEntry) {
|
|
572
|
-
if (!routeEntry.querySchema) return void 0;
|
|
573
|
-
const params = Object.fromEntries(request.nextUrl.searchParams.entries());
|
|
574
|
-
const result = routeEntry.querySchema.safeParse(params);
|
|
575
|
-
return result.success ? result.data : params;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// src/pipeline/context/errors.ts
|
|
675
|
+
// src/pipeline/steps/errors.ts
|
|
579
676
|
function errorStatus(error, fallback) {
|
|
580
677
|
const status = error?.status;
|
|
581
678
|
return typeof status === "number" ? status : fallback;
|
|
@@ -588,7 +685,7 @@ function handlerFailureError(response) {
|
|
|
588
685
|
return Object.assign(new Error(message), { status: response.status });
|
|
589
686
|
}
|
|
590
687
|
|
|
591
|
-
// src/pipeline/
|
|
688
|
+
// src/pipeline/steps/fail.ts
|
|
592
689
|
import { NextResponse as NextResponse2 } from "next/server";
|
|
593
690
|
function fail(ctx, status, message, requestBody) {
|
|
594
691
|
const response = NextResponse2.json({ success: false, error: message }, { status });
|
|
@@ -596,7 +693,7 @@ function fail(ctx, status, message, requestBody) {
|
|
|
596
693
|
return response;
|
|
597
694
|
}
|
|
598
695
|
|
|
599
|
-
// src/pipeline/
|
|
696
|
+
// src/pipeline/steps/run-validate.ts
|
|
600
697
|
async function runValidate(ctx, body) {
|
|
601
698
|
if (!ctx.routeEntry.validateFn) return null;
|
|
602
699
|
try {
|
|
@@ -619,6 +716,14 @@ var HttpError = class extends Error {
|
|
|
619
716
|
}
|
|
620
717
|
};
|
|
621
718
|
|
|
719
|
+
// src/pipeline/steps/parse-query.ts
|
|
720
|
+
function parseQuery(request, routeEntry) {
|
|
721
|
+
if (!routeEntry.querySchema) return void 0;
|
|
722
|
+
const params = Object.fromEntries(request.nextUrl.searchParams.entries());
|
|
723
|
+
const result = routeEntry.querySchema.safeParse(params);
|
|
724
|
+
return result.success ? result.data : params;
|
|
725
|
+
}
|
|
726
|
+
|
|
622
727
|
// src/pipeline/flows/static/static-invoke.ts
|
|
623
728
|
function invokePaidStatic(ctx, wallet, account, body, payment) {
|
|
624
729
|
return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
|
|
@@ -636,14 +741,7 @@ function buildHandlerCtx(ctx, wallet, account, body, payment) {
|
|
|
636
741
|
wallet,
|
|
637
742
|
payment,
|
|
638
743
|
account,
|
|
639
|
-
alert
|
|
640
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
641
|
-
level,
|
|
642
|
-
message,
|
|
643
|
-
route: ctx.routeEntry.key,
|
|
644
|
-
meta: alertMeta
|
|
645
|
-
});
|
|
646
|
-
},
|
|
744
|
+
alert: ctx.report,
|
|
647
745
|
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
648
746
|
};
|
|
649
747
|
}
|
|
@@ -687,45 +785,14 @@ function isThenable(value) {
|
|
|
687
785
|
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
688
786
|
}
|
|
689
787
|
|
|
690
|
-
// src/pipeline/
|
|
691
|
-
function fireProviderQuota(ctx, response, handlerResult) {
|
|
692
|
-
const { providerName, providerConfig } = ctx.routeEntry;
|
|
693
|
-
if (!providerName || !providerConfig?.extractQuota) return;
|
|
694
|
-
if (response.status >= 400) return;
|
|
695
|
-
try {
|
|
696
|
-
const quota = providerConfig.extractQuota(handlerResult, response.headers);
|
|
697
|
-
if (!quota) return;
|
|
698
|
-
const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
|
|
699
|
-
const overage = providerConfig.overage ?? "same-rate";
|
|
700
|
-
const event = {
|
|
701
|
-
provider: providerName,
|
|
702
|
-
route: ctx.routeEntry.key,
|
|
703
|
-
remaining: quota.remaining,
|
|
704
|
-
limit: quota.limit,
|
|
705
|
-
spend: quota.spend,
|
|
706
|
-
level,
|
|
707
|
-
overage,
|
|
708
|
-
message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
|
|
709
|
-
};
|
|
710
|
-
firePluginHook(ctx.deps.plugin, "onProviderQuota", ctx.pluginCtx, event);
|
|
711
|
-
} catch {
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
function computeQuotaLevel(remaining, warn, critical) {
|
|
715
|
-
if (remaining === null) return "healthy";
|
|
716
|
-
if (critical !== void 0 && remaining <= critical) return "critical";
|
|
717
|
-
if (warn !== void 0 && remaining <= warn) return "warn";
|
|
718
|
-
return "healthy";
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// src/pipeline/context/finalize/response.ts
|
|
788
|
+
// src/pipeline/steps/finalize/response.ts
|
|
722
789
|
function finalize(ctx, response, rawResult, requestBody) {
|
|
723
790
|
fireProviderQuota(ctx, response, rawResult);
|
|
724
791
|
firePluginResponse(ctx, response, requestBody, rawResult);
|
|
725
792
|
return response;
|
|
726
793
|
}
|
|
727
794
|
|
|
728
|
-
// src/pipeline/
|
|
795
|
+
// src/pipeline/steps/grant-entitlement.ts
|
|
729
796
|
async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
730
797
|
if (!ctx.routeEntry.siwxEnabled) return;
|
|
731
798
|
try {
|
|
@@ -738,7 +805,7 @@ async function grantEntitlementIfSiwx(ctx, wallet) {
|
|
|
738
805
|
}
|
|
739
806
|
}
|
|
740
807
|
|
|
741
|
-
// src/pipeline/
|
|
808
|
+
// src/pipeline/steps/settlement-context.ts
|
|
742
809
|
function settlementContext(ctx, scope) {
|
|
743
810
|
return {
|
|
744
811
|
route: ctx.routeEntry.key,
|
|
@@ -752,7 +819,7 @@ function settlementContext(ctx, scope) {
|
|
|
752
819
|
};
|
|
753
820
|
}
|
|
754
821
|
|
|
755
|
-
// src/pipeline/
|
|
822
|
+
// src/pipeline/steps/run-settlement-error.ts
|
|
756
823
|
async function runSettlementError(ctx, scope, error, phase) {
|
|
757
824
|
const hook = ctx.routeEntry.settlement?.onSettlementError;
|
|
758
825
|
if (!hook) return;
|
|
@@ -764,7 +831,7 @@ async function runSettlementError(ctx, scope, error, phase) {
|
|
|
764
831
|
}
|
|
765
832
|
}
|
|
766
833
|
|
|
767
|
-
// src/pipeline/
|
|
834
|
+
// src/pipeline/steps/run-after-settle.ts
|
|
768
835
|
async function runAfterSettle(ctx, scope) {
|
|
769
836
|
const hook = ctx.routeEntry.settlement?.afterSettle;
|
|
770
837
|
if (!hook) return;
|
|
@@ -777,11 +844,11 @@ async function runAfterSettle(ctx, scope) {
|
|
|
777
844
|
}
|
|
778
845
|
}
|
|
779
846
|
|
|
780
|
-
// src/pipeline/
|
|
847
|
+
// src/pipeline/steps/finalize/epilogue.ts
|
|
781
848
|
async function runPostSettleEpilogue(args) {
|
|
782
849
|
const { ctx, strategy, wallet, settle, afterSettleScope, rawResult, body } = args;
|
|
783
850
|
await grantEntitlementIfSiwx(ctx, wallet);
|
|
784
|
-
|
|
851
|
+
firePaymentSettled(ctx, {
|
|
785
852
|
protocol: strategy.protocol,
|
|
786
853
|
payer: wallet,
|
|
787
854
|
transaction: settle.settledPayment.transaction ?? "",
|
|
@@ -791,7 +858,7 @@ async function runPostSettleEpilogue(args) {
|
|
|
791
858
|
return finalize(ctx, settle.response, rawResult, body);
|
|
792
859
|
}
|
|
793
860
|
|
|
794
|
-
// src/pipeline/
|
|
861
|
+
// src/pipeline/steps/finalize/request.ts
|
|
795
862
|
async function settleAndFinalizeRequest(args) {
|
|
796
863
|
const { ctx, strategy, verifyOutcome, scope, rawResult, body, billedAmount, onSettleError } = args;
|
|
797
864
|
const { request, routeEntry, deps, report } = ctx;
|
|
@@ -824,7 +891,7 @@ async function settleAndFinalizeRequest(args) {
|
|
|
824
891
|
});
|
|
825
892
|
}
|
|
826
893
|
|
|
827
|
-
// src/pipeline/
|
|
894
|
+
// src/pipeline/steps/finalize/stream.ts
|
|
828
895
|
async function settleAndFinalizeStream(args) {
|
|
829
896
|
const { ctx, strategy, verifyOutcome, source, account, body, bindChannelCharge } = args;
|
|
830
897
|
const { request, routeEntry, deps, report } = ctx;
|
|
@@ -862,7 +929,7 @@ async function settleAndFinalizeStream(args) {
|
|
|
862
929
|
});
|
|
863
930
|
}
|
|
864
931
|
|
|
865
|
-
// src/pipeline/
|
|
932
|
+
// src/pipeline/steps/run-handler-only.ts
|
|
866
933
|
async function runHandlerOnly(ctx, wallet, account) {
|
|
867
934
|
const body = await parseBody(ctx);
|
|
868
935
|
if (!body.ok) return body.response;
|
|
@@ -872,7 +939,7 @@ async function runHandlerOnly(ctx, wallet, account) {
|
|
|
872
939
|
return finalize(ctx, result.response, result.rawResult, body.data);
|
|
873
940
|
}
|
|
874
941
|
|
|
875
|
-
// src/pipeline/
|
|
942
|
+
// src/pipeline/steps/run-before-settle.ts
|
|
876
943
|
async function runBeforeSettle(ctx, scope) {
|
|
877
944
|
const hook = ctx.routeEntry.settlement?.beforeSettle;
|
|
878
945
|
if (!hook) return null;
|
|
@@ -889,7 +956,7 @@ async function runBeforeSettle(ctx, scope) {
|
|
|
889
956
|
}
|
|
890
957
|
}
|
|
891
958
|
|
|
892
|
-
// src/pipeline/
|
|
959
|
+
// src/pipeline/steps/run-settled-handler-error.ts
|
|
893
960
|
async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
|
|
894
961
|
const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
|
|
895
962
|
if (!hook) return;
|
|
@@ -963,7 +1030,7 @@ async function buildSIWXExtension() {
|
|
|
963
1030
|
return declareSIWxExtension();
|
|
964
1031
|
}
|
|
965
1032
|
|
|
966
|
-
// src/pipeline/
|
|
1033
|
+
// src/pipeline/steps/try-siwx-fast-path.ts
|
|
967
1034
|
async function trySiwxFastPath(ctx, account) {
|
|
968
1035
|
const { request, routeEntry, deps } = ctx;
|
|
969
1036
|
if (!routeEntry.siwxEnabled) return null;
|
|
@@ -975,22 +1042,18 @@ async function trySiwxFastPath(ctx, account) {
|
|
|
975
1042
|
ctx.pluginCtx.setVerifiedWallet(wallet);
|
|
976
1043
|
const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
|
|
977
1044
|
if (!entitled) return null;
|
|
978
|
-
|
|
979
|
-
authMode: "siwx",
|
|
980
|
-
wallet,
|
|
981
|
-
route: routeEntry.key
|
|
982
|
-
});
|
|
1045
|
+
fireAuthVerified(ctx, { authMode: "siwx", wallet });
|
|
983
1046
|
return runHandlerOnly(ctx, wallet, account);
|
|
984
1047
|
}
|
|
985
1048
|
|
|
986
|
-
// src/pipeline/
|
|
1049
|
+
// src/pipeline/steps/should-parse-body-early.ts
|
|
987
1050
|
function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
|
|
988
1051
|
if (incomingStrategy) return false;
|
|
989
1052
|
if (!routeEntry.bodySchema) return false;
|
|
990
1053
|
return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
|
|
991
1054
|
}
|
|
992
1055
|
|
|
993
|
-
// src/pipeline/
|
|
1056
|
+
// src/pipeline/steps/resolve-early-body.ts
|
|
994
1057
|
async function resolveEarlyBody(args) {
|
|
995
1058
|
const { ctx, pricing, incomingStrategy } = args;
|
|
996
1059
|
if (!shouldParseBodyEarly(incomingStrategy, ctx.routeEntry, pricing)) {
|
|
@@ -1022,24 +1085,19 @@ function extractBearerToken(header) {
|
|
|
1022
1085
|
return null;
|
|
1023
1086
|
}
|
|
1024
1087
|
|
|
1025
|
-
// src/pipeline/
|
|
1088
|
+
// src/pipeline/steps/run-api-key-gate.ts
|
|
1026
1089
|
async function runApiKeyGate(ctx) {
|
|
1027
|
-
const { request, routeEntry
|
|
1090
|
+
const { request, routeEntry } = ctx;
|
|
1028
1091
|
if (!routeEntry.apiKeyResolver) return { ok: true, account: void 0 };
|
|
1029
1092
|
const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
|
|
1030
1093
|
if (!apiKeyResult.valid) {
|
|
1031
1094
|
return { ok: false, response: fail(ctx, 401, "Invalid or missing API key") };
|
|
1032
1095
|
}
|
|
1033
|
-
|
|
1034
|
-
authMode: "apiKey",
|
|
1035
|
-
wallet: null,
|
|
1036
|
-
route: routeEntry.key,
|
|
1037
|
-
account: apiKeyResult.account
|
|
1038
|
-
});
|
|
1096
|
+
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: apiKeyResult.account });
|
|
1039
1097
|
return { ok: true, account: apiKeyResult.account };
|
|
1040
1098
|
}
|
|
1041
1099
|
|
|
1042
|
-
// src/pipeline/
|
|
1100
|
+
// src/pipeline/steps/protocol-init-error.ts
|
|
1043
1101
|
function protocolInitError(routeEntry, deps) {
|
|
1044
1102
|
if (!routeEntry.pricing) return null;
|
|
1045
1103
|
const errors = [];
|
|
@@ -1062,15 +1120,65 @@ async function runApiKeyOnlyFlow(ctx) {
|
|
|
1062
1120
|
}
|
|
1063
1121
|
const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
|
|
1064
1122
|
if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
|
|
1065
|
-
|
|
1066
|
-
authMode: "apiKey",
|
|
1067
|
-
wallet: null,
|
|
1068
|
-
route: ctx.routeEntry.key,
|
|
1069
|
-
account: result.account
|
|
1070
|
-
});
|
|
1123
|
+
fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
|
|
1071
1124
|
return runHandlerOnly(ctx, null, result.account);
|
|
1072
1125
|
}
|
|
1073
1126
|
|
|
1127
|
+
// src/pricing/format.ts
|
|
1128
|
+
var USDC_DECIMALS = 6;
|
|
1129
|
+
var DECIMAL_RE = /^(\d+)(?:\.(\d+))?$/;
|
|
1130
|
+
function badDecimal(amount) {
|
|
1131
|
+
return Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
|
|
1132
|
+
status: 400
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
function decimalToAtomic(amount, decimals = USDC_DECIMALS) {
|
|
1136
|
+
const match = DECIMAL_RE.exec(amount.trim());
|
|
1137
|
+
if (!match) throw badDecimal(amount);
|
|
1138
|
+
const whole = match[1];
|
|
1139
|
+
const fraction = match[2] ?? "";
|
|
1140
|
+
if (fraction.length > decimals) {
|
|
1141
|
+
throw Object.assign(new Error(`Amount '${amount}' exceeds ${decimals} decimal places`), {
|
|
1142
|
+
status: 400
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
|
|
1146
|
+
return BigInt(normalized || "0");
|
|
1147
|
+
}
|
|
1148
|
+
function atomicToDecimal(atomic, decimals = USDC_DECIMALS) {
|
|
1149
|
+
const divisor = 10n ** BigInt(decimals);
|
|
1150
|
+
const whole = atomic / divisor;
|
|
1151
|
+
const fraction = atomic % divisor;
|
|
1152
|
+
if (fraction === 0n) return whole.toString();
|
|
1153
|
+
const fractionStr = fraction.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
1154
|
+
return `${whole}.${fractionStr}`;
|
|
1155
|
+
}
|
|
1156
|
+
function compareDecimals(a, b) {
|
|
1157
|
+
const av = decimalToAtomic(a);
|
|
1158
|
+
const bv = decimalToAtomic(b);
|
|
1159
|
+
if (av < bv) return -1;
|
|
1160
|
+
if (av > bv) return 1;
|
|
1161
|
+
return 0;
|
|
1162
|
+
}
|
|
1163
|
+
function isPositiveDecimal(value) {
|
|
1164
|
+
try {
|
|
1165
|
+
return decimalToAtomic(value) > 0n;
|
|
1166
|
+
} catch {
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
function multiplyDecimal(decimal, factor) {
|
|
1171
|
+
if (!Number.isFinite(factor) || factor <= 0) return decimal;
|
|
1172
|
+
const [whole, fraction = ""] = decimal.split(".");
|
|
1173
|
+
const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
|
|
1174
|
+
const decimals = fraction.length;
|
|
1175
|
+
if (decimals === 0) return scaled;
|
|
1176
|
+
const padded = scaled.padStart(decimals + 1, "0");
|
|
1177
|
+
const intPart = padded.slice(0, padded.length - decimals);
|
|
1178
|
+
const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
|
|
1179
|
+
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1074
1182
|
// src/pricing/dynamic.ts
|
|
1075
1183
|
var DynamicPricing = class {
|
|
1076
1184
|
constructor(opts) {
|
|
@@ -1106,9 +1214,13 @@ var DynamicPricing = class {
|
|
|
1106
1214
|
}
|
|
1107
1215
|
cap(raw, body) {
|
|
1108
1216
|
if (!this.opts.maxPrice) return raw;
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1217
|
+
let overCap;
|
|
1218
|
+
try {
|
|
1219
|
+
overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
|
|
1220
|
+
} catch {
|
|
1221
|
+
overCap = true;
|
|
1222
|
+
}
|
|
1223
|
+
if (overCap) {
|
|
1112
1224
|
this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
|
|
1113
1225
|
calculated: raw,
|
|
1114
1226
|
maxPrice: this.opts.maxPrice,
|
|
@@ -1185,7 +1297,7 @@ var TieredPricing = class {
|
|
|
1185
1297
|
maxTierPrice() {
|
|
1186
1298
|
let max = "0";
|
|
1187
1299
|
for (const tier of Object.values(this.opts.tiers)) {
|
|
1188
|
-
if (
|
|
1300
|
+
if (compareDecimals(tier.price, max) > 0) max = tier.price;
|
|
1189
1301
|
}
|
|
1190
1302
|
return max;
|
|
1191
1303
|
}
|
|
@@ -1663,17 +1775,6 @@ var mppStrategy = {
|
|
|
1663
1775
|
return buildChargeChallenge(args);
|
|
1664
1776
|
}
|
|
1665
1777
|
};
|
|
1666
|
-
function multiplyDecimal(decimal, factor) {
|
|
1667
|
-
if (!Number.isFinite(factor) || factor <= 0) return decimal;
|
|
1668
|
-
const [whole, fraction = ""] = decimal.split(".");
|
|
1669
|
-
const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
|
|
1670
|
-
const decimals = fraction.length;
|
|
1671
|
-
if (decimals === 0) return scaled;
|
|
1672
|
-
const padded = scaled.padStart(decimals + 1, "0");
|
|
1673
|
-
const intPart = padded.slice(0, padded.length - decimals);
|
|
1674
|
-
const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
|
|
1675
|
-
return fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
1676
|
-
}
|
|
1677
1778
|
async function buildChargeChallenge(args) {
|
|
1678
1779
|
if (!args.deps.mppx) return {};
|
|
1679
1780
|
try {
|
|
@@ -1766,26 +1867,13 @@ function buildCustomRequirement(price, accept) {
|
|
|
1766
1867
|
return {
|
|
1767
1868
|
scheme: accept.scheme,
|
|
1768
1869
|
network: accept.network,
|
|
1769
|
-
amount:
|
|
1870
|
+
amount: decimalToAtomic(price, accept.decimals ?? 6).toString(),
|
|
1770
1871
|
asset: accept.asset,
|
|
1771
1872
|
payTo: accept.payTo,
|
|
1772
1873
|
maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
|
|
1773
1874
|
extra: accept.extra ?? {}
|
|
1774
1875
|
};
|
|
1775
1876
|
}
|
|
1776
|
-
function decimalToAtomicUnits(amount, decimals) {
|
|
1777
|
-
const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
|
|
1778
|
-
if (!match?.groups) {
|
|
1779
|
-
throw new Error(`Invalid decimal amount '${amount}'`);
|
|
1780
|
-
}
|
|
1781
|
-
const whole = match.groups.whole;
|
|
1782
|
-
const fraction = match.groups.fraction ?? "";
|
|
1783
|
-
if (fraction.length > decimals) {
|
|
1784
|
-
throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
|
|
1785
|
-
}
|
|
1786
|
-
const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
|
|
1787
|
-
return normalized === "" ? "0" : normalized;
|
|
1788
|
-
}
|
|
1789
1877
|
|
|
1790
1878
|
// src/protocols/x402/challenge.ts
|
|
1791
1879
|
async function buildX402Challenge(opts) {
|
|
@@ -2148,28 +2236,14 @@ async function buildX402ChallengeContribution(args) {
|
|
|
2148
2236
|
}
|
|
2149
2237
|
function reportSettleFailure(report, err, network) {
|
|
2150
2238
|
const facilitator = err ?? {};
|
|
2151
|
-
|
|
2239
|
+
const meta = {
|
|
2152
2240
|
error: err instanceof Error ? err.message : String(err),
|
|
2153
2241
|
network,
|
|
2154
2242
|
errorReason: facilitator.errorReason,
|
|
2155
2243
|
facilitatorStatus: facilitator.response?.status,
|
|
2156
2244
|
facilitatorBody: facilitator.response?.data ?? facilitator.response?.body
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
// src/protocols/detect.ts
|
|
2161
|
-
function detectProtocol(request) {
|
|
2162
|
-
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
2163
|
-
return "x402";
|
|
2164
|
-
}
|
|
2165
|
-
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
2166
|
-
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
2167
|
-
return "mpp";
|
|
2168
|
-
}
|
|
2169
|
-
if (request.headers.get(HEADERS.SIWX)) {
|
|
2170
|
-
return "siwx";
|
|
2171
|
-
}
|
|
2172
|
-
return null;
|
|
2245
|
+
};
|
|
2246
|
+
report("error", "Settlement failed", meta);
|
|
2173
2247
|
}
|
|
2174
2248
|
|
|
2175
2249
|
// src/protocols/index.ts
|
|
@@ -2192,13 +2266,14 @@ function getAllowedStrategies(allowed) {
|
|
|
2192
2266
|
import { NextResponse as NextResponse4 } from "next/server";
|
|
2193
2267
|
|
|
2194
2268
|
// src/pipeline/challenge-extensions.ts
|
|
2269
|
+
init_evm();
|
|
2195
2270
|
async function buildChallengeExtensions(ctx) {
|
|
2196
2271
|
const { routeEntry } = ctx;
|
|
2197
2272
|
let extensions;
|
|
2198
2273
|
try {
|
|
2199
|
-
const { z } = await import("zod");
|
|
2274
|
+
const { z: z2 } = await import("zod");
|
|
2200
2275
|
const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
|
|
2201
|
-
const toJSON = (schema) =>
|
|
2276
|
+
const toJSON = (schema) => z2.toJSONSchema(schema, {
|
|
2202
2277
|
target: "draft-2020-12",
|
|
2203
2278
|
unrepresentable: "any"
|
|
2204
2279
|
});
|
|
@@ -2219,11 +2294,10 @@ async function buildChallengeExtensions(ctx) {
|
|
|
2219
2294
|
extensions = declareDiscoveryExtension(config);
|
|
2220
2295
|
}
|
|
2221
2296
|
} catch (err) {
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
});
|
|
2297
|
+
ctx.report(
|
|
2298
|
+
"warn",
|
|
2299
|
+
`Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2300
|
+
);
|
|
2227
2301
|
}
|
|
2228
2302
|
if (routeEntry.siwxEnabled) {
|
|
2229
2303
|
try {
|
|
@@ -2237,7 +2311,24 @@ async function buildChallengeExtensions(ctx) {
|
|
|
2237
2311
|
} catch {
|
|
2238
2312
|
}
|
|
2239
2313
|
}
|
|
2240
|
-
|
|
2314
|
+
const hasEvmUpto = ctx.deps.x402Accepts.some(
|
|
2315
|
+
(accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
|
|
2316
|
+
);
|
|
2317
|
+
if (hasEvmUpto) {
|
|
2318
|
+
try {
|
|
2319
|
+
const { declareEip2612GasSponsoringExtension } = await import("@x402/extensions");
|
|
2320
|
+
extensions = {
|
|
2321
|
+
...extensions ?? {},
|
|
2322
|
+
...declareEip2612GasSponsoringExtension()
|
|
2323
|
+
};
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
ctx.report(
|
|
2326
|
+
"warn",
|
|
2327
|
+
`EIP-2612 gas-sponsoring declaration failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
return extensions;
|
|
2241
2332
|
}
|
|
2242
2333
|
|
|
2243
2334
|
// src/pipeline/flows/build402.ts
|
|
@@ -2357,7 +2448,7 @@ async function runDynamicChannelMgmtFlow(args) {
|
|
|
2357
2448
|
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
2358
2449
|
}
|
|
2359
2450
|
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2360
|
-
|
|
2451
|
+
firePaymentVerified(ctx, {
|
|
2361
2452
|
protocol: strategy.protocol,
|
|
2362
2453
|
payer: verifyOutcome.wallet,
|
|
2363
2454
|
amount: price,
|
|
@@ -2392,27 +2483,6 @@ async function runDynamicChannelMgmtFlow(args) {
|
|
|
2392
2483
|
// src/pipeline/flows/dynamic/dynamic-invoke.ts
|
|
2393
2484
|
import { NextResponse as NextResponse6 } from "next/server";
|
|
2394
2485
|
|
|
2395
|
-
// src/pricing/atomic.ts
|
|
2396
|
-
var USDC_DECIMALS = 6;
|
|
2397
|
-
function decimalToAtomic(amount) {
|
|
2398
|
-
const m = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
|
|
2399
|
-
if (!m) {
|
|
2400
|
-
throw Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
|
|
2401
|
-
status: 400
|
|
2402
|
-
});
|
|
2403
|
-
}
|
|
2404
|
-
const whole = m[1];
|
|
2405
|
-
const fraction = (m[2] ?? "").slice(0, USDC_DECIMALS).padEnd(USDC_DECIMALS, "0");
|
|
2406
|
-
return BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0");
|
|
2407
|
-
}
|
|
2408
|
-
function atomicToDecimal(atomic) {
|
|
2409
|
-
const whole = atomic / 10n ** BigInt(USDC_DECIMALS);
|
|
2410
|
-
const fraction = atomic % 10n ** BigInt(USDC_DECIMALS);
|
|
2411
|
-
if (fraction === 0n) return whole.toString();
|
|
2412
|
-
const fractionStr = fraction.toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
|
|
2413
|
-
return `${whole}.${fractionStr}`;
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
2486
|
// src/pricing/charge-context.ts
|
|
2417
2487
|
function createChargeContext(args) {
|
|
2418
2488
|
const { tickCost, maxPrice, route } = args;
|
|
@@ -2465,14 +2535,7 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
|
|
|
2465
2535
|
wallet,
|
|
2466
2536
|
payment,
|
|
2467
2537
|
account,
|
|
2468
|
-
alert
|
|
2469
|
-
firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
|
|
2470
|
-
level,
|
|
2471
|
-
message,
|
|
2472
|
-
route: ctx.routeEntry.key,
|
|
2473
|
-
meta: alertMeta
|
|
2474
|
-
});
|
|
2475
|
-
},
|
|
2538
|
+
alert: ctx.report,
|
|
2476
2539
|
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
2477
2540
|
};
|
|
2478
2541
|
const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
|
|
@@ -2537,7 +2600,7 @@ function resolveDynamicPreflight(strategy, request, routeEntry) {
|
|
|
2537
2600
|
// src/pipeline/flows/dynamic/dynamic-request.ts
|
|
2538
2601
|
async function runDynamicRequestFlow(args) {
|
|
2539
2602
|
const { ctx, strategy, verifyOutcome, account, body, result } = args;
|
|
2540
|
-
const {
|
|
2603
|
+
const { routeEntry } = ctx;
|
|
2541
2604
|
const settleScope = {
|
|
2542
2605
|
wallet: verifyOutcome.wallet,
|
|
2543
2606
|
account,
|
|
@@ -2563,11 +2626,10 @@ async function runDynamicRequestFlow(args) {
|
|
|
2563
2626
|
billedAmount,
|
|
2564
2627
|
onSettleError: async (error, failMessage) => {
|
|
2565
2628
|
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
});
|
|
2629
|
+
ctx.report(
|
|
2630
|
+
"critical",
|
|
2631
|
+
`${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`
|
|
2632
|
+
);
|
|
2571
2633
|
}
|
|
2572
2634
|
});
|
|
2573
2635
|
}
|
|
@@ -2637,7 +2699,7 @@ async function runDynamicPaidFlow(ctx) {
|
|
|
2637
2699
|
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
2638
2700
|
}
|
|
2639
2701
|
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2640
|
-
|
|
2702
|
+
firePaymentVerified(ctx, {
|
|
2641
2703
|
protocol: incomingStrategy.protocol,
|
|
2642
2704
|
payer: verifyOutcome.wallet,
|
|
2643
2705
|
amount: price,
|
|
@@ -2703,7 +2765,6 @@ async function resolveStaticBodyAndPrice(args) {
|
|
|
2703
2765
|
// src/pipeline/flows/static/static-request.ts
|
|
2704
2766
|
async function runStaticRequestFlow(args) {
|
|
2705
2767
|
const { ctx, strategy, verifyOutcome, account, body, price, result } = args;
|
|
2706
|
-
const { deps, routeEntry } = ctx;
|
|
2707
2768
|
const settleScope = {
|
|
2708
2769
|
wallet: verifyOutcome.wallet,
|
|
2709
2770
|
account,
|
|
@@ -2744,11 +2805,10 @@ async function runStaticRequestFlow(args) {
|
|
|
2744
2805
|
billedAmount: price,
|
|
2745
2806
|
onSettleError: async (error, failMessage) => {
|
|
2746
2807
|
await runSettlementError(ctx, settleScope, error, "settle");
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
});
|
|
2808
|
+
ctx.report(
|
|
2809
|
+
"critical",
|
|
2810
|
+
`${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`
|
|
2811
|
+
);
|
|
2752
2812
|
}
|
|
2753
2813
|
});
|
|
2754
2814
|
}
|
|
@@ -2794,7 +2854,7 @@ async function runStaticPaidFlow(ctx) {
|
|
|
2794
2854
|
return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
|
|
2795
2855
|
}
|
|
2796
2856
|
ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
|
|
2797
|
-
|
|
2857
|
+
firePaymentVerified(ctx, {
|
|
2798
2858
|
protocol: incomingStrategy.protocol,
|
|
2799
2859
|
payer: verifyOutcome.wallet,
|
|
2800
2860
|
amount: price,
|
|
@@ -2833,6 +2893,19 @@ async function runPaidFlow(ctx) {
|
|
|
2833
2893
|
import { NextResponse as NextResponse7 } from "next/server";
|
|
2834
2894
|
|
|
2835
2895
|
// src/kv-store/client.ts
|
|
2896
|
+
var BIGINT_SUFFIX = "#__bigint";
|
|
2897
|
+
function stringifyValue(value) {
|
|
2898
|
+
return JSON.stringify(
|
|
2899
|
+
value,
|
|
2900
|
+
(_key, v) => typeof v === "bigint" ? `${v.toString()}${BIGINT_SUFFIX}` : v
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
function parseValue(raw) {
|
|
2904
|
+
return JSON.parse(
|
|
2905
|
+
raw,
|
|
2906
|
+
(_key, v) => typeof v === "string" && v.endsWith(BIGINT_SUFFIX) ? BigInt(v.slice(0, -BIGINT_SUFFIX.length)) : v
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
2836
2909
|
function restKvStore(url, token) {
|
|
2837
2910
|
const base = url.replace(/\/+$/, "");
|
|
2838
2911
|
const authHeader = { Authorization: `Bearer ${token}` };
|
|
@@ -2854,16 +2927,22 @@ function restKvStore(url, token) {
|
|
|
2854
2927
|
const res = await fetch(`${base}/get/${encodeURIComponent(key)}`, { headers: authHeader });
|
|
2855
2928
|
if (!res.ok) throw new Error(`[kv-store] GET ${key}: ${res.status}`);
|
|
2856
2929
|
const { result } = await res.json();
|
|
2857
|
-
|
|
2930
|
+
if (result == null) return null;
|
|
2931
|
+
if (typeof result !== "string") return result;
|
|
2932
|
+
try {
|
|
2933
|
+
return parseValue(result);
|
|
2934
|
+
} catch {
|
|
2935
|
+
return result;
|
|
2936
|
+
}
|
|
2858
2937
|
}
|
|
2859
2938
|
async function set(key, value) {
|
|
2860
|
-
await exec(["SET", key,
|
|
2939
|
+
await exec(["SET", key, stringifyValue(value)]);
|
|
2861
2940
|
}
|
|
2862
2941
|
async function del(key) {
|
|
2863
2942
|
await exec(["DEL", key]);
|
|
2864
2943
|
}
|
|
2865
2944
|
async function setNxEx(key, value, ttlSeconds) {
|
|
2866
|
-
const result = await exec(["SET", key,
|
|
2945
|
+
const result = await exec(["SET", key, stringifyValue(value), "EX", ttlSeconds, "NX"]);
|
|
2867
2946
|
return result === "OK";
|
|
2868
2947
|
}
|
|
2869
2948
|
async function sadd(key, member) {
|
|
@@ -2978,6 +3057,21 @@ async function createKvMppStore(kv, options) {
|
|
|
2978
3057
|
});
|
|
2979
3058
|
}
|
|
2980
3059
|
|
|
3060
|
+
// src/protocols/detect.ts
|
|
3061
|
+
function detectProtocol(request) {
|
|
3062
|
+
if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
|
|
3063
|
+
return "x402";
|
|
3064
|
+
}
|
|
3065
|
+
const auth = request.headers.get(HEADERS.AUTHORIZATION);
|
|
3066
|
+
if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
|
|
3067
|
+
return "mpp";
|
|
3068
|
+
}
|
|
3069
|
+
if (request.headers.get(HEADERS.SIWX)) {
|
|
3070
|
+
return "siwx";
|
|
3071
|
+
}
|
|
3072
|
+
return null;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
2981
3075
|
// src/protocols/mpp/siwx-mode.ts
|
|
2982
3076
|
import { Credential as Credential2 } from "mppx";
|
|
2983
3077
|
async function verifyMppSiwx(request, mppx) {
|
|
@@ -3017,11 +3111,7 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
3017
3111
|
}
|
|
3018
3112
|
if (mppSiwxResult.valid) {
|
|
3019
3113
|
ctx.pluginCtx.setVerifiedWallet(mppSiwxResult.wallet);
|
|
3020
|
-
|
|
3021
|
-
authMode: "siwx",
|
|
3022
|
-
wallet: mppSiwxResult.wallet,
|
|
3023
|
-
route: routeEntry.key
|
|
3024
|
-
});
|
|
3114
|
+
fireAuthVerified(ctx, { authMode: "siwx", wallet: mppSiwxResult.wallet });
|
|
3025
3115
|
const authResponse = await runHandlerOnly(ctx, mppSiwxResult.wallet, void 0);
|
|
3026
3116
|
if (authResponse.status < 400) {
|
|
3027
3117
|
return mppSiwxResult.withReceipt(authResponse);
|
|
@@ -3043,11 +3133,7 @@ async function runSiwxOnlyFlow(ctx) {
|
|
|
3043
3133
|
}
|
|
3044
3134
|
const wallet = normalizeWalletAddress(siwx.wallet);
|
|
3045
3135
|
ctx.pluginCtx.setVerifiedWallet(wallet);
|
|
3046
|
-
|
|
3047
|
-
authMode: "siwx",
|
|
3048
|
-
wallet,
|
|
3049
|
-
route: routeEntry.key
|
|
3050
|
-
});
|
|
3136
|
+
fireAuthVerified(ctx, { authMode: "siwx", wallet });
|
|
3051
3137
|
return runHandlerOnly(ctx, wallet, void 0);
|
|
3052
3138
|
}
|
|
3053
3139
|
async function buildSiwxChallenge(ctx) {
|
|
@@ -3140,7 +3226,7 @@ async function runUnprotectedFlow(ctx) {
|
|
|
3140
3226
|
return runHandlerOnly(ctx, null, void 0);
|
|
3141
3227
|
}
|
|
3142
3228
|
|
|
3143
|
-
// src/orchestrate.ts
|
|
3229
|
+
// src/pipeline/orchestrate.ts
|
|
3144
3230
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
3145
3231
|
return async (request) => {
|
|
3146
3232
|
await deps.initPromise;
|
|
@@ -3185,142 +3271,109 @@ ${issues}`
|
|
|
3185
3271
|
}
|
|
3186
3272
|
|
|
3187
3273
|
// src/builder.ts
|
|
3188
|
-
var RouteBuilder = class {
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
_inputExample = void 0;
|
|
3223
|
-
/** @internal */
|
|
3224
|
-
_hasInputExample = false;
|
|
3225
|
-
/** @internal */
|
|
3226
|
-
_outputExample = void 0;
|
|
3227
|
-
/** @internal */
|
|
3228
|
-
_hasOutputExample = false;
|
|
3229
|
-
/** @internal */
|
|
3230
|
-
_description;
|
|
3231
|
-
/** @internal */
|
|
3232
|
-
_path;
|
|
3233
|
-
/** @internal */
|
|
3234
|
-
_method = "POST";
|
|
3235
|
-
/** @internal */
|
|
3236
|
-
_apiKeyResolver;
|
|
3237
|
-
/** @internal */
|
|
3238
|
-
_providerName;
|
|
3239
|
-
/** @internal */
|
|
3240
|
-
_providerConfig;
|
|
3241
|
-
/** @internal */
|
|
3242
|
-
_validateFn;
|
|
3243
|
-
/** @internal */
|
|
3244
|
-
_settlement;
|
|
3245
|
-
/** @internal */
|
|
3246
|
-
_mppInfo;
|
|
3247
|
-
constructor(key, registry, deps) {
|
|
3248
|
-
this._key = key;
|
|
3249
|
-
this._registry = registry;
|
|
3250
|
-
this._deps = deps;
|
|
3274
|
+
var RouteBuilder = class _RouteBuilder {
|
|
3275
|
+
#s;
|
|
3276
|
+
constructor(key, registry, deps, defaults) {
|
|
3277
|
+
this.#s = {
|
|
3278
|
+
key,
|
|
3279
|
+
registry,
|
|
3280
|
+
deps,
|
|
3281
|
+
authMode: null,
|
|
3282
|
+
pricing: void 0,
|
|
3283
|
+
siwxEnabled: false,
|
|
3284
|
+
protocols: defaults?.protocols ? [...defaults.protocols] : ["x402"],
|
|
3285
|
+
maxPrice: void 0,
|
|
3286
|
+
minPrice: void 0,
|
|
3287
|
+
dynamicPrice: false,
|
|
3288
|
+
tickCost: void 0,
|
|
3289
|
+
unitType: void 0,
|
|
3290
|
+
payTo: void 0,
|
|
3291
|
+
bodySchema: void 0,
|
|
3292
|
+
querySchema: void 0,
|
|
3293
|
+
outputSchema: void 0,
|
|
3294
|
+
inputExample: void 0,
|
|
3295
|
+
hasInputExample: false,
|
|
3296
|
+
outputExample: void 0,
|
|
3297
|
+
hasOutputExample: false,
|
|
3298
|
+
description: void 0,
|
|
3299
|
+
path: void 0,
|
|
3300
|
+
method: "POST",
|
|
3301
|
+
apiKeyResolver: void 0,
|
|
3302
|
+
providerName: void 0,
|
|
3303
|
+
providerConfig: void 0,
|
|
3304
|
+
validateFn: void 0,
|
|
3305
|
+
settlement: void 0,
|
|
3306
|
+
mppInfo: void 0
|
|
3307
|
+
};
|
|
3251
3308
|
}
|
|
3252
3309
|
fork() {
|
|
3253
|
-
const next =
|
|
3254
|
-
|
|
3255
|
-
|
|
3310
|
+
const next = new _RouteBuilder(
|
|
3311
|
+
this.#s.key,
|
|
3312
|
+
this.#s.registry,
|
|
3313
|
+
this.#s.deps
|
|
3314
|
+
);
|
|
3315
|
+
next.#s = { ...this.#s, protocols: [...this.#s.protocols] };
|
|
3256
3316
|
return next;
|
|
3257
3317
|
}
|
|
3258
3318
|
paid(pricingOrOptions, options) {
|
|
3259
|
-
const { pricing, resolvedOptions } = resolvePaidArgs(this.
|
|
3260
|
-
if (this.
|
|
3319
|
+
const { pricing, resolvedOptions } = resolvePaidArgs(this.#s.key, pricingOrOptions, options);
|
|
3320
|
+
if (this.#s.authMode === "unprotected") {
|
|
3261
3321
|
throw new Error(
|
|
3262
|
-
`route '${this.
|
|
3322
|
+
`route '${this.#s.key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
3263
3323
|
);
|
|
3264
3324
|
}
|
|
3265
|
-
if (this.
|
|
3325
|
+
if (this.#s.pricing !== void 0) {
|
|
3266
3326
|
throw new Error(
|
|
3267
|
-
`route '${this.
|
|
3327
|
+
`route '${this.#s.key}': Cannot call .paid() more than once on the same route.`
|
|
3268
3328
|
);
|
|
3269
3329
|
}
|
|
3270
3330
|
const next = this.fork();
|
|
3271
|
-
next.
|
|
3272
|
-
next.
|
|
3331
|
+
next.#s.authMode = "paid";
|
|
3332
|
+
next.#s.pricing = pricing;
|
|
3273
3333
|
if (resolvedOptions?.protocols) {
|
|
3274
|
-
next.
|
|
3275
|
-
} else if (next.
|
|
3276
|
-
next.
|
|
3334
|
+
next.#s.protocols = [...resolvedOptions.protocols];
|
|
3335
|
+
} else if (next.#s.protocols.length === 0) {
|
|
3336
|
+
next.#s.protocols = ["x402"];
|
|
3277
3337
|
}
|
|
3278
|
-
if (resolvedOptions?.maxPrice) next.
|
|
3279
|
-
if (resolvedOptions?.minPrice) next.
|
|
3280
|
-
if (resolvedOptions?.payTo) next.
|
|
3281
|
-
if (resolvedOptions?.mpp) next.
|
|
3282
|
-
if (resolvedOptions?.dynamic) next.
|
|
3283
|
-
if (resolvedOptions?.tickCost) next.
|
|
3284
|
-
if (resolvedOptions?.unitType) next.
|
|
3338
|
+
if (resolvedOptions?.maxPrice) next.#s.maxPrice = resolvedOptions.maxPrice;
|
|
3339
|
+
if (resolvedOptions?.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
|
|
3340
|
+
if (resolvedOptions?.payTo) next.#s.payTo = resolvedOptions.payTo;
|
|
3341
|
+
if (resolvedOptions?.mpp) next.#s.mppInfo = resolvedOptions.mpp;
|
|
3342
|
+
if (resolvedOptions?.dynamic) next.#s.dynamicPrice = true;
|
|
3343
|
+
if (resolvedOptions?.tickCost) next.#s.tickCost = resolvedOptions.tickCost;
|
|
3344
|
+
if (resolvedOptions?.unitType) next.#s.unitType = resolvedOptions.unitType;
|
|
3285
3345
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
3286
|
-
if (next.
|
|
3346
|
+
if (next.#s.dynamicPrice) {
|
|
3287
3347
|
throw new Error(
|
|
3288
|
-
`route '${this.
|
|
3348
|
+
`route '${this.#s.key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
|
|
3289
3349
|
);
|
|
3290
3350
|
}
|
|
3291
3351
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
3292
3352
|
if (!tierKey) {
|
|
3293
|
-
throw new Error(`route '${this.
|
|
3353
|
+
throw new Error(`route '${this.#s.key}': tier key cannot be empty`);
|
|
3294
3354
|
}
|
|
3295
|
-
|
|
3296
|
-
if (isNaN(tierPrice) || tierPrice <= 0) {
|
|
3355
|
+
if (!isPositiveDecimal(tierConfig.price)) {
|
|
3297
3356
|
throw new Error(
|
|
3298
|
-
`route '${this.
|
|
3357
|
+
`route '${this.#s.key}': tier '${tierKey}' price '${tierConfig.price}' must be a positive decimal string`
|
|
3299
3358
|
);
|
|
3300
3359
|
}
|
|
3301
3360
|
}
|
|
3302
3361
|
}
|
|
3303
|
-
if (resolvedOptions?.maxPrice !== void 0) {
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
`route '${this._key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
|
|
3308
|
-
);
|
|
3309
|
-
}
|
|
3362
|
+
if (resolvedOptions?.maxPrice !== void 0 && !isPositiveDecimal(resolvedOptions.maxPrice)) {
|
|
3363
|
+
throw new Error(
|
|
3364
|
+
`route '${this.#s.key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
|
|
3365
|
+
);
|
|
3310
3366
|
}
|
|
3311
|
-
if (resolvedOptions?.tickCost !== void 0) {
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
`route '${this._key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
|
|
3316
|
-
);
|
|
3317
|
-
}
|
|
3367
|
+
if (resolvedOptions?.tickCost !== void 0 && !isPositiveDecimal(resolvedOptions.tickCost)) {
|
|
3368
|
+
throw new Error(
|
|
3369
|
+
`route '${this.#s.key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
|
|
3370
|
+
);
|
|
3318
3371
|
}
|
|
3319
|
-
if (next.
|
|
3320
|
-
throw new Error(`route '${this.
|
|
3372
|
+
if (next.#s.dynamicPrice && !next.#s.maxPrice) {
|
|
3373
|
+
throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires maxPrice`);
|
|
3321
3374
|
}
|
|
3322
|
-
if (next.
|
|
3323
|
-
throw new Error(`route '${this.
|
|
3375
|
+
if (next.#s.dynamicPrice && !next.#s.tickCost) {
|
|
3376
|
+
throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires tickCost`);
|
|
3324
3377
|
}
|
|
3325
3378
|
return next;
|
|
3326
3379
|
}
|
|
@@ -3335,25 +3388,25 @@ var RouteBuilder = class {
|
|
|
3335
3388
|
* ```
|
|
3336
3389
|
*/
|
|
3337
3390
|
siwx() {
|
|
3338
|
-
if (this.
|
|
3391
|
+
if (this.#s.authMode === "unprotected") {
|
|
3339
3392
|
throw new Error(
|
|
3340
|
-
`route '${this.
|
|
3393
|
+
`route '${this.#s.key}': Cannot combine .unprotected() and .siwx() on the same route.`
|
|
3341
3394
|
);
|
|
3342
3395
|
}
|
|
3343
|
-
if (this.
|
|
3396
|
+
if (this.#s.apiKeyResolver) {
|
|
3344
3397
|
throw new Error(
|
|
3345
|
-
`route '${this.
|
|
3398
|
+
`route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
|
|
3346
3399
|
);
|
|
3347
3400
|
}
|
|
3348
3401
|
const next = this.fork();
|
|
3349
|
-
next.
|
|
3350
|
-
if (next.
|
|
3351
|
-
next.
|
|
3352
|
-
if (next.
|
|
3402
|
+
next.#s.siwxEnabled = true;
|
|
3403
|
+
if (next.#s.authMode === "paid" || next.#s.pricing) {
|
|
3404
|
+
next.#s.authMode = "paid";
|
|
3405
|
+
if (next.#s.protocols.length === 0) next.#s.protocols = ["x402"];
|
|
3353
3406
|
return next;
|
|
3354
3407
|
}
|
|
3355
|
-
next.
|
|
3356
|
-
next.
|
|
3408
|
+
next.#s.authMode = "siwx";
|
|
3409
|
+
next.#s.protocols = [];
|
|
3357
3410
|
return next;
|
|
3358
3411
|
}
|
|
3359
3412
|
/**
|
|
@@ -3370,14 +3423,14 @@ var RouteBuilder = class {
|
|
|
3370
3423
|
* ```
|
|
3371
3424
|
*/
|
|
3372
3425
|
apiKey(resolver) {
|
|
3373
|
-
if (this.
|
|
3426
|
+
if (this.#s.siwxEnabled) {
|
|
3374
3427
|
throw new Error(
|
|
3375
|
-
`route '${this.
|
|
3428
|
+
`route '${this.#s.key}': Combining .apiKey() and .siwx() is not supported on the same route.`
|
|
3376
3429
|
);
|
|
3377
3430
|
}
|
|
3378
3431
|
const next = this.fork();
|
|
3379
|
-
next.
|
|
3380
|
-
next.
|
|
3432
|
+
next.#s.authMode = "apiKey";
|
|
3433
|
+
next.#s.apiKeyResolver = resolver;
|
|
3381
3434
|
return next;
|
|
3382
3435
|
}
|
|
3383
3436
|
/**
|
|
@@ -3390,19 +3443,19 @@ var RouteBuilder = class {
|
|
|
3390
3443
|
* ```
|
|
3391
3444
|
*/
|
|
3392
3445
|
unprotected() {
|
|
3393
|
-
if (this.
|
|
3446
|
+
if (this.#s.authMode && this.#s.authMode !== "unprotected") {
|
|
3394
3447
|
throw new Error(
|
|
3395
|
-
`route '${this.
|
|
3448
|
+
`route '${this.#s.key}': Cannot combine .unprotected() and .${this.#s.authMode}() on the same route.`
|
|
3396
3449
|
);
|
|
3397
3450
|
}
|
|
3398
|
-
if (this.
|
|
3451
|
+
if (this.#s.pricing) {
|
|
3399
3452
|
throw new Error(
|
|
3400
|
-
`route '${this.
|
|
3453
|
+
`route '${this.#s.key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
3401
3454
|
);
|
|
3402
3455
|
}
|
|
3403
3456
|
const next = this.fork();
|
|
3404
|
-
next.
|
|
3405
|
-
next.
|
|
3457
|
+
next.#s.authMode = "unprotected";
|
|
3458
|
+
next.#s.protocols = [];
|
|
3406
3459
|
return next;
|
|
3407
3460
|
}
|
|
3408
3461
|
/**
|
|
@@ -3421,8 +3474,8 @@ var RouteBuilder = class {
|
|
|
3421
3474
|
*/
|
|
3422
3475
|
provider(name, config) {
|
|
3423
3476
|
const next = this.fork();
|
|
3424
|
-
next.
|
|
3425
|
-
next.
|
|
3477
|
+
next.#s.providerName = name;
|
|
3478
|
+
next.#s.providerConfig = config ?? {};
|
|
3426
3479
|
return next;
|
|
3427
3480
|
}
|
|
3428
3481
|
/**
|
|
@@ -3437,7 +3490,7 @@ var RouteBuilder = class {
|
|
|
3437
3490
|
*/
|
|
3438
3491
|
body(schema) {
|
|
3439
3492
|
const next = this.fork();
|
|
3440
|
-
next.
|
|
3493
|
+
next.#s.bodySchema = schema;
|
|
3441
3494
|
return next;
|
|
3442
3495
|
}
|
|
3443
3496
|
/**
|
|
@@ -3453,8 +3506,8 @@ var RouteBuilder = class {
|
|
|
3453
3506
|
*/
|
|
3454
3507
|
query(schema) {
|
|
3455
3508
|
const next = this.fork();
|
|
3456
|
-
next.
|
|
3457
|
-
next.
|
|
3509
|
+
next.#s.querySchema = schema;
|
|
3510
|
+
next.#s.method = "GET";
|
|
3458
3511
|
return next;
|
|
3459
3512
|
}
|
|
3460
3513
|
/**
|
|
@@ -3471,7 +3524,7 @@ var RouteBuilder = class {
|
|
|
3471
3524
|
*/
|
|
3472
3525
|
output(schema) {
|
|
3473
3526
|
const next = this.fork();
|
|
3474
|
-
next.
|
|
3527
|
+
next.#s.outputSchema = schema;
|
|
3475
3528
|
return next;
|
|
3476
3529
|
}
|
|
3477
3530
|
/**
|
|
@@ -3485,8 +3538,8 @@ var RouteBuilder = class {
|
|
|
3485
3538
|
*/
|
|
3486
3539
|
inputExample(example) {
|
|
3487
3540
|
const next = this.fork();
|
|
3488
|
-
next.
|
|
3489
|
-
next.
|
|
3541
|
+
next.#s.inputExample = example;
|
|
3542
|
+
next.#s.hasInputExample = true;
|
|
3490
3543
|
return next;
|
|
3491
3544
|
}
|
|
3492
3545
|
/**
|
|
@@ -3500,8 +3553,8 @@ var RouteBuilder = class {
|
|
|
3500
3553
|
*/
|
|
3501
3554
|
outputExample(example) {
|
|
3502
3555
|
const next = this.fork();
|
|
3503
|
-
next.
|
|
3504
|
-
next.
|
|
3556
|
+
next.#s.outputExample = example;
|
|
3557
|
+
next.#s.hasOutputExample = true;
|
|
3505
3558
|
return next;
|
|
3506
3559
|
}
|
|
3507
3560
|
/**
|
|
@@ -3515,7 +3568,7 @@ var RouteBuilder = class {
|
|
|
3515
3568
|
*/
|
|
3516
3569
|
description(text) {
|
|
3517
3570
|
const next = this.fork();
|
|
3518
|
-
next.
|
|
3571
|
+
next.#s.description = text;
|
|
3519
3572
|
return next;
|
|
3520
3573
|
}
|
|
3521
3574
|
/**
|
|
@@ -3529,7 +3582,7 @@ var RouteBuilder = class {
|
|
|
3529
3582
|
*/
|
|
3530
3583
|
path(p) {
|
|
3531
3584
|
const next = this.fork();
|
|
3532
|
-
next.
|
|
3585
|
+
next.#s.path = p;
|
|
3533
3586
|
return next;
|
|
3534
3587
|
}
|
|
3535
3588
|
/**
|
|
@@ -3543,7 +3596,7 @@ var RouteBuilder = class {
|
|
|
3543
3596
|
*/
|
|
3544
3597
|
method(m) {
|
|
3545
3598
|
const next = this.fork();
|
|
3546
|
-
next.
|
|
3599
|
+
next.#s.method = m;
|
|
3547
3600
|
return next;
|
|
3548
3601
|
}
|
|
3549
3602
|
/**
|
|
@@ -3562,7 +3615,7 @@ var RouteBuilder = class {
|
|
|
3562
3615
|
*/
|
|
3563
3616
|
validate(fn) {
|
|
3564
3617
|
const next = this.fork();
|
|
3565
|
-
next.
|
|
3618
|
+
next.#s.validateFn = fn;
|
|
3566
3619
|
return next;
|
|
3567
3620
|
}
|
|
3568
3621
|
/**
|
|
@@ -3580,7 +3633,7 @@ var RouteBuilder = class {
|
|
|
3580
3633
|
*/
|
|
3581
3634
|
settlement(lifecycle) {
|
|
3582
3635
|
const next = this.fork();
|
|
3583
|
-
next.
|
|
3636
|
+
next.#s.settlement = lifecycle;
|
|
3584
3637
|
return next;
|
|
3585
3638
|
}
|
|
3586
3639
|
/**
|
|
@@ -3623,79 +3676,79 @@ var RouteBuilder = class {
|
|
|
3623
3676
|
return this.register(fn, true);
|
|
3624
3677
|
}
|
|
3625
3678
|
register(handlerFn, streaming) {
|
|
3626
|
-
if (!this.
|
|
3679
|
+
if (!this.#s.authMode) {
|
|
3627
3680
|
throw new Error(
|
|
3628
|
-
`route '${this.
|
|
3681
|
+
`route '${this.#s.key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
3629
3682
|
);
|
|
3630
3683
|
}
|
|
3631
|
-
if (this.
|
|
3684
|
+
if (this.#s.validateFn && !this.#s.bodySchema) {
|
|
3632
3685
|
throw new Error(
|
|
3633
|
-
`route '${this.
|
|
3686
|
+
`route '${this.#s.key}': .validate() requires .body() \u2014 validation runs on parsed body`
|
|
3634
3687
|
);
|
|
3635
3688
|
}
|
|
3636
|
-
if (this.
|
|
3637
|
-
throw new Error(`route '${this.
|
|
3689
|
+
if (this.#s.settlement && !this.#s.pricing) {
|
|
3690
|
+
throw new Error(`route '${this.#s.key}': .settlement() requires a paid route`);
|
|
3638
3691
|
}
|
|
3639
|
-
if (this.
|
|
3640
|
-
const hasUpto = this.
|
|
3692
|
+
if (this.#s.dynamicPrice && this.#s.protocols.includes("x402")) {
|
|
3693
|
+
const hasUpto = this.#s.deps.x402Accepts.some((accept) => accept.scheme === "upto");
|
|
3641
3694
|
if (!hasUpto) {
|
|
3642
3695
|
throw new Error(
|
|
3643
|
-
`route '${this.
|
|
3696
|
+
`route '${this.#s.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.`
|
|
3644
3697
|
);
|
|
3645
3698
|
}
|
|
3646
3699
|
}
|
|
3647
|
-
if (this.
|
|
3648
|
-
if (!this.
|
|
3700
|
+
if (this.#s.dynamicPrice && this.#s.protocols.includes("mpp")) {
|
|
3701
|
+
if (!this.#s.deps.mppSessionConfig) {
|
|
3649
3702
|
throw new Error(
|
|
3650
|
-
`route '${this.
|
|
3703
|
+
`route '${this.#s.key}': .paid({ dynamic: true }) on an MPP route requires session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
|
|
3651
3704
|
);
|
|
3652
3705
|
}
|
|
3653
3706
|
}
|
|
3654
|
-
if (streaming && !this.
|
|
3707
|
+
if (streaming && !this.#s.dynamicPrice) {
|
|
3655
3708
|
throw new Error(
|
|
3656
|
-
`route '${this.
|
|
3709
|
+
`route '${this.#s.key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
|
|
3657
3710
|
);
|
|
3658
3711
|
}
|
|
3659
3712
|
validateExamples(
|
|
3660
|
-
this.
|
|
3661
|
-
this.
|
|
3662
|
-
this.
|
|
3663
|
-
this.
|
|
3664
|
-
this.
|
|
3665
|
-
this.
|
|
3666
|
-
this.
|
|
3667
|
-
this.
|
|
3713
|
+
this.#s.key,
|
|
3714
|
+
this.#s.bodySchema,
|
|
3715
|
+
this.#s.querySchema,
|
|
3716
|
+
this.#s.outputSchema,
|
|
3717
|
+
this.#s.inputExample,
|
|
3718
|
+
this.#s.hasInputExample,
|
|
3719
|
+
this.#s.outputExample,
|
|
3720
|
+
this.#s.hasOutputExample
|
|
3668
3721
|
);
|
|
3669
3722
|
const entry = {
|
|
3670
|
-
key: this.
|
|
3671
|
-
authMode: this.
|
|
3672
|
-
siwxEnabled: this.
|
|
3673
|
-
pricing: this.
|
|
3674
|
-
dynamicPrice: this.
|
|
3723
|
+
key: this.#s.key,
|
|
3724
|
+
authMode: this.#s.authMode,
|
|
3725
|
+
siwxEnabled: this.#s.siwxEnabled,
|
|
3726
|
+
pricing: this.#s.pricing,
|
|
3727
|
+
dynamicPrice: this.#s.dynamicPrice ? true : void 0,
|
|
3675
3728
|
streaming: streaming ? true : void 0,
|
|
3676
|
-
protocols: this.
|
|
3677
|
-
bodySchema: this.
|
|
3678
|
-
querySchema: this.
|
|
3679
|
-
outputSchema: this.
|
|
3680
|
-
inputExample: this.
|
|
3681
|
-
outputExample: this.
|
|
3682
|
-
description: this.
|
|
3683
|
-
path: this.
|
|
3684
|
-
method: this.
|
|
3685
|
-
maxPrice: this.
|
|
3686
|
-
minPrice: this.
|
|
3687
|
-
payTo: this.
|
|
3688
|
-
apiKeyResolver: this.
|
|
3689
|
-
providerName: this.
|
|
3690
|
-
providerConfig: this.
|
|
3691
|
-
validateFn: this.
|
|
3692
|
-
settlement: this.
|
|
3693
|
-
mppInfo: this.
|
|
3694
|
-
tickCost: this.
|
|
3695
|
-
unitType: this.
|
|
3729
|
+
protocols: this.#s.protocols,
|
|
3730
|
+
bodySchema: this.#s.bodySchema,
|
|
3731
|
+
querySchema: this.#s.querySchema,
|
|
3732
|
+
outputSchema: this.#s.outputSchema,
|
|
3733
|
+
inputExample: this.#s.hasInputExample ? this.#s.inputExample : void 0,
|
|
3734
|
+
outputExample: this.#s.hasOutputExample ? this.#s.outputExample : void 0,
|
|
3735
|
+
description: this.#s.description,
|
|
3736
|
+
path: this.#s.path,
|
|
3737
|
+
method: this.#s.method,
|
|
3738
|
+
maxPrice: this.#s.maxPrice,
|
|
3739
|
+
minPrice: this.#s.minPrice,
|
|
3740
|
+
payTo: this.#s.payTo,
|
|
3741
|
+
apiKeyResolver: this.#s.apiKeyResolver,
|
|
3742
|
+
providerName: this.#s.providerName,
|
|
3743
|
+
providerConfig: this.#s.providerConfig,
|
|
3744
|
+
validateFn: this.#s.validateFn,
|
|
3745
|
+
settlement: this.#s.settlement,
|
|
3746
|
+
mppInfo: this.#s.mppInfo,
|
|
3747
|
+
tickCost: this.#s.tickCost,
|
|
3748
|
+
unitType: this.#s.unitType
|
|
3696
3749
|
};
|
|
3697
|
-
this.
|
|
3698
|
-
return createRequestHandler(entry, handlerFn, this.
|
|
3750
|
+
this.#s.registry.register(entry);
|
|
3751
|
+
return createRequestHandler(entry, handlerFn, this.#s.deps);
|
|
3699
3752
|
}
|
|
3700
3753
|
};
|
|
3701
3754
|
function resolvePaidArgs(routeKey, pricingOrOptions, options) {
|
|
@@ -3915,7 +3968,7 @@ function toProtocolObject(protocol, mppInfo) {
|
|
|
3915
3968
|
mpp: {
|
|
3916
3969
|
method: mppInfo?.method ?? "tempo",
|
|
3917
3970
|
intent: mppInfo?.intent ?? "charge",
|
|
3918
|
-
currency: mppInfo?.currency ??
|
|
3971
|
+
currency: mppInfo?.currency ?? TEMPO_USDC_ADDRESS
|
|
3919
3972
|
}
|
|
3920
3973
|
};
|
|
3921
3974
|
}
|
|
@@ -3939,17 +3992,16 @@ function buildPricingInfo(entry) {
|
|
|
3939
3992
|
};
|
|
3940
3993
|
}
|
|
3941
3994
|
if ("tiers" in entry.pricing) {
|
|
3942
|
-
const tierPrices = Object.values(entry.pricing.tiers).map((tier) =>
|
|
3943
|
-
const
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
if (min === max) {
|
|
3995
|
+
const tierPrices = Object.values(entry.pricing.tiers).map((tier) => tier.price);
|
|
3996
|
+
const extrema = tierExtrema(tierPrices);
|
|
3997
|
+
if (extrema) {
|
|
3998
|
+
if (extrema.min === extrema.max) {
|
|
3947
3999
|
return {
|
|
3948
|
-
price: { mode: "fixed", currency: "USD", amount:
|
|
4000
|
+
price: { mode: "fixed", currency: "USD", amount: extrema.min }
|
|
3949
4001
|
};
|
|
3950
4002
|
}
|
|
3951
4003
|
return {
|
|
3952
|
-
price: { mode: "dynamic", currency: "USD", min:
|
|
4004
|
+
price: { mode: "dynamic", currency: "USD", min: extrema.min, max: extrema.max }
|
|
3953
4005
|
};
|
|
3954
4006
|
}
|
|
3955
4007
|
return {
|
|
@@ -3963,6 +4015,20 @@ function buildPricingInfo(entry) {
|
|
|
3963
4015
|
}
|
|
3964
4016
|
return void 0;
|
|
3965
4017
|
}
|
|
4018
|
+
function tierExtrema(prices) {
|
|
4019
|
+
if (prices.length === 0) return null;
|
|
4020
|
+
let min = prices[0];
|
|
4021
|
+
let max = prices[0];
|
|
4022
|
+
try {
|
|
4023
|
+
for (const price of prices.slice(1)) {
|
|
4024
|
+
if (compareDecimals(price, min) < 0) min = price;
|
|
4025
|
+
if (compareDecimals(price, max) > 0) max = price;
|
|
4026
|
+
}
|
|
4027
|
+
} catch {
|
|
4028
|
+
return null;
|
|
4029
|
+
}
|
|
4030
|
+
return { min, max };
|
|
4031
|
+
}
|
|
3966
4032
|
|
|
3967
4033
|
// src/discovery/llms-txt.ts
|
|
3968
4034
|
import { NextResponse as NextResponse10 } from "next/server";
|
|
@@ -3995,142 +4061,256 @@ function formatRouterConfigIssues(issues) {
|
|
|
3995
4061
|
return issues.map((issue) => issue.message).join("\n");
|
|
3996
4062
|
}
|
|
3997
4063
|
|
|
3998
|
-
// src/config/
|
|
3999
|
-
|
|
4064
|
+
// src/config/schema.ts
|
|
4065
|
+
init_constants();
|
|
4066
|
+
import { z } from "zod";
|
|
4000
4067
|
|
|
4001
|
-
// src/config/
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4068
|
+
// src/config/utils.ts
|
|
4069
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
4070
|
+
var EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
4071
|
+
var EVM_PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
|
|
4072
|
+
var SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
4073
|
+
var ZERO_EVM_ADDRESS_RE = /^0x0{40}$/i;
|
|
4074
|
+
function isUrl(value) {
|
|
4075
|
+
try {
|
|
4076
|
+
new URL(value);
|
|
4077
|
+
return true;
|
|
4078
|
+
} catch {
|
|
4079
|
+
return false;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
var isEvmAddress = (v) => EVM_ADDRESS_RE.test(v);
|
|
4083
|
+
var isEvmPrivateKey = (v) => EVM_PRIVATE_KEY_RE.test(v);
|
|
4084
|
+
var isPlaceholderEvm = (v) => ZERO_EVM_ADDRESS_RE.test(v);
|
|
4085
|
+
var isSolanaAddress = (v) => SOLANA_ADDRESS_RE.test(v);
|
|
4086
|
+
var isX402Network = (v) => v.startsWith("eip155:") || v.startsWith("solana:");
|
|
4087
|
+
var canonicalizeEvm = (addr) => addr.toLowerCase();
|
|
4088
|
+
function operatorAddressesCollide(opKey, fpKey) {
|
|
4089
|
+
if (!opKey || !fpKey || !isEvmPrivateKey(opKey) || !isEvmPrivateKey(fpKey)) return null;
|
|
4090
|
+
const op = privateKeyToAccount(opKey).address.toLowerCase();
|
|
4091
|
+
const fp = privateKeyToAccount(fpKey).address.toLowerCase();
|
|
4092
|
+
return op === fp ? op : null;
|
|
4093
|
+
}
|
|
4094
|
+
function trimAll(raw) {
|
|
4095
|
+
const out = {};
|
|
4096
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
4097
|
+
if (typeof v !== "string") {
|
|
4098
|
+
out[k] = void 0;
|
|
4099
|
+
continue;
|
|
4100
|
+
}
|
|
4101
|
+
const trimmed = v.trim();
|
|
4102
|
+
out[k] = trimmed.length > 0 ? trimmed : void 0;
|
|
4103
|
+
}
|
|
4104
|
+
return out;
|
|
4021
4105
|
}
|
|
4022
4106
|
|
|
4023
|
-
// src/config/
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4107
|
+
// src/config/schema.ts
|
|
4108
|
+
function addIssue(ctx, params, message, path = []) {
|
|
4109
|
+
ctx.addIssue({ code: "custom", path, params, message });
|
|
4110
|
+
}
|
|
4111
|
+
var x402 = { protocol: "x402" };
|
|
4112
|
+
var mpp = { protocol: "mpp" };
|
|
4113
|
+
var envShape = {
|
|
4114
|
+
BASE_URL: z.string().refine(isUrl, {
|
|
4115
|
+
params: { code: "invalid_base_url" },
|
|
4116
|
+
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."
|
|
4117
|
+
}).optional(),
|
|
4118
|
+
EVM_PAYEE_ADDRESS: z.string().refine(isEvmAddress, {
|
|
4119
|
+
params: { code: "invalid_x402_payee", ...x402 },
|
|
4120
|
+
message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
|
|
4121
|
+
}).refine((v) => !isPlaceholderEvm(v), {
|
|
4122
|
+
params: { code: "placeholder_payee", ...x402 },
|
|
4123
|
+
message: "EVM_PAYEE_ADDRESS is the zero address (0x000\u2026000) \u2014 payments to this address are unrecoverable. Set it to a wallet you control."
|
|
4124
|
+
}).optional(),
|
|
4125
|
+
CDP_API_KEY_ID: z.string().optional(),
|
|
4126
|
+
CDP_API_KEY_SECRET: z.string().optional(),
|
|
4127
|
+
SOLANA_PAYEE_ADDRESS: z.string().refine(isSolanaAddress, {
|
|
4128
|
+
params: { code: "invalid_solana_payee", ...x402 },
|
|
4129
|
+
message: "SOLANA_PAYEE_ADDRESS must be a base58 Solana address (32\u201344 chars). When set, the router also accepts Solana payments."
|
|
4130
|
+
}).optional(),
|
|
4131
|
+
SOLANA_FACILITATOR_URL: z.string().refine(isUrl, {
|
|
4132
|
+
params: { code: "invalid_solana_facilitator_url", ...x402 },
|
|
4133
|
+
message: "SOLANA_FACILITATOR_URL must be a valid URL \u2014 override for the Solana x402 facilitator. Defaults to DEFAULT_SOLANA_FACILITATOR_URL."
|
|
4134
|
+
}).optional(),
|
|
4135
|
+
MPP_SECRET_KEY: z.string().optional(),
|
|
4136
|
+
MPP_CURRENCY: z.string().refine(isEvmAddress, {
|
|
4137
|
+
params: { code: "invalid_mpp_currency", ...mpp },
|
|
4138
|
+
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."
|
|
4139
|
+
}).optional(),
|
|
4140
|
+
TEMPO_RPC_URL: z.string().refine(isUrl, {
|
|
4141
|
+
params: { code: "invalid_mpp_rpc_url", ...mpp },
|
|
4142
|
+
message: "TEMPO_RPC_URL must be a valid URL \u2014 authenticated Tempo JSON-RPC endpoint. Public rpc.tempo.xyz returns 401."
|
|
4143
|
+
}).optional(),
|
|
4144
|
+
MPP_OPERATOR_KEY: z.string().refine(isEvmPrivateKey, {
|
|
4145
|
+
params: { code: "invalid_mpp_operator_key", ...mpp },
|
|
4146
|
+
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."
|
|
4147
|
+
}).optional(),
|
|
4148
|
+
MPP_FEE_PAYER_KEY: z.string().refine(isEvmPrivateKey, {
|
|
4149
|
+
params: { code: "invalid_mpp_fee_payer_key", ...mpp },
|
|
4150
|
+
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."
|
|
4151
|
+
}).optional(),
|
|
4152
|
+
KV_REST_API_URL: z.string().optional(),
|
|
4153
|
+
KV_REST_API_TOKEN: z.string().optional(),
|
|
4154
|
+
NODE_ENV: z.string().optional()
|
|
4155
|
+
};
|
|
4156
|
+
var ENV_KEYS = Object.keys(envShape);
|
|
4157
|
+
var EnvInputSchema = z.object(envShape).passthrough().superRefine((env, ctx) => {
|
|
4158
|
+
if (env.BASE_URL === void 0) {
|
|
4159
|
+
addIssue(
|
|
4160
|
+
ctx,
|
|
4161
|
+
{ code: "missing_base_url" },
|
|
4162
|
+
"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.",
|
|
4163
|
+
["BASE_URL"]
|
|
4164
|
+
);
|
|
4165
|
+
}
|
|
4166
|
+
if (env.EVM_PAYEE_ADDRESS === void 0) {
|
|
4167
|
+
addIssue(
|
|
4168
|
+
ctx,
|
|
4169
|
+
{ code: "missing_x402_payee", ...x402 },
|
|
4170
|
+
"EVM_PAYEE_ADDRESS is required \u2014 the EVM address that receives x402 and MPP payments.",
|
|
4171
|
+
["EVM_PAYEE_ADDRESS"]
|
|
4172
|
+
);
|
|
4173
|
+
}
|
|
4174
|
+
if (env.MPP_SECRET_KEY) {
|
|
4175
|
+
if (env.MPP_CURRENCY === void 0) {
|
|
4176
|
+
addIssue(
|
|
4177
|
+
ctx,
|
|
4178
|
+
{ code: "missing_mpp_currency", ...mpp },
|
|
4179
|
+
"MPP_CURRENCY is required when MPP is enabled \u2014 the Tempo currency address MPP charges in. Use TEMPO_USDC_ADDRESS for Tempo USDC.",
|
|
4180
|
+
["MPP_CURRENCY"]
|
|
4181
|
+
);
|
|
4182
|
+
}
|
|
4183
|
+
if (env.TEMPO_RPC_URL === void 0) {
|
|
4184
|
+
addIssue(
|
|
4185
|
+
ctx,
|
|
4186
|
+
{ code: "missing_mpp_rpc_url", ...mpp },
|
|
4187
|
+
"TEMPO_RPC_URL is required when MPP is enabled \u2014 authenticated Tempo JSON-RPC endpoint. Public rpc.tempo.xyz returns 401.",
|
|
4188
|
+
["TEMPO_RPC_URL"]
|
|
4189
|
+
);
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
const collision = operatorAddressesCollide(env.MPP_OPERATOR_KEY, env.MPP_FEE_PAYER_KEY);
|
|
4193
|
+
if (collision) {
|
|
4194
|
+
addIssue(
|
|
4195
|
+
ctx,
|
|
4196
|
+
{ code: "mpp_operator_equals_fee_payer", ...mpp },
|
|
4197
|
+
`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.`,
|
|
4198
|
+
["MPP_FEE_PAYER_KEY"]
|
|
4199
|
+
);
|
|
4200
|
+
}
|
|
4201
|
+
});
|
|
4202
|
+
function collectKvWarnings(env, kvStoreOptionProvided) {
|
|
4203
|
+
if (kvStoreOptionProvided) return [];
|
|
4204
|
+
const warn = (code, message) => ({
|
|
4205
|
+
code,
|
|
4206
|
+
severity: "warning",
|
|
4207
|
+
message
|
|
4208
|
+
});
|
|
4209
|
+
if (env.KV_REST_API_URL && !env.KV_REST_API_TOKEN) {
|
|
4036
4210
|
return [
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
}
|
|
4211
|
+
warn(
|
|
4212
|
+
"kv_url_without_token",
|
|
4213
|
+
"KV_REST_API_URL is set but KV_REST_API_TOKEN is missing \u2014 falling back to in-memory KV (unsafe in serverless production)."
|
|
4214
|
+
)
|
|
4042
4215
|
];
|
|
4043
4216
|
}
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4217
|
+
if (env.KV_REST_API_TOKEN && !env.KV_REST_API_URL) {
|
|
4218
|
+
return [
|
|
4219
|
+
warn(
|
|
4220
|
+
"kv_token_without_url",
|
|
4221
|
+
"KV_REST_API_TOKEN is set but KV_REST_API_URL is missing \u2014 falling back to in-memory KV (unsafe in serverless production)."
|
|
4222
|
+
)
|
|
4223
|
+
];
|
|
4224
|
+
}
|
|
4225
|
+
if (env.KV_REST_API_URL && env.KV_REST_API_TOKEN && !isUrl(env.KV_REST_API_URL)) {
|
|
4226
|
+
return [
|
|
4227
|
+
warn(
|
|
4228
|
+
"invalid_kv_url",
|
|
4229
|
+
`KV_REST_API_URL is not a valid URL \u2014 KV calls will fail at request time. Got: ${env.KV_REST_API_URL}`
|
|
4230
|
+
)
|
|
4231
|
+
];
|
|
4232
|
+
}
|
|
4233
|
+
if (!env.KV_REST_API_URL && !env.KV_REST_API_TOKEN && env.NODE_ENV === "production") {
|
|
4234
|
+
return [
|
|
4235
|
+
warn(
|
|
4236
|
+
"missing_kv_in_production",
|
|
4237
|
+
"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."
|
|
4238
|
+
)
|
|
4239
|
+
];
|
|
4240
|
+
}
|
|
4241
|
+
return [];
|
|
4069
4242
|
}
|
|
4070
|
-
function
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
};
|
|
4243
|
+
function getConfiguredX402Accepts2(config) {
|
|
4244
|
+
if (config.x402?.accepts?.length) return [...config.x402.accepts];
|
|
4245
|
+
return [
|
|
4246
|
+
{
|
|
4247
|
+
scheme: "exact",
|
|
4248
|
+
network: config.network ?? BASE_MAINNET_NETWORK,
|
|
4249
|
+
payTo: config.payeeAddress
|
|
4250
|
+
}
|
|
4251
|
+
];
|
|
4080
4252
|
}
|
|
4081
|
-
function
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
}
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4253
|
+
function validateX402Config(config, env, options) {
|
|
4254
|
+
const accepts = getConfiguredX402Accepts2(config);
|
|
4255
|
+
const issues = [];
|
|
4256
|
+
const push = (code, message) => issues.push({ code, protocol: "x402", message });
|
|
4257
|
+
if (accepts.length === 0) {
|
|
4258
|
+
push("missing_x402_accepts", "x402 requires at least one accept configuration.");
|
|
4259
|
+
return issues;
|
|
4260
|
+
}
|
|
4261
|
+
if (accepts.some((a) => !a.network)) {
|
|
4262
|
+
push("missing_x402_network", "x402 accepts require a network.");
|
|
4263
|
+
}
|
|
4264
|
+
const unsupported = accepts.find((a) => a.network && !isX402Network(a.network));
|
|
4265
|
+
if (unsupported) {
|
|
4266
|
+
push(
|
|
4267
|
+
"unsupported_x402_network",
|
|
4268
|
+
`unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
4269
|
+
);
|
|
4270
|
+
}
|
|
4271
|
+
if (accepts.some((a) => (a.scheme ?? "exact") !== "exact" && !a.asset)) {
|
|
4272
|
+
push("missing_x402_asset", "non-exact x402 accepts require an asset.");
|
|
4273
|
+
}
|
|
4274
|
+
if (accepts.some(
|
|
4275
|
+
(a) => a.decimals !== void 0 && (!Number.isInteger(a.decimals) || a.decimals < 0)
|
|
4276
|
+
)) {
|
|
4277
|
+
push("invalid_x402_decimals", "x402 accept decimals must be a non-negative integer.");
|
|
4278
|
+
}
|
|
4279
|
+
if (!config.payeeAddress && accepts.some((a) => !a.payTo)) {
|
|
4280
|
+
push(
|
|
4281
|
+
"missing_x402_payee",
|
|
4282
|
+
"x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
4283
|
+
);
|
|
4284
|
+
}
|
|
4285
|
+
const placeholder = [
|
|
4094
4286
|
config.payeeAddress,
|
|
4095
|
-
...accepts.map((
|
|
4096
|
-
]);
|
|
4097
|
-
if (
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
}
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
}
|
|
4287
|
+
...accepts.map((a) => typeof a.payTo === "string" ? a.payTo : void 0)
|
|
4288
|
+
].find((v) => v !== void 0 && isPlaceholderEvm(v));
|
|
4289
|
+
if (placeholder) {
|
|
4290
|
+
push(
|
|
4291
|
+
"placeholder_payee",
|
|
4292
|
+
`x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
4293
|
+
);
|
|
4294
|
+
}
|
|
4295
|
+
if (options.requireCdpKeys !== false) {
|
|
4296
|
+
const hasEvm = accepts.some(
|
|
4297
|
+
(a) => typeof a.network === "string" && a.network.startsWith("eip155:")
|
|
4298
|
+
);
|
|
4299
|
+
if (hasEvm) {
|
|
4300
|
+
const missing = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET"].filter((k) => !env[k]);
|
|
4301
|
+
if (missing.length > 0) {
|
|
4302
|
+
push(
|
|
4303
|
+
"missing_cdp_keys",
|
|
4304
|
+
`x402 EVM facilitator (Coinbase) requires ${missing.join(" and ")}.`
|
|
4305
|
+
);
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
return issues;
|
|
4117
4310
|
}
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
var CHECKS2 = [
|
|
4122
|
-
checkSecretKey,
|
|
4123
|
-
checkCurrency,
|
|
4124
|
-
checkRecipient,
|
|
4125
|
-
checkPlaceholderRecipient,
|
|
4126
|
-
checkRpcUrl,
|
|
4127
|
-
checkFeePayerKey,
|
|
4128
|
-
checkOperatorKey,
|
|
4129
|
-
checkOperatorMatchesFeePayer
|
|
4130
|
-
];
|
|
4131
|
-
function validateMppConfig(config, env) {
|
|
4132
|
-
const mpp = config.mpp;
|
|
4133
|
-
if (!mpp) {
|
|
4311
|
+
function validateMppConfig(config) {
|
|
4312
|
+
const m = config.mpp;
|
|
4313
|
+
if (!m) {
|
|
4134
4314
|
return [
|
|
4135
4315
|
{
|
|
4136
4316
|
code: "missing_mpp_config",
|
|
@@ -4139,109 +4319,184 @@ function validateMppConfig(config, env) {
|
|
|
4139
4319
|
}
|
|
4140
4320
|
];
|
|
4141
4321
|
}
|
|
4142
|
-
const
|
|
4143
|
-
|
|
4144
|
-
|
|
4322
|
+
const issues = [];
|
|
4323
|
+
const push = (code, message) => issues.push({ code, protocol: "mpp", message });
|
|
4324
|
+
if (!m.secretKey) {
|
|
4325
|
+
push(
|
|
4326
|
+
"missing_mpp_secret_key",
|
|
4327
|
+
"MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
4328
|
+
);
|
|
4329
|
+
}
|
|
4330
|
+
if (!m.currency) {
|
|
4331
|
+
push("missing_mpp_currency", "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency.");
|
|
4332
|
+
} else if (!isEvmAddress(m.currency)) {
|
|
4333
|
+
push(
|
|
4334
|
+
"invalid_mpp_currency",
|
|
4335
|
+
"MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_ADDRESS for Tempo USDC."
|
|
4336
|
+
);
|
|
4337
|
+
}
|
|
4338
|
+
const recipient = m.recipient ?? config.payeeAddress;
|
|
4339
|
+
if (!recipient) {
|
|
4340
|
+
push(
|
|
4341
|
+
"missing_mpp_recipient",
|
|
4342
|
+
"MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
4343
|
+
);
|
|
4344
|
+
} else if (!isEvmAddress(recipient)) {
|
|
4345
|
+
push(
|
|
4346
|
+
"invalid_mpp_recipient",
|
|
4347
|
+
"MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
4348
|
+
);
|
|
4349
|
+
}
|
|
4350
|
+
const placeholder = [m.recipient, config.payeeAddress].find(
|
|
4351
|
+
(v) => typeof v === "string" && isPlaceholderEvm(v)
|
|
4145
4352
|
);
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
protocol: "mpp",
|
|
4152
|
-
message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
4153
|
-
};
|
|
4154
|
-
}
|
|
4155
|
-
function checkCurrency({ mpp }) {
|
|
4156
|
-
if (!mpp.currency) {
|
|
4157
|
-
return {
|
|
4158
|
-
code: "missing_mpp_currency",
|
|
4159
|
-
protocol: "mpp",
|
|
4160
|
-
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
4161
|
-
};
|
|
4353
|
+
if (placeholder) {
|
|
4354
|
+
push(
|
|
4355
|
+
"placeholder_payee",
|
|
4356
|
+
`MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
4357
|
+
);
|
|
4162
4358
|
}
|
|
4163
|
-
if (!
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
};
|
|
4359
|
+
if (!m.rpcUrl) {
|
|
4360
|
+
push(
|
|
4361
|
+
"missing_mpp_rpc_url",
|
|
4362
|
+
"MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
4363
|
+
);
|
|
4169
4364
|
}
|
|
4170
|
-
|
|
4365
|
+
if (m.feePayerKey && !isEvmPrivateKey(m.feePayerKey)) {
|
|
4366
|
+
push(
|
|
4367
|
+
"invalid_mpp_fee_payer_key",
|
|
4368
|
+
"MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
4369
|
+
);
|
|
4370
|
+
}
|
|
4371
|
+
if (m.operatorKey && !isEvmPrivateKey(m.operatorKey)) {
|
|
4372
|
+
push(
|
|
4373
|
+
"invalid_mpp_operator_key",
|
|
4374
|
+
"MPP operatorKey must be a 0x-prefixed 32-byte EVM private key."
|
|
4375
|
+
);
|
|
4376
|
+
}
|
|
4377
|
+
const collision = operatorAddressesCollide(m.operatorKey, m.feePayerKey);
|
|
4378
|
+
if (collision) {
|
|
4379
|
+
push(
|
|
4380
|
+
"mpp_operator_equals_fee_payer",
|
|
4381
|
+
`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).`
|
|
4382
|
+
);
|
|
4383
|
+
}
|
|
4384
|
+
return issues;
|
|
4171
4385
|
}
|
|
4172
|
-
function
|
|
4173
|
-
|
|
4174
|
-
|
|
4386
|
+
function translateZodIssues(error) {
|
|
4387
|
+
return error.issues.map((issue) => {
|
|
4388
|
+
const params = issue.params;
|
|
4389
|
+
if (!params?.code) {
|
|
4390
|
+
throw new Error(
|
|
4391
|
+
`[router] schema issue missing params.code (path=${issue.path.join(".")}, message=${issue.message}). Every refinement / addIssue call must set params.code.`
|
|
4392
|
+
);
|
|
4393
|
+
}
|
|
4175
4394
|
return {
|
|
4176
|
-
code:
|
|
4177
|
-
|
|
4178
|
-
|
|
4395
|
+
code: params.code,
|
|
4396
|
+
message: issue.message,
|
|
4397
|
+
...params.protocol ? { protocol: params.protocol } : {},
|
|
4398
|
+
...params.severity ? { severity: params.severity } : {}
|
|
4179
4399
|
};
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
function routerConfigFromEnv(options) {
|
|
4403
|
+
const rawEnv = options.env ?? process.env;
|
|
4404
|
+
const env = trimAll(rawEnv);
|
|
4405
|
+
const optionIssues = [];
|
|
4406
|
+
if (!options.title?.trim()) {
|
|
4407
|
+
optionIssues.push({
|
|
4408
|
+
code: "missing_discovery_title",
|
|
4409
|
+
message: "discovery `title` is required. Pass a short product name."
|
|
4410
|
+
});
|
|
4180
4411
|
}
|
|
4181
|
-
if (!
|
|
4182
|
-
|
|
4183
|
-
code: "
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
};
|
|
4412
|
+
if (!options.description?.trim()) {
|
|
4413
|
+
optionIssues.push({
|
|
4414
|
+
code: "missing_discovery_description",
|
|
4415
|
+
message: "discovery `description` is required. One sentence is enough."
|
|
4416
|
+
});
|
|
4187
4417
|
}
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
}
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
}
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
const
|
|
4227
|
-
const
|
|
4228
|
-
|
|
4418
|
+
if (options.guidance === void 0) {
|
|
4419
|
+
optionIssues.push({
|
|
4420
|
+
code: "missing_discovery_guidance",
|
|
4421
|
+
message: "discovery `guidance` is required. Provide an empty string to opt out of `/llms.txt`."
|
|
4422
|
+
});
|
|
4423
|
+
}
|
|
4424
|
+
if (options.serverUrl !== void 0 && !isUrl(options.serverUrl)) {
|
|
4425
|
+
optionIssues.push({
|
|
4426
|
+
code: "invalid_server_url",
|
|
4427
|
+
message: `discovery \`serverUrl\` must be a valid URL. Got: ${options.serverUrl}`
|
|
4428
|
+
});
|
|
4429
|
+
}
|
|
4430
|
+
const parsed = EnvInputSchema.safeParse(env);
|
|
4431
|
+
const envIssues = parsed.success ? [] : translateZodIssues(parsed.error);
|
|
4432
|
+
const issues = [...envIssues, ...optionIssues];
|
|
4433
|
+
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
4434
|
+
for (const warning of collectKvWarnings(env, options.kvStore !== void 0)) {
|
|
4435
|
+
console.warn(`[router] ${warning.message}`);
|
|
4436
|
+
}
|
|
4437
|
+
const payeeAddress = canonicalizeEvm(env.EVM_PAYEE_ADDRESS);
|
|
4438
|
+
const accepts = [
|
|
4439
|
+
{ scheme: "exact", network: BASE_MAINNET_NETWORK, payTo: payeeAddress },
|
|
4440
|
+
{
|
|
4441
|
+
scheme: "upto",
|
|
4442
|
+
network: BASE_MAINNET_NETWORK,
|
|
4443
|
+
payTo: payeeAddress,
|
|
4444
|
+
asset: BASE_USDC_ADDRESS,
|
|
4445
|
+
decimals: BASE_USDC_DECIMALS
|
|
4446
|
+
}
|
|
4447
|
+
];
|
|
4448
|
+
if (env.SOLANA_PAYEE_ADDRESS) {
|
|
4449
|
+
accepts.push({
|
|
4450
|
+
scheme: "exact",
|
|
4451
|
+
network: SOLANA_MAINNET_NETWORK,
|
|
4452
|
+
payTo: env.SOLANA_PAYEE_ADDRESS
|
|
4453
|
+
});
|
|
4454
|
+
}
|
|
4455
|
+
const configuredSolanaFacilitator = options.x402Facilitators?.solana;
|
|
4456
|
+
const solanaFacilitator = typeof configuredSolanaFacilitator === "string" ? configuredSolanaFacilitator : configuredSolanaFacilitator ?? env.SOLANA_FACILITATOR_URL ?? DEFAULT_SOLANA_FACILITATOR_URL;
|
|
4457
|
+
const mppEnabled = options.protocols?.includes("mpp") ?? Boolean(env.MPP_SECRET_KEY);
|
|
4458
|
+
const protocols = options.protocols ? [...options.protocols] : mppEnabled ? ["x402", "mpp"] : ["x402"];
|
|
4459
|
+
const mppConfig = mppEnabled ? {
|
|
4460
|
+
secretKey: env.MPP_SECRET_KEY,
|
|
4461
|
+
currency: canonicalizeEvm(env.MPP_CURRENCY),
|
|
4462
|
+
rpcUrl: env.TEMPO_RPC_URL,
|
|
4463
|
+
recipient: payeeAddress,
|
|
4464
|
+
...env.MPP_FEE_PAYER_KEY ? { feePayerKey: env.MPP_FEE_PAYER_KEY } : {},
|
|
4465
|
+
...env.MPP_OPERATOR_KEY ? { operatorKey: env.MPP_OPERATOR_KEY, session: {} } : {}
|
|
4466
|
+
} : void 0;
|
|
4229
4467
|
return {
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4468
|
+
payeeAddress,
|
|
4469
|
+
baseUrl: env.BASE_URL,
|
|
4470
|
+
network: BASE_MAINNET_NETWORK,
|
|
4471
|
+
protocols,
|
|
4472
|
+
x402: {
|
|
4473
|
+
accepts,
|
|
4474
|
+
facilitators: {
|
|
4475
|
+
...options.x402Facilitators,
|
|
4476
|
+
solana: solanaFacilitator
|
|
4477
|
+
}
|
|
4478
|
+
},
|
|
4479
|
+
...mppConfig ? { mpp: mppConfig } : {},
|
|
4480
|
+
discovery: {
|
|
4481
|
+
title: options.title,
|
|
4482
|
+
version: options.version ?? "1.0.0",
|
|
4483
|
+
description: options.description,
|
|
4484
|
+
guidance: options.guidance,
|
|
4485
|
+
...options.contact ? { contact: options.contact } : {},
|
|
4486
|
+
...options.ownershipProofs ? { ownershipProofs: options.ownershipProofs } : {},
|
|
4487
|
+
...options.methodHints ? { methodHints: options.methodHints } : {},
|
|
4488
|
+
...options.serverUrl ? { serverUrl: options.serverUrl } : {}
|
|
4489
|
+
},
|
|
4490
|
+
...options.prices ? { prices: options.prices } : {},
|
|
4491
|
+
...options.plugin ? { plugin: options.plugin } : {},
|
|
4492
|
+
...options.kvStore ? { kvStore: options.kvStore } : {},
|
|
4493
|
+
strictRoutes: options.strictRoutes ?? false
|
|
4233
4494
|
};
|
|
4234
4495
|
}
|
|
4235
|
-
|
|
4236
|
-
// src/config/validate.ts
|
|
4237
|
-
function validateRouterConfig(config, options = {}) {
|
|
4238
|
-
const issues = getRouterConfigIssues(config, options);
|
|
4239
|
-
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
4240
|
-
}
|
|
4241
4496
|
function getRouterConfigIssues(config, options = {}) {
|
|
4242
|
-
const env = options.env ??
|
|
4243
|
-
const issues = [];
|
|
4497
|
+
const env = options.env ?? {};
|
|
4244
4498
|
const protocols = config.protocols ?? ["x402"];
|
|
4499
|
+
const issues = [];
|
|
4245
4500
|
if (!config.baseUrl) {
|
|
4246
4501
|
issues.push({
|
|
4247
4502
|
code: "missing_base_url",
|
|
@@ -4254,84 +4509,17 @@ function getRouterConfigIssues(config, options = {}) {
|
|
|
4254
4509
|
message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
4255
4510
|
});
|
|
4256
4511
|
}
|
|
4257
|
-
if (protocols.includes("x402"))
|
|
4258
|
-
|
|
4259
|
-
}
|
|
4260
|
-
if (protocols.includes("mpp")) {
|
|
4261
|
-
issues.push(...validateMppConfig(config, env));
|
|
4262
|
-
}
|
|
4512
|
+
if (protocols.includes("x402")) issues.push(...validateX402Config(config, env, options));
|
|
4513
|
+
if (protocols.includes("mpp")) issues.push(...validateMppConfig(config));
|
|
4263
4514
|
return issues;
|
|
4264
4515
|
}
|
|
4265
4516
|
|
|
4266
|
-
// src/config/env.ts
|
|
4267
|
-
init_constants();
|
|
4268
|
-
function mppFromEnv(env, options = {}) {
|
|
4269
|
-
const secretKey = env.MPP_SECRET_KEY;
|
|
4270
|
-
const currency = env.MPP_CURRENCY;
|
|
4271
|
-
const rpcUrl = env.TEMPO_RPC_URL;
|
|
4272
|
-
const feePayerKey = options.feePayerKey ?? env.MPP_FEE_PAYER_KEY;
|
|
4273
|
-
const feePayerKeySource = options.feePayerKey !== void 0 ? "feePayerKey" : "MPP_FEE_PAYER_KEY";
|
|
4274
|
-
const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
|
|
4275
|
-
if (!hasAnyMppEnv) return void 0;
|
|
4276
|
-
const missing = [
|
|
4277
|
-
secretKey ? null : "MPP_SECRET_KEY",
|
|
4278
|
-
currency ? null : "MPP_CURRENCY",
|
|
4279
|
-
rpcUrl ? null : "TEMPO_RPC_URL"
|
|
4280
|
-
].filter(Boolean);
|
|
4281
|
-
if (missing.length > 0) {
|
|
4282
|
-
throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
|
|
4283
|
-
}
|
|
4284
|
-
if (!isEvmAddress(currency)) {
|
|
4285
|
-
throw new Error("MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address");
|
|
4286
|
-
}
|
|
4287
|
-
if (options.recipient && !isEvmAddress(options.recipient)) {
|
|
4288
|
-
throw new Error("MPP recipient must be a 0x-prefixed EVM address");
|
|
4289
|
-
}
|
|
4290
|
-
if (feePayerKey && !isEvmPrivateKey(feePayerKey)) {
|
|
4291
|
-
throw new Error(`${feePayerKeySource} must be a 0x-prefixed 32-byte EVM private key`);
|
|
4292
|
-
}
|
|
4293
|
-
return {
|
|
4294
|
-
secretKey,
|
|
4295
|
-
currency,
|
|
4296
|
-
rpcUrl,
|
|
4297
|
-
...options.recipient ? { recipient: options.recipient } : {},
|
|
4298
|
-
...feePayerKey ? { feePayerKey } : {}
|
|
4299
|
-
};
|
|
4300
|
-
}
|
|
4301
|
-
function x402AcceptsFromEnv(env, options = {}) {
|
|
4302
|
-
const payeeEnv = options.payeeEnv ?? "X402_WALLET_ADDRESS";
|
|
4303
|
-
const solanaPayeeEnv = options.solanaPayeeEnv ?? "SOLANA_PAYEE_ADDRESS";
|
|
4304
|
-
const payeeAddress = options.payeeAddress ?? env[payeeEnv];
|
|
4305
|
-
if (!payeeAddress) {
|
|
4306
|
-
throw new Error(`${payeeEnv} is required to build x402 accepts`);
|
|
4307
|
-
}
|
|
4308
|
-
const accepts = [
|
|
4309
|
-
{
|
|
4310
|
-
scheme: "exact",
|
|
4311
|
-
network: options.network ?? BASE_NETWORK,
|
|
4312
|
-
payTo: payeeAddress
|
|
4313
|
-
}
|
|
4314
|
-
];
|
|
4315
|
-
const solanaPayeeAddress = options.solanaPayeeAddress ?? env[solanaPayeeEnv];
|
|
4316
|
-
if (solanaPayeeAddress) {
|
|
4317
|
-
accepts.push({
|
|
4318
|
-
scheme: "exact",
|
|
4319
|
-
network: SOLANA_MAINNET_NETWORK,
|
|
4320
|
-
payTo: solanaPayeeAddress
|
|
4321
|
-
});
|
|
4322
|
-
}
|
|
4323
|
-
return accepts;
|
|
4324
|
-
}
|
|
4325
|
-
function paidOptionsForProtocols(protocols) {
|
|
4326
|
-
return { protocols: [...protocols] };
|
|
4327
|
-
}
|
|
4328
|
-
|
|
4329
4517
|
// src/init/x402.ts
|
|
4330
|
-
async function initX402(config, configError) {
|
|
4518
|
+
async function initX402(config, kvStore, configError) {
|
|
4331
4519
|
if (configError) return { initError: configError };
|
|
4332
4520
|
try {
|
|
4333
|
-
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (
|
|
4334
|
-
const result = await createX402Server2(config);
|
|
4521
|
+
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_x402_server(), x402_server_exports));
|
|
4522
|
+
const result = await createX402Server2(config, kvStore);
|
|
4335
4523
|
await result.initPromise;
|
|
4336
4524
|
return {
|
|
4337
4525
|
server: result.server,
|
|
@@ -4342,7 +4530,7 @@ async function initX402(config, configError) {
|
|
|
4342
4530
|
}
|
|
4343
4531
|
}
|
|
4344
4532
|
|
|
4345
|
-
// src/mppx
|
|
4533
|
+
// src/init/mppx.ts
|
|
4346
4534
|
function getMppxRequestContext(args) {
|
|
4347
4535
|
const {
|
|
4348
4536
|
Mppx,
|
|
@@ -4464,9 +4652,10 @@ function createRouter(config) {
|
|
|
4464
4652
|
const kvStore = resolveKvStore(config.kvStore);
|
|
4465
4653
|
const nonceStore = kvStore ? createKvNonceStore(kvStore) : new MemoryNonceStore();
|
|
4466
4654
|
const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
|
|
4467
|
-
const network = config.network ??
|
|
4655
|
+
const network = config.network ?? BASE_MAINNET_NETWORK;
|
|
4468
4656
|
const x402Accepts = getConfiguredX402Accepts(config);
|
|
4469
4657
|
const configIssues = getRouterConfigIssues(config, {
|
|
4658
|
+
env: process.env,
|
|
4470
4659
|
requireCdpKeys: process.env.NODE_ENV === "production"
|
|
4471
4660
|
});
|
|
4472
4661
|
const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
|
|
@@ -4513,7 +4702,7 @@ function createRouter(config) {
|
|
|
4513
4702
|
mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
|
|
4514
4703
|
};
|
|
4515
4704
|
deps.initPromise = (async () => {
|
|
4516
|
-
const x402Result = await initX402(config, x402ConfigError);
|
|
4705
|
+
const x402Result = await initX402(config, kvStore, x402ConfigError);
|
|
4517
4706
|
deps.x402Server = x402Result.server ?? null;
|
|
4518
4707
|
deps.x402FacilitatorsByNetwork = x402Result.facilitatorsByNetwork;
|
|
4519
4708
|
if (x402Result.initError) deps.x402InitError = x402Result.initError;
|
|
@@ -4542,11 +4731,10 @@ function createRouter(config) {
|
|
|
4542
4731
|
`[router] strictRoutes=true forbids key/path divergence for route '${definition.path}'. Remove custom \`key\` or make it equal to \`path\`.`
|
|
4543
4732
|
);
|
|
4544
4733
|
}
|
|
4545
|
-
let builder = new RouteBuilder(key, registry, deps
|
|
4734
|
+
let builder = new RouteBuilder(key, registry, deps, {
|
|
4735
|
+
protocols: config.protocols
|
|
4736
|
+
});
|
|
4546
4737
|
builder = builder.path(normalizedPath);
|
|
4547
|
-
if (config.protocols) {
|
|
4548
|
-
builder._protocols = [...config.protocols];
|
|
4549
|
-
}
|
|
4550
4738
|
if (definition.method) {
|
|
4551
4739
|
builder = builder.method(definition.method);
|
|
4552
4740
|
}
|
|
@@ -4589,29 +4777,21 @@ function normalizePath(path) {
|
|
|
4589
4777
|
normalized = normalized.replace(/^api\/+/, "");
|
|
4590
4778
|
return normalized.replace(/\/+$/, "");
|
|
4591
4779
|
}
|
|
4780
|
+
function createRouterFromEnv(options) {
|
|
4781
|
+
return createRouter(routerConfigFromEnv(options));
|
|
4782
|
+
}
|
|
4592
4783
|
export {
|
|
4593
|
-
|
|
4784
|
+
BASE_MAINNET_NETWORK,
|
|
4785
|
+
BASE_USDC_ADDRESS,
|
|
4786
|
+
BASE_USDC_DECIMALS,
|
|
4787
|
+
DEFAULT_SOLANA_FACILITATOR_URL,
|
|
4594
4788
|
HttpError,
|
|
4595
|
-
MemoryEntitlementStore,
|
|
4596
|
-
MemoryNonceStore,
|
|
4597
|
-
RouteBuilder,
|
|
4598
|
-
RouteRegistry,
|
|
4599
4789
|
RouterConfigError,
|
|
4600
|
-
SIWX_CHALLENGE_EXPIRY_MS,
|
|
4601
|
-
SIWX_ERROR_MESSAGES,
|
|
4602
4790
|
SOLANA_MAINNET_NETWORK,
|
|
4603
|
-
|
|
4791
|
+
TEMPO_USDC_ADDRESS,
|
|
4792
|
+
TEMPO_USDC_DECIMALS,
|
|
4604
4793
|
ZERO_EVM_ADDRESS,
|
|
4605
|
-
consolePlugin,
|
|
4606
|
-
createKvEntitlementStore,
|
|
4607
|
-
createKvMppStore,
|
|
4608
|
-
createKvNonceStore,
|
|
4609
4794
|
createRouter,
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
mppFromEnv,
|
|
4613
|
-
paidOptionsForProtocols,
|
|
4614
|
-
validateRouterConfig,
|
|
4615
|
-
withPrefix,
|
|
4616
|
-
x402AcceptsFromEnv
|
|
4795
|
+
createRouterFromEnv,
|
|
4796
|
+
routerConfigFromEnv
|
|
4617
4797
|
};
|