@agentcash/router 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -2
- package/dist/index.cjs +343 -56
- package/dist/index.d.cts +51 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.js +331 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,51 @@ export const env = createEnv({
|
|
|
46
46
|
});
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Without these keys, x402 routes
|
|
49
|
+
Without these keys, x402 routes that use the default EVM facilitator will fail to initialize.
|
|
50
|
+
Run `validateRouterConfig(config)` during startup if you want missing facilitator, MPP, or store
|
|
51
|
+
configuration to throw immediately in every environment.
|
|
52
|
+
|
|
53
|
+
### Recommended strict setup
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import {
|
|
57
|
+
createRouter,
|
|
58
|
+
mppFromEnv,
|
|
59
|
+
validateRouterConfig,
|
|
60
|
+
x402AcceptsFromEnv,
|
|
61
|
+
type ProtocolType,
|
|
62
|
+
} from '@agentcash/router';
|
|
63
|
+
|
|
64
|
+
const accepts = x402AcceptsFromEnv(process.env);
|
|
65
|
+
const protocols: ProtocolType[] = process.env.MPP_SECRET_KEY ? ['x402', 'mpp'] : ['x402'];
|
|
66
|
+
|
|
67
|
+
const config = {
|
|
68
|
+
payeeAddress: process.env.X402_WALLET_ADDRESS!,
|
|
69
|
+
baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
|
|
70
|
+
strictRoutes: true,
|
|
71
|
+
protocols,
|
|
72
|
+
x402: { accepts },
|
|
73
|
+
mpp: mppFromEnv(process.env, {
|
|
74
|
+
recipient: process.env.X402_WALLET_ADDRESS,
|
|
75
|
+
useDefaultStore: true,
|
|
76
|
+
}),
|
|
77
|
+
discovery: {
|
|
78
|
+
title: 'My API',
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
validateRouterConfig(config);
|
|
84
|
+
export const router = createRouter(config);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`x402AcceptsFromEnv()` always adds Base (`BASE_NETWORK`) and also adds Solana
|
|
88
|
+
mainnet (`SOLANA_MAINNET_NETWORK`) when `SOLANA_PAYEE_ADDRESS` is set. Solana
|
|
89
|
+
addresses are case-sensitive and are preserved as-is.
|
|
90
|
+
|
|
91
|
+
`mppFromEnv()` returns `undefined` when no MPP env vars are present. If any MPP
|
|
92
|
+
env var is present, the full trio is required: `MPP_SECRET_KEY`, `MPP_CURRENCY`,
|
|
93
|
+
and `TEMPO_RPC_URL`.
|
|
50
94
|
|
|
51
95
|
## Quick Start
|
|
52
96
|
|
|
@@ -143,9 +187,36 @@ Creates a `ServiceRouter` instance.
|
|
|
143
187
|
| `plugin` | `RouterPlugin` | `undefined` | Observability plugin |
|
|
144
188
|
| `prices` | `Record<string, string>` | `undefined` | Central pricing map (auto-applied) |
|
|
145
189
|
| `siwx.nonceStore` | `NonceStore` | `MemoryNonceStore` | Custom nonce store |
|
|
146
|
-
| `mpp` | `{ secretKey, currency, recipient? }` | `undefined` | MPP config |
|
|
190
|
+
| `mpp` | `{ secretKey, currency, recipient?, rpcUrl?, useDefaultStore? }` | `undefined` | MPP config |
|
|
191
|
+
| `protocols` | `('x402' \| 'mpp')[]` | `['x402']` | Default protocols for auto-priced routes |
|
|
147
192
|
| `strictRoutes` | `boolean` | `false` | Enforce `route({ path })` and prevent key/path divergence |
|
|
148
193
|
|
|
194
|
+
### Config validation helpers
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import {
|
|
198
|
+
BASE_NETWORK,
|
|
199
|
+
SOLANA_MAINNET_NETWORK,
|
|
200
|
+
TEMPO_USDC_CURRENCY,
|
|
201
|
+
getRouterConfigIssues,
|
|
202
|
+
mppFromEnv,
|
|
203
|
+
paidOptionsForProtocols,
|
|
204
|
+
validateRouterConfig,
|
|
205
|
+
x402AcceptsFromEnv,
|
|
206
|
+
} from '@agentcash/router';
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- `validateRouterConfig(config)` throws `RouterConfigError` with structured
|
|
210
|
+
issues. Use it when you want invalid env/config to fail at startup.
|
|
211
|
+
- `getRouterConfigIssues(config)` returns the same structured issues without
|
|
212
|
+
throwing.
|
|
213
|
+
- `x402AcceptsFromEnv(env)` builds Base and optional Solana x402 accepts from
|
|
214
|
+
`X402_WALLET_ADDRESS` and `SOLANA_PAYEE_ADDRESS`.
|
|
215
|
+
- `mppFromEnv(env)` builds MPP config only when MPP env is present, and rejects
|
|
216
|
+
partial MPP env.
|
|
217
|
+
- `paidOptionsForProtocols(protocols)` copies a protocol array into a
|
|
218
|
+
route-level `PaidOptions` object.
|
|
219
|
+
|
|
149
220
|
### Path-First Routing
|
|
150
221
|
|
|
151
222
|
Use path-first route definitions to keep runtime, OpenAPI, and discovery aligned:
|
|
@@ -259,12 +330,19 @@ interface HandlerContext<TBody, TQuery> {
|
|
|
259
330
|
query: TQuery; // Parsed + validated
|
|
260
331
|
request: NextRequest; // Raw request
|
|
261
332
|
wallet: string | null; // Verified wallet address
|
|
333
|
+
payment: HandlerPaymentContext | null; // Payment metadata for this request
|
|
262
334
|
account: unknown; // From .apiKey() resolver
|
|
263
335
|
alert: AlertFn; // Fire observability alerts
|
|
264
336
|
setVerifiedWallet: (addr: string) => void;
|
|
265
337
|
}
|
|
266
338
|
```
|
|
267
339
|
|
|
340
|
+
`payment` is `null` for unprotected, API-key-only, and SIWX-only requests. For
|
|
341
|
+
paid requests it includes `protocol`, `status`, `payer`, `amount`, `network`,
|
|
342
|
+
and best-effort recipient/transaction/receipt metadata when the protocol
|
|
343
|
+
provides it. x402 handlers currently see `status: 'verified'` because settlement
|
|
344
|
+
happens after a successful handler response.
|
|
345
|
+
|
|
268
346
|
### RouterPlugin
|
|
269
347
|
|
|
270
348
|
Pluggable observability. All hooks are optional and fire-and-forget.
|
package/dist/index.cjs
CHANGED
|
@@ -188,6 +188,18 @@ var init_x402_facilitators = __esm({
|
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
// src/constants.ts
|
|
192
|
+
var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
|
|
193
|
+
var init_constants = __esm({
|
|
194
|
+
"src/constants.ts"() {
|
|
195
|
+
"use strict";
|
|
196
|
+
BASE_NETWORK = "eip155:8453";
|
|
197
|
+
SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
198
|
+
TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
199
|
+
ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
191
203
|
// src/x402-config.ts
|
|
192
204
|
async function resolvePayToValue(payTo, request, fallback, body) {
|
|
193
205
|
if (!payTo) return fallback;
|
|
@@ -201,7 +213,7 @@ function getConfiguredX402Accepts(config) {
|
|
|
201
213
|
return [
|
|
202
214
|
{
|
|
203
215
|
scheme: "exact",
|
|
204
|
-
network: config.network ??
|
|
216
|
+
network: config.network ?? BASE_NETWORK,
|
|
205
217
|
payTo: config.payeeAddress
|
|
206
218
|
}
|
|
207
219
|
];
|
|
@@ -230,6 +242,7 @@ async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, b
|
|
|
230
242
|
var init_x402_config = __esm({
|
|
231
243
|
"src/x402-config.ts"() {
|
|
232
244
|
"use strict";
|
|
245
|
+
init_constants();
|
|
233
246
|
}
|
|
234
247
|
});
|
|
235
248
|
|
|
@@ -376,17 +389,28 @@ var init_upstash_rest = __esm({
|
|
|
376
389
|
// src/index.ts
|
|
377
390
|
var index_exports = {};
|
|
378
391
|
__export(index_exports, {
|
|
392
|
+
BASE_NETWORK: () => BASE_NETWORK,
|
|
379
393
|
HttpError: () => HttpError,
|
|
380
394
|
MemoryEntitlementStore: () => MemoryEntitlementStore,
|
|
381
395
|
MemoryNonceStore: () => MemoryNonceStore,
|
|
382
396
|
RouteBuilder: () => RouteBuilder,
|
|
383
397
|
RouteRegistry: () => RouteRegistry,
|
|
398
|
+
RouterConfigError: () => RouterConfigError,
|
|
384
399
|
SIWX_CHALLENGE_EXPIRY_MS: () => SIWX_CHALLENGE_EXPIRY_MS,
|
|
385
400
|
SIWX_ERROR_MESSAGES: () => SIWX_ERROR_MESSAGES,
|
|
401
|
+
SOLANA_MAINNET_NETWORK: () => SOLANA_MAINNET_NETWORK,
|
|
402
|
+
TEMPO_USDC_CURRENCY: () => TEMPO_USDC_CURRENCY,
|
|
403
|
+
ZERO_EVM_ADDRESS: () => ZERO_EVM_ADDRESS,
|
|
386
404
|
consolePlugin: () => consolePlugin,
|
|
387
405
|
createRedisEntitlementStore: () => createRedisEntitlementStore,
|
|
388
406
|
createRedisNonceStore: () => createRedisNonceStore,
|
|
389
|
-
createRouter: () => createRouter
|
|
407
|
+
createRouter: () => createRouter,
|
|
408
|
+
formatRouterConfigIssues: () => formatRouterConfigIssues,
|
|
409
|
+
getRouterConfigIssues: () => getRouterConfigIssues,
|
|
410
|
+
mppFromEnv: () => mppFromEnv,
|
|
411
|
+
paidOptionsForProtocols: () => paidOptionsForProtocols,
|
|
412
|
+
validateRouterConfig: () => validateRouterConfig,
|
|
413
|
+
x402AcceptsFromEnv: () => x402AcceptsFromEnv
|
|
390
414
|
});
|
|
391
415
|
module.exports = __toCommonJS(index_exports);
|
|
392
416
|
|
|
@@ -1029,6 +1053,10 @@ function getRequirementNetwork(requirements, fallback) {
|
|
|
1029
1053
|
const network = requirements?.network;
|
|
1030
1054
|
return typeof network === "string" ? network : fallback;
|
|
1031
1055
|
}
|
|
1056
|
+
function getRequirementRecipient(requirements) {
|
|
1057
|
+
const payTo = requirements?.payTo;
|
|
1058
|
+
return typeof payTo === "string" ? payTo : void 0;
|
|
1059
|
+
}
|
|
1032
1060
|
function siwxSignatureType(network) {
|
|
1033
1061
|
return network.startsWith("solana:") ? "ed25519" : "eip191";
|
|
1034
1062
|
}
|
|
@@ -1047,7 +1075,7 @@ function getSupportedChains(x402Accepts, fallbackNetwork) {
|
|
|
1047
1075
|
return chains;
|
|
1048
1076
|
}
|
|
1049
1077
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
1050
|
-
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
|
|
1078
|
+
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody, payment) {
|
|
1051
1079
|
const ctx = {
|
|
1052
1080
|
body: parsedBody,
|
|
1053
1081
|
query: parseQuery(request, routeEntry),
|
|
@@ -1055,6 +1083,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1055
1083
|
requestId: meta.requestId,
|
|
1056
1084
|
route: routeEntry.key,
|
|
1057
1085
|
wallet,
|
|
1086
|
+
payment,
|
|
1058
1087
|
account,
|
|
1059
1088
|
alert(level, message, alertMeta) {
|
|
1060
1089
|
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
@@ -1107,7 +1136,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1107
1136
|
pluginCtx,
|
|
1108
1137
|
wallet,
|
|
1109
1138
|
account2,
|
|
1110
|
-
body2.data
|
|
1139
|
+
body2.data,
|
|
1140
|
+
null
|
|
1111
1141
|
);
|
|
1112
1142
|
finalize(response, rawResult, meta, pluginCtx, body2.data);
|
|
1113
1143
|
return response;
|
|
@@ -1384,7 +1414,16 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1384
1414
|
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1385
1415
|
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
1386
1416
|
const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
|
|
1417
|
+
const matchedRecipient = getRequirementRecipient(verifyRequirements);
|
|
1387
1418
|
const wallet = normalizeWalletAddress(verify.payer);
|
|
1419
|
+
const payment = {
|
|
1420
|
+
protocol: "x402",
|
|
1421
|
+
status: "verified",
|
|
1422
|
+
payer: wallet,
|
|
1423
|
+
amount: price,
|
|
1424
|
+
network: matchedNetwork,
|
|
1425
|
+
...matchedRecipient ? { recipient: matchedRecipient } : {}
|
|
1426
|
+
};
|
|
1388
1427
|
pluginCtx.setVerifiedWallet(wallet);
|
|
1389
1428
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1390
1429
|
protocol: "x402",
|
|
@@ -1398,7 +1437,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1398
1437
|
pluginCtx,
|
|
1399
1438
|
wallet,
|
|
1400
1439
|
account,
|
|
1401
|
-
body.data
|
|
1440
|
+
body.data,
|
|
1441
|
+
payment
|
|
1402
1442
|
);
|
|
1403
1443
|
if (response.status < 400) {
|
|
1404
1444
|
try {
|
|
@@ -1497,7 +1537,15 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1497
1537
|
pluginCtx,
|
|
1498
1538
|
wallet,
|
|
1499
1539
|
account,
|
|
1500
|
-
body.data
|
|
1540
|
+
body.data,
|
|
1541
|
+
{
|
|
1542
|
+
protocol: "mpp",
|
|
1543
|
+
status: "verified",
|
|
1544
|
+
payer: wallet,
|
|
1545
|
+
amount: price,
|
|
1546
|
+
network: "tempo:4217",
|
|
1547
|
+
recipient: deps.payeeAddress
|
|
1548
|
+
}
|
|
1501
1549
|
);
|
|
1502
1550
|
if (response2.status < 400) {
|
|
1503
1551
|
let mppResult2;
|
|
@@ -1630,7 +1678,17 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1630
1678
|
pluginCtx,
|
|
1631
1679
|
wallet,
|
|
1632
1680
|
account,
|
|
1633
|
-
body.data
|
|
1681
|
+
body.data,
|
|
1682
|
+
{
|
|
1683
|
+
protocol: "mpp",
|
|
1684
|
+
status: "settled",
|
|
1685
|
+
payer: wallet,
|
|
1686
|
+
amount: price,
|
|
1687
|
+
network: "tempo:4217",
|
|
1688
|
+
recipient: deps.payeeAddress,
|
|
1689
|
+
...txHash ? { transaction: txHash } : {},
|
|
1690
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1691
|
+
}
|
|
1634
1692
|
);
|
|
1635
1693
|
if (response.status < 400) {
|
|
1636
1694
|
if (routeEntry.siwxEnabled) {
|
|
@@ -2590,61 +2648,282 @@ function createLlmsTxtHandler(discovery) {
|
|
|
2590
2648
|
|
|
2591
2649
|
// src/index.ts
|
|
2592
2650
|
init_x402_config();
|
|
2651
|
+
init_constants();
|
|
2652
|
+
|
|
2653
|
+
// src/config.ts
|
|
2654
|
+
init_constants();
|
|
2593
2655
|
init_evm();
|
|
2594
2656
|
init_solana();
|
|
2657
|
+
init_x402_config();
|
|
2658
|
+
var RouterConfigError = class extends Error {
|
|
2659
|
+
issues;
|
|
2660
|
+
constructor(issues) {
|
|
2661
|
+
super(formatRouterConfigIssues(issues));
|
|
2662
|
+
this.name = "RouterConfigError";
|
|
2663
|
+
this.issues = issues;
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
function validateRouterConfig(config, options = {}) {
|
|
2667
|
+
const issues = getRouterConfigIssues(config, options);
|
|
2668
|
+
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
2669
|
+
}
|
|
2670
|
+
function getRouterConfigIssues(config, options = {}) {
|
|
2671
|
+
const env = options.env ?? process.env;
|
|
2672
|
+
const issues = [];
|
|
2673
|
+
const protocols = config.protocols ?? ["x402"];
|
|
2674
|
+
if (!config.baseUrl) {
|
|
2675
|
+
issues.push({
|
|
2676
|
+
code: "missing_base_url",
|
|
2677
|
+
message: '[router] baseUrl is required in RouterConfig. Set it to your production domain (e.g., "https://api.example.com"). The realm is used for payment matching and must be correct.'
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
if (config.protocols && config.protocols.length === 0) {
|
|
2681
|
+
issues.push({
|
|
2682
|
+
code: "empty_protocols",
|
|
2683
|
+
message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
if (protocols.includes("x402")) {
|
|
2687
|
+
issues.push(...validateX402Config(config, env, options));
|
|
2688
|
+
}
|
|
2689
|
+
if (protocols.includes("mpp")) {
|
|
2690
|
+
issues.push(...validateMppConfig(config, env));
|
|
2691
|
+
}
|
|
2692
|
+
return issues;
|
|
2693
|
+
}
|
|
2694
|
+
function formatRouterConfigIssues(issues) {
|
|
2695
|
+
return issues.map((issue) => issue.message).join("\n");
|
|
2696
|
+
}
|
|
2697
|
+
function mppFromEnv(env, options = {}) {
|
|
2698
|
+
const secretKey = env.MPP_SECRET_KEY;
|
|
2699
|
+
const currency = env.MPP_CURRENCY;
|
|
2700
|
+
const rpcUrl = env.TEMPO_RPC_URL;
|
|
2701
|
+
const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
|
|
2702
|
+
if (!hasAnyMppEnv) return void 0;
|
|
2703
|
+
const missing = [
|
|
2704
|
+
secretKey ? null : "MPP_SECRET_KEY",
|
|
2705
|
+
currency ? null : "MPP_CURRENCY",
|
|
2706
|
+
rpcUrl ? null : "TEMPO_RPC_URL"
|
|
2707
|
+
].filter(Boolean);
|
|
2708
|
+
if (missing.length > 0) {
|
|
2709
|
+
throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
|
|
2710
|
+
}
|
|
2711
|
+
return {
|
|
2712
|
+
secretKey,
|
|
2713
|
+
currency,
|
|
2714
|
+
rpcUrl,
|
|
2715
|
+
...options.recipient ? { recipient: options.recipient } : {},
|
|
2716
|
+
...options.feePayerKey ? { feePayerKey: options.feePayerKey } : {},
|
|
2717
|
+
...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
function x402AcceptsFromEnv(env, options = {}) {
|
|
2721
|
+
const payeeEnv = options.payeeEnv ?? "X402_WALLET_ADDRESS";
|
|
2722
|
+
const solanaPayeeEnv = options.solanaPayeeEnv ?? "SOLANA_PAYEE_ADDRESS";
|
|
2723
|
+
const payeeAddress = options.payeeAddress ?? env[payeeEnv];
|
|
2724
|
+
if (!payeeAddress) {
|
|
2725
|
+
throw new Error(`${payeeEnv} is required to build x402 accepts`);
|
|
2726
|
+
}
|
|
2727
|
+
const accepts = [
|
|
2728
|
+
{
|
|
2729
|
+
scheme: "exact",
|
|
2730
|
+
network: options.network ?? BASE_NETWORK,
|
|
2731
|
+
payTo: payeeAddress
|
|
2732
|
+
}
|
|
2733
|
+
];
|
|
2734
|
+
const solanaPayeeAddress = options.solanaPayeeAddress ?? env[solanaPayeeEnv];
|
|
2735
|
+
if (solanaPayeeAddress) {
|
|
2736
|
+
accepts.push({
|
|
2737
|
+
scheme: "exact",
|
|
2738
|
+
network: SOLANA_MAINNET_NETWORK,
|
|
2739
|
+
payTo: solanaPayeeAddress
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
return accepts;
|
|
2743
|
+
}
|
|
2744
|
+
function paidOptionsForProtocols(protocols) {
|
|
2745
|
+
return { protocols: [...protocols] };
|
|
2746
|
+
}
|
|
2747
|
+
function validateX402Config(config, env, options) {
|
|
2748
|
+
const issues = [];
|
|
2749
|
+
const accepts = getConfiguredX402Accepts(config);
|
|
2750
|
+
if (accepts.length === 0) {
|
|
2751
|
+
issues.push({
|
|
2752
|
+
code: "missing_x402_accepts",
|
|
2753
|
+
protocol: "x402",
|
|
2754
|
+
message: "x402 requires at least one accept configuration."
|
|
2755
|
+
});
|
|
2756
|
+
return issues;
|
|
2757
|
+
}
|
|
2758
|
+
const acceptWithoutNetwork = accepts.find((accept) => !accept.network);
|
|
2759
|
+
if (acceptWithoutNetwork) {
|
|
2760
|
+
issues.push({
|
|
2761
|
+
code: "missing_x402_network",
|
|
2762
|
+
protocol: "x402",
|
|
2763
|
+
message: "x402 accepts require a network."
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
const unsupported = accepts.find(
|
|
2767
|
+
(accept) => accept.network && !isSupportedX402Network(accept.network)
|
|
2768
|
+
);
|
|
2769
|
+
if (unsupported) {
|
|
2770
|
+
issues.push({
|
|
2771
|
+
code: "unsupported_x402_network",
|
|
2772
|
+
protocol: "x402",
|
|
2773
|
+
message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
const missingAsset = accepts.find(
|
|
2777
|
+
(accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
|
|
2778
|
+
);
|
|
2779
|
+
if (missingAsset) {
|
|
2780
|
+
issues.push({
|
|
2781
|
+
code: "missing_x402_asset",
|
|
2782
|
+
protocol: "x402",
|
|
2783
|
+
message: "non-exact x402 accepts require an asset."
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
const invalidDecimals = accepts.find(
|
|
2787
|
+
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2788
|
+
);
|
|
2789
|
+
if (invalidDecimals) {
|
|
2790
|
+
issues.push({
|
|
2791
|
+
code: "invalid_x402_decimals",
|
|
2792
|
+
protocol: "x402",
|
|
2793
|
+
message: "x402 accept decimals must be a non-negative integer."
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2796
|
+
if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2797
|
+
issues.push({
|
|
2798
|
+
code: "missing_x402_payee",
|
|
2799
|
+
protocol: "x402",
|
|
2800
|
+
message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
const placeholder = findPlaceholderPayee([
|
|
2804
|
+
config.payeeAddress,
|
|
2805
|
+
...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
|
|
2806
|
+
]);
|
|
2807
|
+
if (placeholder) {
|
|
2808
|
+
issues.push({
|
|
2809
|
+
code: "placeholder_payee",
|
|
2810
|
+
protocol: "x402",
|
|
2811
|
+
message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
if (options.requireCdpKeys !== false && usesDefaultEvmFacilitator(config)) {
|
|
2815
|
+
const missing = [
|
|
2816
|
+
env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
|
|
2817
|
+
env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
|
|
2818
|
+
].filter(Boolean);
|
|
2819
|
+
if (missing.length > 0) {
|
|
2820
|
+
issues.push({
|
|
2821
|
+
code: "missing_cdp_keys",
|
|
2822
|
+
protocol: "x402",
|
|
2823
|
+
message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
return issues;
|
|
2828
|
+
}
|
|
2829
|
+
function validateMppConfig(config, env) {
|
|
2830
|
+
const issues = [];
|
|
2831
|
+
const mpp = config.mpp;
|
|
2832
|
+
if (!mpp) {
|
|
2833
|
+
return [
|
|
2834
|
+
{
|
|
2835
|
+
code: "missing_mpp_config",
|
|
2836
|
+
protocol: "mpp",
|
|
2837
|
+
message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
2838
|
+
}
|
|
2839
|
+
];
|
|
2840
|
+
}
|
|
2841
|
+
if (!mpp.secretKey) {
|
|
2842
|
+
issues.push({
|
|
2843
|
+
code: "missing_mpp_secret_key",
|
|
2844
|
+
protocol: "mpp",
|
|
2845
|
+
message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
if (!mpp.currency) {
|
|
2849
|
+
issues.push({
|
|
2850
|
+
code: "missing_mpp_currency",
|
|
2851
|
+
protocol: "mpp",
|
|
2852
|
+
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
if (!mpp.recipient && !config.payeeAddress) {
|
|
2856
|
+
issues.push({
|
|
2857
|
+
code: "missing_mpp_recipient",
|
|
2858
|
+
protocol: "mpp",
|
|
2859
|
+
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
2863
|
+
if (placeholder) {
|
|
2864
|
+
issues.push({
|
|
2865
|
+
code: "placeholder_payee",
|
|
2866
|
+
protocol: "mpp",
|
|
2867
|
+
message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
|
|
2871
|
+
issues.push({
|
|
2872
|
+
code: "missing_mpp_rpc_url",
|
|
2873
|
+
protocol: "mpp",
|
|
2874
|
+
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
|
|
2878
|
+
issues.push({
|
|
2879
|
+
code: "missing_mpp_default_store_env",
|
|
2880
|
+
protocol: "mpp",
|
|
2881
|
+
message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
return issues;
|
|
2885
|
+
}
|
|
2886
|
+
function usesDefaultEvmFacilitator(config) {
|
|
2887
|
+
return getConfiguredX402Networks(config).some(
|
|
2888
|
+
(network) => typeof network === "string" && isEvmNetwork(network)
|
|
2889
|
+
) && config.x402?.facilitators?.evm === void 0;
|
|
2890
|
+
}
|
|
2891
|
+
function isSupportedX402Network(network) {
|
|
2892
|
+
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2893
|
+
}
|
|
2894
|
+
function findPlaceholderPayee(values) {
|
|
2895
|
+
return values.find((value) => value?.toLowerCase() === ZERO_EVM_ADDRESS) ?? null;
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// src/index.ts
|
|
2899
|
+
init_constants();
|
|
2595
2900
|
function createRouter(config) {
|
|
2596
2901
|
const registry = new RouteRegistry();
|
|
2597
2902
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
2598
2903
|
const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
|
|
2599
|
-
const network = config.network ??
|
|
2904
|
+
const network = config.network ?? BASE_NETWORK;
|
|
2600
2905
|
const x402Accepts = getConfiguredX402Accepts(config);
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
)
|
|
2610
|
-
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
} else if (x402Accepts.some((accept) => !accept.network)) {
|
|
2618
|
-
x402ConfigError = "x402 accepts require a network.";
|
|
2619
|
-
} else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
|
|
2620
|
-
const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
|
|
2621
|
-
x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
|
|
2622
|
-
} else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
|
|
2623
|
-
x402ConfigError = "non-exact x402 accepts require an asset.";
|
|
2624
|
-
} else if (x402Accepts.some(
|
|
2625
|
-
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2626
|
-
)) {
|
|
2627
|
-
x402ConfigError = "x402 accept decimals must be a non-negative integer.";
|
|
2628
|
-
} else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2629
|
-
x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
|
|
2630
|
-
}
|
|
2631
|
-
}
|
|
2632
|
-
if (config.protocols?.includes("mpp")) {
|
|
2633
|
-
if (!config.mpp) {
|
|
2634
|
-
mppConfigError = 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.';
|
|
2635
|
-
} else if (!config.mpp.recipient && !config.payeeAddress) {
|
|
2636
|
-
mppConfigError = "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config.";
|
|
2637
|
-
} else if (!(config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL)) {
|
|
2638
|
-
mppConfigError = "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object.";
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
const allConfigErrors = [x402ConfigError, mppConfigError].filter(Boolean);
|
|
2642
|
-
if (allConfigErrors.length > 0) {
|
|
2643
|
-
for (const err of allConfigErrors) console.error(`[router] ${err}`);
|
|
2906
|
+
const configIssues = getRouterConfigIssues(config, {
|
|
2907
|
+
requireCdpKeys: process.env.NODE_ENV === "production"
|
|
2908
|
+
});
|
|
2909
|
+
const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
|
|
2910
|
+
if (baseUrlIssue) throw new RouterConfigError([baseUrlIssue]);
|
|
2911
|
+
const emptyProtocolsIssue = configIssues.find((issue) => issue.code === "empty_protocols");
|
|
2912
|
+
if (emptyProtocolsIssue) throw new RouterConfigError([emptyProtocolsIssue]);
|
|
2913
|
+
const protocolConfigIssues = configIssues.filter(
|
|
2914
|
+
(issue) => issue.code !== "missing_base_url" && issue.code !== "empty_protocols"
|
|
2915
|
+
);
|
|
2916
|
+
const x402ConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "x402");
|
|
2917
|
+
const mppConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "mpp");
|
|
2918
|
+
const x402ConfigError = x402ConfigIssues.length > 0 ? formatRouterConfigIssues(x402ConfigIssues) : void 0;
|
|
2919
|
+
const mppConfigError = mppConfigIssues.length > 0 ? formatRouterConfigIssues(mppConfigIssues) : void 0;
|
|
2920
|
+
if (protocolConfigIssues.length > 0) {
|
|
2921
|
+
for (const issue of protocolConfigIssues) console.error(`[router] ${issue.message}`);
|
|
2644
2922
|
if (process.env.NODE_ENV === "production") {
|
|
2645
|
-
throw new
|
|
2923
|
+
throw new RouterConfigError(protocolConfigIssues);
|
|
2646
2924
|
}
|
|
2647
2925
|
}
|
|
2926
|
+
const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2648
2927
|
if (config.plugin?.init) {
|
|
2649
2928
|
try {
|
|
2650
2929
|
const result = config.plugin.init({ origin: resolvedBaseUrl });
|
|
@@ -2789,9 +3068,6 @@ function createRouter(config) {
|
|
|
2789
3068
|
registry
|
|
2790
3069
|
};
|
|
2791
3070
|
}
|
|
2792
|
-
function isSupportedX402Network(network) {
|
|
2793
|
-
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2794
|
-
}
|
|
2795
3071
|
function normalizePath(path) {
|
|
2796
3072
|
let normalized = path.trim();
|
|
2797
3073
|
normalized = normalized.replace(/^\/+/, "");
|
|
@@ -2800,15 +3076,26 @@ function normalizePath(path) {
|
|
|
2800
3076
|
}
|
|
2801
3077
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2802
3078
|
0 && (module.exports = {
|
|
3079
|
+
BASE_NETWORK,
|
|
2803
3080
|
HttpError,
|
|
2804
3081
|
MemoryEntitlementStore,
|
|
2805
3082
|
MemoryNonceStore,
|
|
2806
3083
|
RouteBuilder,
|
|
2807
3084
|
RouteRegistry,
|
|
3085
|
+
RouterConfigError,
|
|
2808
3086
|
SIWX_CHALLENGE_EXPIRY_MS,
|
|
2809
3087
|
SIWX_ERROR_MESSAGES,
|
|
3088
|
+
SOLANA_MAINNET_NETWORK,
|
|
3089
|
+
TEMPO_USDC_CURRENCY,
|
|
3090
|
+
ZERO_EVM_ADDRESS,
|
|
2810
3091
|
consolePlugin,
|
|
2811
3092
|
createRedisEntitlementStore,
|
|
2812
3093
|
createRedisNonceStore,
|
|
2813
|
-
createRouter
|
|
3094
|
+
createRouter,
|
|
3095
|
+
formatRouterConfigIssues,
|
|
3096
|
+
getRouterConfigIssues,
|
|
3097
|
+
mppFromEnv,
|
|
3098
|
+
paidOptionsForProtocols,
|
|
3099
|
+
validateRouterConfig,
|
|
3100
|
+
x402AcceptsFromEnv
|
|
2814
3101
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -260,6 +260,17 @@ interface PaidOptions {
|
|
|
260
260
|
/** Override MPP protocol metadata in x-payment-info discovery. */
|
|
261
261
|
mpp?: MppProtocolInfo;
|
|
262
262
|
}
|
|
263
|
+
type PaymentStatus = 'verified' | 'settled';
|
|
264
|
+
interface HandlerPaymentContext {
|
|
265
|
+
protocol: ProtocolType;
|
|
266
|
+
status: PaymentStatus;
|
|
267
|
+
payer: string;
|
|
268
|
+
amount: string;
|
|
269
|
+
network: string;
|
|
270
|
+
recipient?: string;
|
|
271
|
+
transaction?: string;
|
|
272
|
+
receipt?: string;
|
|
273
|
+
}
|
|
263
274
|
interface HandlerContext<TBody = undefined, TQuery = undefined> {
|
|
264
275
|
body: TBody;
|
|
265
276
|
query: TQuery;
|
|
@@ -267,6 +278,7 @@ interface HandlerContext<TBody = undefined, TQuery = undefined> {
|
|
|
267
278
|
requestId: string;
|
|
268
279
|
route: string;
|
|
269
280
|
wallet: string | null;
|
|
281
|
+
payment: HandlerPaymentContext | null;
|
|
270
282
|
account: unknown;
|
|
271
283
|
alert: AlertFn;
|
|
272
284
|
setVerifiedWallet: (addr: string) => void;
|
|
@@ -640,6 +652,44 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
|
|
|
640
652
|
handler(fn: HandlerArg<TBody, TQuery, HasAuth, NeedsBody, HasBody, NeedsInputExample, NeedsOutputExample>): (request: NextRequest) => Promise<Response>;
|
|
641
653
|
}
|
|
642
654
|
|
|
655
|
+
declare const BASE_NETWORK = "eip155:8453";
|
|
656
|
+
declare const SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
657
|
+
declare const TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
658
|
+
declare const ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
659
|
+
|
|
660
|
+
type RouterEnv = Record<string, string | undefined>;
|
|
661
|
+
type RouterConfigIssueCode = 'missing_base_url' | 'empty_protocols' | 'missing_x402_accepts' | 'missing_x402_network' | 'unsupported_x402_network' | 'missing_x402_asset' | 'invalid_x402_decimals' | 'missing_x402_payee' | 'missing_cdp_keys' | 'placeholder_payee' | 'missing_mpp_config' | 'missing_mpp_secret_key' | 'missing_mpp_currency' | 'missing_mpp_recipient' | 'missing_mpp_rpc_url' | 'missing_mpp_default_store_env';
|
|
662
|
+
interface RouterConfigIssue {
|
|
663
|
+
code: RouterConfigIssueCode;
|
|
664
|
+
message: string;
|
|
665
|
+
protocol?: ProtocolType;
|
|
666
|
+
}
|
|
667
|
+
interface RouterConfigValidationOptions {
|
|
668
|
+
env?: RouterEnv;
|
|
669
|
+
requireCdpKeys?: boolean;
|
|
670
|
+
}
|
|
671
|
+
declare class RouterConfigError extends Error {
|
|
672
|
+
readonly issues: RouterConfigIssue[];
|
|
673
|
+
constructor(issues: RouterConfigIssue[]);
|
|
674
|
+
}
|
|
675
|
+
declare function validateRouterConfig(config: RouterConfig, options?: RouterConfigValidationOptions): void;
|
|
676
|
+
declare function getRouterConfigIssues(config: RouterConfig, options?: RouterConfigValidationOptions): RouterConfigIssue[];
|
|
677
|
+
declare function formatRouterConfigIssues(issues: readonly RouterConfigIssue[]): string;
|
|
678
|
+
declare function mppFromEnv(env: RouterEnv, options?: {
|
|
679
|
+
recipient?: string;
|
|
680
|
+
require?: boolean;
|
|
681
|
+
useDefaultStore?: boolean;
|
|
682
|
+
feePayerKey?: string;
|
|
683
|
+
}): RouterConfig['mpp'] | undefined;
|
|
684
|
+
declare function x402AcceptsFromEnv(env: RouterEnv, options?: {
|
|
685
|
+
payeeAddress?: string;
|
|
686
|
+
payeeEnv?: string;
|
|
687
|
+
network?: string;
|
|
688
|
+
solanaPayeeAddress?: string;
|
|
689
|
+
solanaPayeeEnv?: string;
|
|
690
|
+
}): X402AcceptConfig[];
|
|
691
|
+
declare function paidOptionsForProtocols(protocols: readonly ProtocolType[]): PaidOptions;
|
|
692
|
+
|
|
643
693
|
interface MonitorEntry {
|
|
644
694
|
provider: string;
|
|
645
695
|
route: string;
|
|
@@ -660,4 +710,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
|
|
|
660
710
|
prices?: P;
|
|
661
711
|
}): ServiceRouter<Extract<keyof P, string>>;
|
|
662
712
|
|
|
663
|
-
export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type MppProtocolInfo, type NonceStore, type OveragePolicy, type PaidOptions, type PayToConfig, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402AcceptConfig, type X402FacilitatorTarget, type X402FacilitatorsConfig, type X402ResolvedAccept, type X402RouterFacilitatorConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
|
|
713
|
+
export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, BASE_NETWORK, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, type HandlerPaymentContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type MppProtocolInfo, type NonceStore, type OveragePolicy, type PaidOptions, type PayToConfig, type PaymentEvent, type PaymentStatus, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigValidationOptions, type RouterEnv, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettlementEvent, TEMPO_USDC_CURRENCY, type TierConfig, type X402AcceptConfig, type X402FacilitatorTarget, type X402FacilitatorsConfig, type X402ResolvedAccept, type X402RouterFacilitatorConfig, type X402Server, ZERO_EVM_ADDRESS, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter, formatRouterConfigIssues, getRouterConfigIssues, mppFromEnv, paidOptionsForProtocols, validateRouterConfig, x402AcceptsFromEnv };
|
package/dist/index.d.ts
CHANGED
|
@@ -260,6 +260,17 @@ interface PaidOptions {
|
|
|
260
260
|
/** Override MPP protocol metadata in x-payment-info discovery. */
|
|
261
261
|
mpp?: MppProtocolInfo;
|
|
262
262
|
}
|
|
263
|
+
type PaymentStatus = 'verified' | 'settled';
|
|
264
|
+
interface HandlerPaymentContext {
|
|
265
|
+
protocol: ProtocolType;
|
|
266
|
+
status: PaymentStatus;
|
|
267
|
+
payer: string;
|
|
268
|
+
amount: string;
|
|
269
|
+
network: string;
|
|
270
|
+
recipient?: string;
|
|
271
|
+
transaction?: string;
|
|
272
|
+
receipt?: string;
|
|
273
|
+
}
|
|
263
274
|
interface HandlerContext<TBody = undefined, TQuery = undefined> {
|
|
264
275
|
body: TBody;
|
|
265
276
|
query: TQuery;
|
|
@@ -267,6 +278,7 @@ interface HandlerContext<TBody = undefined, TQuery = undefined> {
|
|
|
267
278
|
requestId: string;
|
|
268
279
|
route: string;
|
|
269
280
|
wallet: string | null;
|
|
281
|
+
payment: HandlerPaymentContext | null;
|
|
270
282
|
account: unknown;
|
|
271
283
|
alert: AlertFn;
|
|
272
284
|
setVerifiedWallet: (addr: string) => void;
|
|
@@ -640,6 +652,44 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
|
|
|
640
652
|
handler(fn: HandlerArg<TBody, TQuery, HasAuth, NeedsBody, HasBody, NeedsInputExample, NeedsOutputExample>): (request: NextRequest) => Promise<Response>;
|
|
641
653
|
}
|
|
642
654
|
|
|
655
|
+
declare const BASE_NETWORK = "eip155:8453";
|
|
656
|
+
declare const SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
657
|
+
declare const TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
658
|
+
declare const ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
659
|
+
|
|
660
|
+
type RouterEnv = Record<string, string | undefined>;
|
|
661
|
+
type RouterConfigIssueCode = 'missing_base_url' | 'empty_protocols' | 'missing_x402_accepts' | 'missing_x402_network' | 'unsupported_x402_network' | 'missing_x402_asset' | 'invalid_x402_decimals' | 'missing_x402_payee' | 'missing_cdp_keys' | 'placeholder_payee' | 'missing_mpp_config' | 'missing_mpp_secret_key' | 'missing_mpp_currency' | 'missing_mpp_recipient' | 'missing_mpp_rpc_url' | 'missing_mpp_default_store_env';
|
|
662
|
+
interface RouterConfigIssue {
|
|
663
|
+
code: RouterConfigIssueCode;
|
|
664
|
+
message: string;
|
|
665
|
+
protocol?: ProtocolType;
|
|
666
|
+
}
|
|
667
|
+
interface RouterConfigValidationOptions {
|
|
668
|
+
env?: RouterEnv;
|
|
669
|
+
requireCdpKeys?: boolean;
|
|
670
|
+
}
|
|
671
|
+
declare class RouterConfigError extends Error {
|
|
672
|
+
readonly issues: RouterConfigIssue[];
|
|
673
|
+
constructor(issues: RouterConfigIssue[]);
|
|
674
|
+
}
|
|
675
|
+
declare function validateRouterConfig(config: RouterConfig, options?: RouterConfigValidationOptions): void;
|
|
676
|
+
declare function getRouterConfigIssues(config: RouterConfig, options?: RouterConfigValidationOptions): RouterConfigIssue[];
|
|
677
|
+
declare function formatRouterConfigIssues(issues: readonly RouterConfigIssue[]): string;
|
|
678
|
+
declare function mppFromEnv(env: RouterEnv, options?: {
|
|
679
|
+
recipient?: string;
|
|
680
|
+
require?: boolean;
|
|
681
|
+
useDefaultStore?: boolean;
|
|
682
|
+
feePayerKey?: string;
|
|
683
|
+
}): RouterConfig['mpp'] | undefined;
|
|
684
|
+
declare function x402AcceptsFromEnv(env: RouterEnv, options?: {
|
|
685
|
+
payeeAddress?: string;
|
|
686
|
+
payeeEnv?: string;
|
|
687
|
+
network?: string;
|
|
688
|
+
solanaPayeeAddress?: string;
|
|
689
|
+
solanaPayeeEnv?: string;
|
|
690
|
+
}): X402AcceptConfig[];
|
|
691
|
+
declare function paidOptionsForProtocols(protocols: readonly ProtocolType[]): PaidOptions;
|
|
692
|
+
|
|
643
693
|
interface MonitorEntry {
|
|
644
694
|
provider: string;
|
|
645
695
|
route: string;
|
|
@@ -660,4 +710,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
|
|
|
660
710
|
prices?: P;
|
|
661
711
|
}): ServiceRouter<Extract<keyof P, string>>;
|
|
662
712
|
|
|
663
|
-
export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type MppProtocolInfo, type NonceStore, type OveragePolicy, type PaidOptions, type PayToConfig, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402AcceptConfig, type X402FacilitatorTarget, type X402FacilitatorsConfig, type X402ResolvedAccept, type X402RouterFacilitatorConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
|
|
713
|
+
export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, BASE_NETWORK, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, type HandlerPaymentContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type MppProtocolInfo, type NonceStore, type OveragePolicy, type PaidOptions, type PayToConfig, type PaymentEvent, type PaymentStatus, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigValidationOptions, type RouterEnv, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettlementEvent, TEMPO_USDC_CURRENCY, type TierConfig, type X402AcceptConfig, type X402FacilitatorTarget, type X402FacilitatorsConfig, type X402ResolvedAccept, type X402RouterFacilitatorConfig, type X402Server, ZERO_EVM_ADDRESS, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter, formatRouterConfigIssues, getRouterConfigIssues, mppFromEnv, paidOptionsForProtocols, validateRouterConfig, x402AcceptsFromEnv };
|
package/dist/index.js
CHANGED
|
@@ -166,6 +166,18 @@ var init_x402_facilitators = __esm({
|
|
|
166
166
|
}
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
+
// src/constants.ts
|
|
170
|
+
var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
|
|
171
|
+
var init_constants = __esm({
|
|
172
|
+
"src/constants.ts"() {
|
|
173
|
+
"use strict";
|
|
174
|
+
BASE_NETWORK = "eip155:8453";
|
|
175
|
+
SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
176
|
+
TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
177
|
+
ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
169
181
|
// src/x402-config.ts
|
|
170
182
|
async function resolvePayToValue(payTo, request, fallback, body) {
|
|
171
183
|
if (!payTo) return fallback;
|
|
@@ -179,7 +191,7 @@ function getConfiguredX402Accepts(config) {
|
|
|
179
191
|
return [
|
|
180
192
|
{
|
|
181
193
|
scheme: "exact",
|
|
182
|
-
network: config.network ??
|
|
194
|
+
network: config.network ?? BASE_NETWORK,
|
|
183
195
|
payTo: config.payeeAddress
|
|
184
196
|
}
|
|
185
197
|
];
|
|
@@ -208,6 +220,7 @@ async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, b
|
|
|
208
220
|
var init_x402_config = __esm({
|
|
209
221
|
"src/x402-config.ts"() {
|
|
210
222
|
"use strict";
|
|
223
|
+
init_constants();
|
|
211
224
|
}
|
|
212
225
|
});
|
|
213
226
|
|
|
@@ -990,6 +1003,10 @@ function getRequirementNetwork(requirements, fallback) {
|
|
|
990
1003
|
const network = requirements?.network;
|
|
991
1004
|
return typeof network === "string" ? network : fallback;
|
|
992
1005
|
}
|
|
1006
|
+
function getRequirementRecipient(requirements) {
|
|
1007
|
+
const payTo = requirements?.payTo;
|
|
1008
|
+
return typeof payTo === "string" ? payTo : void 0;
|
|
1009
|
+
}
|
|
993
1010
|
function siwxSignatureType(network) {
|
|
994
1011
|
return network.startsWith("solana:") ? "ed25519" : "eip191";
|
|
995
1012
|
}
|
|
@@ -1008,7 +1025,7 @@ function getSupportedChains(x402Accepts, fallbackNetwork) {
|
|
|
1008
1025
|
return chains;
|
|
1009
1026
|
}
|
|
1010
1027
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
1011
|
-
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
|
|
1028
|
+
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody, payment) {
|
|
1012
1029
|
const ctx = {
|
|
1013
1030
|
body: parsedBody,
|
|
1014
1031
|
query: parseQuery(request, routeEntry),
|
|
@@ -1016,6 +1033,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1016
1033
|
requestId: meta.requestId,
|
|
1017
1034
|
route: routeEntry.key,
|
|
1018
1035
|
wallet,
|
|
1036
|
+
payment,
|
|
1019
1037
|
account,
|
|
1020
1038
|
alert(level, message, alertMeta) {
|
|
1021
1039
|
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
@@ -1068,7 +1086,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1068
1086
|
pluginCtx,
|
|
1069
1087
|
wallet,
|
|
1070
1088
|
account2,
|
|
1071
|
-
body2.data
|
|
1089
|
+
body2.data,
|
|
1090
|
+
null
|
|
1072
1091
|
);
|
|
1073
1092
|
finalize(response, rawResult, meta, pluginCtx, body2.data);
|
|
1074
1093
|
return response;
|
|
@@ -1345,7 +1364,16 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1345
1364
|
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1346
1365
|
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
1347
1366
|
const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
|
|
1367
|
+
const matchedRecipient = getRequirementRecipient(verifyRequirements);
|
|
1348
1368
|
const wallet = normalizeWalletAddress(verify.payer);
|
|
1369
|
+
const payment = {
|
|
1370
|
+
protocol: "x402",
|
|
1371
|
+
status: "verified",
|
|
1372
|
+
payer: wallet,
|
|
1373
|
+
amount: price,
|
|
1374
|
+
network: matchedNetwork,
|
|
1375
|
+
...matchedRecipient ? { recipient: matchedRecipient } : {}
|
|
1376
|
+
};
|
|
1349
1377
|
pluginCtx.setVerifiedWallet(wallet);
|
|
1350
1378
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1351
1379
|
protocol: "x402",
|
|
@@ -1359,7 +1387,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1359
1387
|
pluginCtx,
|
|
1360
1388
|
wallet,
|
|
1361
1389
|
account,
|
|
1362
|
-
body.data
|
|
1390
|
+
body.data,
|
|
1391
|
+
payment
|
|
1363
1392
|
);
|
|
1364
1393
|
if (response.status < 400) {
|
|
1365
1394
|
try {
|
|
@@ -1458,7 +1487,15 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1458
1487
|
pluginCtx,
|
|
1459
1488
|
wallet,
|
|
1460
1489
|
account,
|
|
1461
|
-
body.data
|
|
1490
|
+
body.data,
|
|
1491
|
+
{
|
|
1492
|
+
protocol: "mpp",
|
|
1493
|
+
status: "verified",
|
|
1494
|
+
payer: wallet,
|
|
1495
|
+
amount: price,
|
|
1496
|
+
network: "tempo:4217",
|
|
1497
|
+
recipient: deps.payeeAddress
|
|
1498
|
+
}
|
|
1462
1499
|
);
|
|
1463
1500
|
if (response2.status < 400) {
|
|
1464
1501
|
let mppResult2;
|
|
@@ -1591,7 +1628,17 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1591
1628
|
pluginCtx,
|
|
1592
1629
|
wallet,
|
|
1593
1630
|
account,
|
|
1594
|
-
body.data
|
|
1631
|
+
body.data,
|
|
1632
|
+
{
|
|
1633
|
+
protocol: "mpp",
|
|
1634
|
+
status: "settled",
|
|
1635
|
+
payer: wallet,
|
|
1636
|
+
amount: price,
|
|
1637
|
+
network: "tempo:4217",
|
|
1638
|
+
recipient: deps.payeeAddress,
|
|
1639
|
+
...txHash ? { transaction: txHash } : {},
|
|
1640
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1641
|
+
}
|
|
1595
1642
|
);
|
|
1596
1643
|
if (response.status < 400) {
|
|
1597
1644
|
if (routeEntry.siwxEnabled) {
|
|
@@ -2551,61 +2598,282 @@ function createLlmsTxtHandler(discovery) {
|
|
|
2551
2598
|
|
|
2552
2599
|
// src/index.ts
|
|
2553
2600
|
init_x402_config();
|
|
2601
|
+
init_constants();
|
|
2602
|
+
|
|
2603
|
+
// src/config.ts
|
|
2604
|
+
init_constants();
|
|
2554
2605
|
init_evm();
|
|
2555
2606
|
init_solana();
|
|
2607
|
+
init_x402_config();
|
|
2608
|
+
var RouterConfigError = class extends Error {
|
|
2609
|
+
issues;
|
|
2610
|
+
constructor(issues) {
|
|
2611
|
+
super(formatRouterConfigIssues(issues));
|
|
2612
|
+
this.name = "RouterConfigError";
|
|
2613
|
+
this.issues = issues;
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
function validateRouterConfig(config, options = {}) {
|
|
2617
|
+
const issues = getRouterConfigIssues(config, options);
|
|
2618
|
+
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
2619
|
+
}
|
|
2620
|
+
function getRouterConfigIssues(config, options = {}) {
|
|
2621
|
+
const env = options.env ?? process.env;
|
|
2622
|
+
const issues = [];
|
|
2623
|
+
const protocols = config.protocols ?? ["x402"];
|
|
2624
|
+
if (!config.baseUrl) {
|
|
2625
|
+
issues.push({
|
|
2626
|
+
code: "missing_base_url",
|
|
2627
|
+
message: '[router] baseUrl is required in RouterConfig. Set it to your production domain (e.g., "https://api.example.com"). The realm is used for payment matching and must be correct.'
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
if (config.protocols && config.protocols.length === 0) {
|
|
2631
|
+
issues.push({
|
|
2632
|
+
code: "empty_protocols",
|
|
2633
|
+
message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
if (protocols.includes("x402")) {
|
|
2637
|
+
issues.push(...validateX402Config(config, env, options));
|
|
2638
|
+
}
|
|
2639
|
+
if (protocols.includes("mpp")) {
|
|
2640
|
+
issues.push(...validateMppConfig(config, env));
|
|
2641
|
+
}
|
|
2642
|
+
return issues;
|
|
2643
|
+
}
|
|
2644
|
+
function formatRouterConfigIssues(issues) {
|
|
2645
|
+
return issues.map((issue) => issue.message).join("\n");
|
|
2646
|
+
}
|
|
2647
|
+
function mppFromEnv(env, options = {}) {
|
|
2648
|
+
const secretKey = env.MPP_SECRET_KEY;
|
|
2649
|
+
const currency = env.MPP_CURRENCY;
|
|
2650
|
+
const rpcUrl = env.TEMPO_RPC_URL;
|
|
2651
|
+
const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
|
|
2652
|
+
if (!hasAnyMppEnv) return void 0;
|
|
2653
|
+
const missing = [
|
|
2654
|
+
secretKey ? null : "MPP_SECRET_KEY",
|
|
2655
|
+
currency ? null : "MPP_CURRENCY",
|
|
2656
|
+
rpcUrl ? null : "TEMPO_RPC_URL"
|
|
2657
|
+
].filter(Boolean);
|
|
2658
|
+
if (missing.length > 0) {
|
|
2659
|
+
throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
|
|
2660
|
+
}
|
|
2661
|
+
return {
|
|
2662
|
+
secretKey,
|
|
2663
|
+
currency,
|
|
2664
|
+
rpcUrl,
|
|
2665
|
+
...options.recipient ? { recipient: options.recipient } : {},
|
|
2666
|
+
...options.feePayerKey ? { feePayerKey: options.feePayerKey } : {},
|
|
2667
|
+
...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
function x402AcceptsFromEnv(env, options = {}) {
|
|
2671
|
+
const payeeEnv = options.payeeEnv ?? "X402_WALLET_ADDRESS";
|
|
2672
|
+
const solanaPayeeEnv = options.solanaPayeeEnv ?? "SOLANA_PAYEE_ADDRESS";
|
|
2673
|
+
const payeeAddress = options.payeeAddress ?? env[payeeEnv];
|
|
2674
|
+
if (!payeeAddress) {
|
|
2675
|
+
throw new Error(`${payeeEnv} is required to build x402 accepts`);
|
|
2676
|
+
}
|
|
2677
|
+
const accepts = [
|
|
2678
|
+
{
|
|
2679
|
+
scheme: "exact",
|
|
2680
|
+
network: options.network ?? BASE_NETWORK,
|
|
2681
|
+
payTo: payeeAddress
|
|
2682
|
+
}
|
|
2683
|
+
];
|
|
2684
|
+
const solanaPayeeAddress = options.solanaPayeeAddress ?? env[solanaPayeeEnv];
|
|
2685
|
+
if (solanaPayeeAddress) {
|
|
2686
|
+
accepts.push({
|
|
2687
|
+
scheme: "exact",
|
|
2688
|
+
network: SOLANA_MAINNET_NETWORK,
|
|
2689
|
+
payTo: solanaPayeeAddress
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
return accepts;
|
|
2693
|
+
}
|
|
2694
|
+
function paidOptionsForProtocols(protocols) {
|
|
2695
|
+
return { protocols: [...protocols] };
|
|
2696
|
+
}
|
|
2697
|
+
function validateX402Config(config, env, options) {
|
|
2698
|
+
const issues = [];
|
|
2699
|
+
const accepts = getConfiguredX402Accepts(config);
|
|
2700
|
+
if (accepts.length === 0) {
|
|
2701
|
+
issues.push({
|
|
2702
|
+
code: "missing_x402_accepts",
|
|
2703
|
+
protocol: "x402",
|
|
2704
|
+
message: "x402 requires at least one accept configuration."
|
|
2705
|
+
});
|
|
2706
|
+
return issues;
|
|
2707
|
+
}
|
|
2708
|
+
const acceptWithoutNetwork = accepts.find((accept) => !accept.network);
|
|
2709
|
+
if (acceptWithoutNetwork) {
|
|
2710
|
+
issues.push({
|
|
2711
|
+
code: "missing_x402_network",
|
|
2712
|
+
protocol: "x402",
|
|
2713
|
+
message: "x402 accepts require a network."
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
const unsupported = accepts.find(
|
|
2717
|
+
(accept) => accept.network && !isSupportedX402Network(accept.network)
|
|
2718
|
+
);
|
|
2719
|
+
if (unsupported) {
|
|
2720
|
+
issues.push({
|
|
2721
|
+
code: "unsupported_x402_network",
|
|
2722
|
+
protocol: "x402",
|
|
2723
|
+
message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
const missingAsset = accepts.find(
|
|
2727
|
+
(accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
|
|
2728
|
+
);
|
|
2729
|
+
if (missingAsset) {
|
|
2730
|
+
issues.push({
|
|
2731
|
+
code: "missing_x402_asset",
|
|
2732
|
+
protocol: "x402",
|
|
2733
|
+
message: "non-exact x402 accepts require an asset."
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
const invalidDecimals = accepts.find(
|
|
2737
|
+
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2738
|
+
);
|
|
2739
|
+
if (invalidDecimals) {
|
|
2740
|
+
issues.push({
|
|
2741
|
+
code: "invalid_x402_decimals",
|
|
2742
|
+
protocol: "x402",
|
|
2743
|
+
message: "x402 accept decimals must be a non-negative integer."
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2747
|
+
issues.push({
|
|
2748
|
+
code: "missing_x402_payee",
|
|
2749
|
+
protocol: "x402",
|
|
2750
|
+
message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
const placeholder = findPlaceholderPayee([
|
|
2754
|
+
config.payeeAddress,
|
|
2755
|
+
...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
|
|
2756
|
+
]);
|
|
2757
|
+
if (placeholder) {
|
|
2758
|
+
issues.push({
|
|
2759
|
+
code: "placeholder_payee",
|
|
2760
|
+
protocol: "x402",
|
|
2761
|
+
message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
if (options.requireCdpKeys !== false && usesDefaultEvmFacilitator(config)) {
|
|
2765
|
+
const missing = [
|
|
2766
|
+
env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
|
|
2767
|
+
env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
|
|
2768
|
+
].filter(Boolean);
|
|
2769
|
+
if (missing.length > 0) {
|
|
2770
|
+
issues.push({
|
|
2771
|
+
code: "missing_cdp_keys",
|
|
2772
|
+
protocol: "x402",
|
|
2773
|
+
message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
return issues;
|
|
2778
|
+
}
|
|
2779
|
+
function validateMppConfig(config, env) {
|
|
2780
|
+
const issues = [];
|
|
2781
|
+
const mpp = config.mpp;
|
|
2782
|
+
if (!mpp) {
|
|
2783
|
+
return [
|
|
2784
|
+
{
|
|
2785
|
+
code: "missing_mpp_config",
|
|
2786
|
+
protocol: "mpp",
|
|
2787
|
+
message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
2788
|
+
}
|
|
2789
|
+
];
|
|
2790
|
+
}
|
|
2791
|
+
if (!mpp.secretKey) {
|
|
2792
|
+
issues.push({
|
|
2793
|
+
code: "missing_mpp_secret_key",
|
|
2794
|
+
protocol: "mpp",
|
|
2795
|
+
message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
if (!mpp.currency) {
|
|
2799
|
+
issues.push({
|
|
2800
|
+
code: "missing_mpp_currency",
|
|
2801
|
+
protocol: "mpp",
|
|
2802
|
+
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
if (!mpp.recipient && !config.payeeAddress) {
|
|
2806
|
+
issues.push({
|
|
2807
|
+
code: "missing_mpp_recipient",
|
|
2808
|
+
protocol: "mpp",
|
|
2809
|
+
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
2813
|
+
if (placeholder) {
|
|
2814
|
+
issues.push({
|
|
2815
|
+
code: "placeholder_payee",
|
|
2816
|
+
protocol: "mpp",
|
|
2817
|
+
message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
|
|
2821
|
+
issues.push({
|
|
2822
|
+
code: "missing_mpp_rpc_url",
|
|
2823
|
+
protocol: "mpp",
|
|
2824
|
+
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
|
|
2828
|
+
issues.push({
|
|
2829
|
+
code: "missing_mpp_default_store_env",
|
|
2830
|
+
protocol: "mpp",
|
|
2831
|
+
message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
return issues;
|
|
2835
|
+
}
|
|
2836
|
+
function usesDefaultEvmFacilitator(config) {
|
|
2837
|
+
return getConfiguredX402Networks(config).some(
|
|
2838
|
+
(network) => typeof network === "string" && isEvmNetwork(network)
|
|
2839
|
+
) && config.x402?.facilitators?.evm === void 0;
|
|
2840
|
+
}
|
|
2841
|
+
function isSupportedX402Network(network) {
|
|
2842
|
+
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2843
|
+
}
|
|
2844
|
+
function findPlaceholderPayee(values) {
|
|
2845
|
+
return values.find((value) => value?.toLowerCase() === ZERO_EVM_ADDRESS) ?? null;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
// src/index.ts
|
|
2849
|
+
init_constants();
|
|
2556
2850
|
function createRouter(config) {
|
|
2557
2851
|
const registry = new RouteRegistry();
|
|
2558
2852
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
2559
2853
|
const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
|
|
2560
|
-
const network = config.network ??
|
|
2854
|
+
const network = config.network ?? BASE_NETWORK;
|
|
2561
2855
|
const x402Accepts = getConfiguredX402Accepts(config);
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
)
|
|
2571
|
-
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
} else if (x402Accepts.some((accept) => !accept.network)) {
|
|
2579
|
-
x402ConfigError = "x402 accepts require a network.";
|
|
2580
|
-
} else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
|
|
2581
|
-
const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
|
|
2582
|
-
x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
|
|
2583
|
-
} else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
|
|
2584
|
-
x402ConfigError = "non-exact x402 accepts require an asset.";
|
|
2585
|
-
} else if (x402Accepts.some(
|
|
2586
|
-
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2587
|
-
)) {
|
|
2588
|
-
x402ConfigError = "x402 accept decimals must be a non-negative integer.";
|
|
2589
|
-
} else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2590
|
-
x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
if (config.protocols?.includes("mpp")) {
|
|
2594
|
-
if (!config.mpp) {
|
|
2595
|
-
mppConfigError = 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.';
|
|
2596
|
-
} else if (!config.mpp.recipient && !config.payeeAddress) {
|
|
2597
|
-
mppConfigError = "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config.";
|
|
2598
|
-
} else if (!(config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL)) {
|
|
2599
|
-
mppConfigError = "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object.";
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
const allConfigErrors = [x402ConfigError, mppConfigError].filter(Boolean);
|
|
2603
|
-
if (allConfigErrors.length > 0) {
|
|
2604
|
-
for (const err of allConfigErrors) console.error(`[router] ${err}`);
|
|
2856
|
+
const configIssues = getRouterConfigIssues(config, {
|
|
2857
|
+
requireCdpKeys: process.env.NODE_ENV === "production"
|
|
2858
|
+
});
|
|
2859
|
+
const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
|
|
2860
|
+
if (baseUrlIssue) throw new RouterConfigError([baseUrlIssue]);
|
|
2861
|
+
const emptyProtocolsIssue = configIssues.find((issue) => issue.code === "empty_protocols");
|
|
2862
|
+
if (emptyProtocolsIssue) throw new RouterConfigError([emptyProtocolsIssue]);
|
|
2863
|
+
const protocolConfigIssues = configIssues.filter(
|
|
2864
|
+
(issue) => issue.code !== "missing_base_url" && issue.code !== "empty_protocols"
|
|
2865
|
+
);
|
|
2866
|
+
const x402ConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "x402");
|
|
2867
|
+
const mppConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "mpp");
|
|
2868
|
+
const x402ConfigError = x402ConfigIssues.length > 0 ? formatRouterConfigIssues(x402ConfigIssues) : void 0;
|
|
2869
|
+
const mppConfigError = mppConfigIssues.length > 0 ? formatRouterConfigIssues(mppConfigIssues) : void 0;
|
|
2870
|
+
if (protocolConfigIssues.length > 0) {
|
|
2871
|
+
for (const issue of protocolConfigIssues) console.error(`[router] ${issue.message}`);
|
|
2605
2872
|
if (process.env.NODE_ENV === "production") {
|
|
2606
|
-
throw new
|
|
2873
|
+
throw new RouterConfigError(protocolConfigIssues);
|
|
2607
2874
|
}
|
|
2608
2875
|
}
|
|
2876
|
+
const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2609
2877
|
if (config.plugin?.init) {
|
|
2610
2878
|
try {
|
|
2611
2879
|
const result = config.plugin.init({ origin: resolvedBaseUrl });
|
|
@@ -2750,9 +3018,6 @@ function createRouter(config) {
|
|
|
2750
3018
|
registry
|
|
2751
3019
|
};
|
|
2752
3020
|
}
|
|
2753
|
-
function isSupportedX402Network(network) {
|
|
2754
|
-
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2755
|
-
}
|
|
2756
3021
|
function normalizePath(path) {
|
|
2757
3022
|
let normalized = path.trim();
|
|
2758
3023
|
normalized = normalized.replace(/^\/+/, "");
|
|
@@ -2760,15 +3025,26 @@ function normalizePath(path) {
|
|
|
2760
3025
|
return normalized.replace(/\/+$/, "");
|
|
2761
3026
|
}
|
|
2762
3027
|
export {
|
|
3028
|
+
BASE_NETWORK,
|
|
2763
3029
|
HttpError,
|
|
2764
3030
|
MemoryEntitlementStore,
|
|
2765
3031
|
MemoryNonceStore,
|
|
2766
3032
|
RouteBuilder,
|
|
2767
3033
|
RouteRegistry,
|
|
3034
|
+
RouterConfigError,
|
|
2768
3035
|
SIWX_CHALLENGE_EXPIRY_MS,
|
|
2769
3036
|
SIWX_ERROR_MESSAGES,
|
|
3037
|
+
SOLANA_MAINNET_NETWORK,
|
|
3038
|
+
TEMPO_USDC_CURRENCY,
|
|
3039
|
+
ZERO_EVM_ADDRESS,
|
|
2770
3040
|
consolePlugin,
|
|
2771
3041
|
createRedisEntitlementStore,
|
|
2772
3042
|
createRedisNonceStore,
|
|
2773
|
-
createRouter
|
|
3043
|
+
createRouter,
|
|
3044
|
+
formatRouterConfigIssues,
|
|
3045
|
+
getRouterConfigIssues,
|
|
3046
|
+
mppFromEnv,
|
|
3047
|
+
paidOptionsForProtocols,
|
|
3048
|
+
validateRouterConfig,
|
|
3049
|
+
x402AcceptsFromEnv
|
|
2774
3050
|
};
|