@agentcash/router 1.4.0 → 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 +346 -56
- package/dist/index.d.cts +51 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.js +334 -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;
|
|
@@ -1153,6 +1183,9 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1153
1183
|
return fail(status, message, meta, pluginCtx, earlyBodyData);
|
|
1154
1184
|
}
|
|
1155
1185
|
}
|
|
1186
|
+
} else {
|
|
1187
|
+
firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
|
|
1188
|
+
return earlyBodyResult.response;
|
|
1156
1189
|
}
|
|
1157
1190
|
}
|
|
1158
1191
|
if (routeEntry.authMode === "siwx" || routeEntry.siwxEnabled) {
|
|
@@ -1381,7 +1414,16 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1381
1414
|
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1382
1415
|
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
1383
1416
|
const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
|
|
1417
|
+
const matchedRecipient = getRequirementRecipient(verifyRequirements);
|
|
1384
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
|
+
};
|
|
1385
1427
|
pluginCtx.setVerifiedWallet(wallet);
|
|
1386
1428
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1387
1429
|
protocol: "x402",
|
|
@@ -1395,7 +1437,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1395
1437
|
pluginCtx,
|
|
1396
1438
|
wallet,
|
|
1397
1439
|
account,
|
|
1398
|
-
body.data
|
|
1440
|
+
body.data,
|
|
1441
|
+
payment
|
|
1399
1442
|
);
|
|
1400
1443
|
if (response.status < 400) {
|
|
1401
1444
|
try {
|
|
@@ -1494,7 +1537,15 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1494
1537
|
pluginCtx,
|
|
1495
1538
|
wallet,
|
|
1496
1539
|
account,
|
|
1497
|
-
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
|
+
}
|
|
1498
1549
|
);
|
|
1499
1550
|
if (response2.status < 400) {
|
|
1500
1551
|
let mppResult2;
|
|
@@ -1627,7 +1678,17 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1627
1678
|
pluginCtx,
|
|
1628
1679
|
wallet,
|
|
1629
1680
|
account,
|
|
1630
|
-
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
|
+
}
|
|
1631
1692
|
);
|
|
1632
1693
|
if (response.status < 400) {
|
|
1633
1694
|
if (routeEntry.siwxEnabled) {
|
|
@@ -2587,61 +2648,282 @@ function createLlmsTxtHandler(discovery) {
|
|
|
2587
2648
|
|
|
2588
2649
|
// src/index.ts
|
|
2589
2650
|
init_x402_config();
|
|
2651
|
+
init_constants();
|
|
2652
|
+
|
|
2653
|
+
// src/config.ts
|
|
2654
|
+
init_constants();
|
|
2590
2655
|
init_evm();
|
|
2591
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();
|
|
2592
2900
|
function createRouter(config) {
|
|
2593
2901
|
const registry = new RouteRegistry();
|
|
2594
2902
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
2595
2903
|
const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
|
|
2596
|
-
const network = config.network ??
|
|
2904
|
+
const network = config.network ?? BASE_NETWORK;
|
|
2597
2905
|
const x402Accepts = getConfiguredX402Accepts(config);
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
)
|
|
2607
|
-
|
|
2608
|
-
const
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
} else if (x402Accepts.some((accept) => !accept.network)) {
|
|
2615
|
-
x402ConfigError = "x402 accepts require a network.";
|
|
2616
|
-
} else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
|
|
2617
|
-
const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
|
|
2618
|
-
x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
|
|
2619
|
-
} else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
|
|
2620
|
-
x402ConfigError = "non-exact x402 accepts require an asset.";
|
|
2621
|
-
} else if (x402Accepts.some(
|
|
2622
|
-
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2623
|
-
)) {
|
|
2624
|
-
x402ConfigError = "x402 accept decimals must be a non-negative integer.";
|
|
2625
|
-
} else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2626
|
-
x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
if (config.protocols?.includes("mpp")) {
|
|
2630
|
-
if (!config.mpp) {
|
|
2631
|
-
mppConfigError = 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.';
|
|
2632
|
-
} else if (!config.mpp.recipient && !config.payeeAddress) {
|
|
2633
|
-
mppConfigError = "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config.";
|
|
2634
|
-
} else if (!(config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL)) {
|
|
2635
|
-
mppConfigError = "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object.";
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
const allConfigErrors = [x402ConfigError, mppConfigError].filter(Boolean);
|
|
2639
|
-
if (allConfigErrors.length > 0) {
|
|
2640
|
-
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}`);
|
|
2641
2922
|
if (process.env.NODE_ENV === "production") {
|
|
2642
|
-
throw new
|
|
2923
|
+
throw new RouterConfigError(protocolConfigIssues);
|
|
2643
2924
|
}
|
|
2644
2925
|
}
|
|
2926
|
+
const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2645
2927
|
if (config.plugin?.init) {
|
|
2646
2928
|
try {
|
|
2647
2929
|
const result = config.plugin.init({ origin: resolvedBaseUrl });
|
|
@@ -2786,9 +3068,6 @@ function createRouter(config) {
|
|
|
2786
3068
|
registry
|
|
2787
3069
|
};
|
|
2788
3070
|
}
|
|
2789
|
-
function isSupportedX402Network(network) {
|
|
2790
|
-
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2791
|
-
}
|
|
2792
3071
|
function normalizePath(path) {
|
|
2793
3072
|
let normalized = path.trim();
|
|
2794
3073
|
normalized = normalized.replace(/^\/+/, "");
|
|
@@ -2797,15 +3076,26 @@ function normalizePath(path) {
|
|
|
2797
3076
|
}
|
|
2798
3077
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2799
3078
|
0 && (module.exports = {
|
|
3079
|
+
BASE_NETWORK,
|
|
2800
3080
|
HttpError,
|
|
2801
3081
|
MemoryEntitlementStore,
|
|
2802
3082
|
MemoryNonceStore,
|
|
2803
3083
|
RouteBuilder,
|
|
2804
3084
|
RouteRegistry,
|
|
3085
|
+
RouterConfigError,
|
|
2805
3086
|
SIWX_CHALLENGE_EXPIRY_MS,
|
|
2806
3087
|
SIWX_ERROR_MESSAGES,
|
|
3088
|
+
SOLANA_MAINNET_NETWORK,
|
|
3089
|
+
TEMPO_USDC_CURRENCY,
|
|
3090
|
+
ZERO_EVM_ADDRESS,
|
|
2807
3091
|
consolePlugin,
|
|
2808
3092
|
createRedisEntitlementStore,
|
|
2809
3093
|
createRedisNonceStore,
|
|
2810
|
-
createRouter
|
|
3094
|
+
createRouter,
|
|
3095
|
+
formatRouterConfigIssues,
|
|
3096
|
+
getRouterConfigIssues,
|
|
3097
|
+
mppFromEnv,
|
|
3098
|
+
paidOptionsForProtocols,
|
|
3099
|
+
validateRouterConfig,
|
|
3100
|
+
x402AcceptsFromEnv
|
|
2811
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;
|
|
@@ -1114,6 +1133,9 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1114
1133
|
return fail(status, message, meta, pluginCtx, earlyBodyData);
|
|
1115
1134
|
}
|
|
1116
1135
|
}
|
|
1136
|
+
} else {
|
|
1137
|
+
firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
|
|
1138
|
+
return earlyBodyResult.response;
|
|
1117
1139
|
}
|
|
1118
1140
|
}
|
|
1119
1141
|
if (routeEntry.authMode === "siwx" || routeEntry.siwxEnabled) {
|
|
@@ -1342,7 +1364,16 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1342
1364
|
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1343
1365
|
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
1344
1366
|
const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
|
|
1367
|
+
const matchedRecipient = getRequirementRecipient(verifyRequirements);
|
|
1345
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
|
+
};
|
|
1346
1377
|
pluginCtx.setVerifiedWallet(wallet);
|
|
1347
1378
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1348
1379
|
protocol: "x402",
|
|
@@ -1356,7 +1387,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1356
1387
|
pluginCtx,
|
|
1357
1388
|
wallet,
|
|
1358
1389
|
account,
|
|
1359
|
-
body.data
|
|
1390
|
+
body.data,
|
|
1391
|
+
payment
|
|
1360
1392
|
);
|
|
1361
1393
|
if (response.status < 400) {
|
|
1362
1394
|
try {
|
|
@@ -1455,7 +1487,15 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1455
1487
|
pluginCtx,
|
|
1456
1488
|
wallet,
|
|
1457
1489
|
account,
|
|
1458
|
-
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
|
+
}
|
|
1459
1499
|
);
|
|
1460
1500
|
if (response2.status < 400) {
|
|
1461
1501
|
let mppResult2;
|
|
@@ -1588,7 +1628,17 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1588
1628
|
pluginCtx,
|
|
1589
1629
|
wallet,
|
|
1590
1630
|
account,
|
|
1591
|
-
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
|
+
}
|
|
1592
1642
|
);
|
|
1593
1643
|
if (response.status < 400) {
|
|
1594
1644
|
if (routeEntry.siwxEnabled) {
|
|
@@ -2548,61 +2598,282 @@ function createLlmsTxtHandler(discovery) {
|
|
|
2548
2598
|
|
|
2549
2599
|
// src/index.ts
|
|
2550
2600
|
init_x402_config();
|
|
2601
|
+
init_constants();
|
|
2602
|
+
|
|
2603
|
+
// src/config.ts
|
|
2604
|
+
init_constants();
|
|
2551
2605
|
init_evm();
|
|
2552
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();
|
|
2553
2850
|
function createRouter(config) {
|
|
2554
2851
|
const registry = new RouteRegistry();
|
|
2555
2852
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
2556
2853
|
const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
|
|
2557
|
-
const network = config.network ??
|
|
2854
|
+
const network = config.network ?? BASE_NETWORK;
|
|
2558
2855
|
const x402Accepts = getConfiguredX402Accepts(config);
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
)
|
|
2568
|
-
|
|
2569
|
-
const
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
} else if (x402Accepts.some((accept) => !accept.network)) {
|
|
2576
|
-
x402ConfigError = "x402 accepts require a network.";
|
|
2577
|
-
} else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
|
|
2578
|
-
const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
|
|
2579
|
-
x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
|
|
2580
|
-
} else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
|
|
2581
|
-
x402ConfigError = "non-exact x402 accepts require an asset.";
|
|
2582
|
-
} else if (x402Accepts.some(
|
|
2583
|
-
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2584
|
-
)) {
|
|
2585
|
-
x402ConfigError = "x402 accept decimals must be a non-negative integer.";
|
|
2586
|
-
} else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2587
|
-
x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
if (config.protocols?.includes("mpp")) {
|
|
2591
|
-
if (!config.mpp) {
|
|
2592
|
-
mppConfigError = 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.';
|
|
2593
|
-
} else if (!config.mpp.recipient && !config.payeeAddress) {
|
|
2594
|
-
mppConfigError = "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config.";
|
|
2595
|
-
} else if (!(config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL)) {
|
|
2596
|
-
mppConfigError = "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object.";
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
const allConfigErrors = [x402ConfigError, mppConfigError].filter(Boolean);
|
|
2600
|
-
if (allConfigErrors.length > 0) {
|
|
2601
|
-
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}`);
|
|
2602
2872
|
if (process.env.NODE_ENV === "production") {
|
|
2603
|
-
throw new
|
|
2873
|
+
throw new RouterConfigError(protocolConfigIssues);
|
|
2604
2874
|
}
|
|
2605
2875
|
}
|
|
2876
|
+
const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2606
2877
|
if (config.plugin?.init) {
|
|
2607
2878
|
try {
|
|
2608
2879
|
const result = config.plugin.init({ origin: resolvedBaseUrl });
|
|
@@ -2747,9 +3018,6 @@ function createRouter(config) {
|
|
|
2747
3018
|
registry
|
|
2748
3019
|
};
|
|
2749
3020
|
}
|
|
2750
|
-
function isSupportedX402Network(network) {
|
|
2751
|
-
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2752
|
-
}
|
|
2753
3021
|
function normalizePath(path) {
|
|
2754
3022
|
let normalized = path.trim();
|
|
2755
3023
|
normalized = normalized.replace(/^\/+/, "");
|
|
@@ -2757,15 +3025,26 @@ function normalizePath(path) {
|
|
|
2757
3025
|
return normalized.replace(/\/+$/, "");
|
|
2758
3026
|
}
|
|
2759
3027
|
export {
|
|
3028
|
+
BASE_NETWORK,
|
|
2760
3029
|
HttpError,
|
|
2761
3030
|
MemoryEntitlementStore,
|
|
2762
3031
|
MemoryNonceStore,
|
|
2763
3032
|
RouteBuilder,
|
|
2764
3033
|
RouteRegistry,
|
|
3034
|
+
RouterConfigError,
|
|
2765
3035
|
SIWX_CHALLENGE_EXPIRY_MS,
|
|
2766
3036
|
SIWX_ERROR_MESSAGES,
|
|
3037
|
+
SOLANA_MAINNET_NETWORK,
|
|
3038
|
+
TEMPO_USDC_CURRENCY,
|
|
3039
|
+
ZERO_EVM_ADDRESS,
|
|
2767
3040
|
consolePlugin,
|
|
2768
3041
|
createRedisEntitlementStore,
|
|
2769
3042
|
createRedisNonceStore,
|
|
2770
|
-
createRouter
|
|
3043
|
+
createRouter,
|
|
3044
|
+
formatRouterConfigIssues,
|
|
3045
|
+
getRouterConfigIssues,
|
|
3046
|
+
mppFromEnv,
|
|
3047
|
+
paidOptionsForProtocols,
|
|
3048
|
+
validateRouterConfig,
|
|
3049
|
+
x402AcceptsFromEnv
|
|
2771
3050
|
};
|