@agentcash/router 1.4.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -9
- package/dist/index.cjs +658 -112
- package/dist/index.d.cts +148 -41
- package/dist/index.d.ts +148 -41
- package/dist/index.js +646 -111
- package/package.json +3 -3
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
|
|
|
@@ -446,7 +470,7 @@ var import_server2 = require("next/server");
|
|
|
446
470
|
|
|
447
471
|
// src/auth/normalize-wallet.ts
|
|
448
472
|
function normalizeWalletAddress(address) {
|
|
449
|
-
return
|
|
473
|
+
return /^0x/i.test(address) ? address.toLowerCase() : address;
|
|
450
474
|
}
|
|
451
475
|
|
|
452
476
|
// src/plugin.ts
|
|
@@ -611,12 +635,13 @@ var HttpError = class extends Error {
|
|
|
611
635
|
};
|
|
612
636
|
|
|
613
637
|
// src/handler.ts
|
|
614
|
-
async function safeCallHandler(handler, ctx) {
|
|
638
|
+
async function safeCallHandler(handler, ctx, options = {}) {
|
|
615
639
|
try {
|
|
616
640
|
const result = await handler(ctx);
|
|
617
641
|
if (result instanceof Response) return result;
|
|
618
642
|
return import_server.NextResponse.json(result);
|
|
619
643
|
} catch (error) {
|
|
644
|
+
options.onError?.(error);
|
|
620
645
|
const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
|
|
621
646
|
const message = error instanceof Error ? error.message : "Internal error";
|
|
622
647
|
return import_server.NextResponse.json({ success: false, error: message }, { status });
|
|
@@ -739,6 +764,9 @@ async function verifyX402Payment(opts) {
|
|
|
739
764
|
throw err;
|
|
740
765
|
}
|
|
741
766
|
if (!verify.isValid) return invalidPaymentVerification();
|
|
767
|
+
if (typeof verify.payer !== "string" || verify.payer.length === 0) {
|
|
768
|
+
throw new Error("x402 verification succeeded without a payer address");
|
|
769
|
+
}
|
|
742
770
|
return {
|
|
743
771
|
valid: true,
|
|
744
772
|
payer: verify.payer,
|
|
@@ -1029,6 +1057,21 @@ function getRequirementNetwork(requirements, fallback) {
|
|
|
1029
1057
|
const network = requirements?.network;
|
|
1030
1058
|
return typeof network === "string" ? network : fallback;
|
|
1031
1059
|
}
|
|
1060
|
+
function getRequirementRecipient(requirements) {
|
|
1061
|
+
const payTo = requirements?.payTo;
|
|
1062
|
+
return typeof payTo === "string" ? payTo : void 0;
|
|
1063
|
+
}
|
|
1064
|
+
function errorStatus(error, fallback) {
|
|
1065
|
+
const status = error?.status;
|
|
1066
|
+
return typeof status === "number" ? status : fallback;
|
|
1067
|
+
}
|
|
1068
|
+
function errorMessage(error, fallback) {
|
|
1069
|
+
return error instanceof Error ? error.message : fallback;
|
|
1070
|
+
}
|
|
1071
|
+
function handlerFailureError(response) {
|
|
1072
|
+
const message = response.statusText || `Handler returned HTTP ${response.status}`;
|
|
1073
|
+
return Object.assign(new Error(message), { status: response.status });
|
|
1074
|
+
}
|
|
1032
1075
|
function siwxSignatureType(network) {
|
|
1033
1076
|
return network.startsWith("solana:") ? "ed25519" : "eip191";
|
|
1034
1077
|
}
|
|
@@ -1047,7 +1090,7 @@ function getSupportedChains(x402Accepts, fallbackNetwork) {
|
|
|
1047
1090
|
return chains;
|
|
1048
1091
|
}
|
|
1049
1092
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
1050
|
-
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
|
|
1093
|
+
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody, payment) {
|
|
1051
1094
|
const ctx = {
|
|
1052
1095
|
body: parsedBody,
|
|
1053
1096
|
query: parseQuery(request, routeEntry),
|
|
@@ -1055,6 +1098,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1055
1098
|
requestId: meta.requestId,
|
|
1056
1099
|
route: routeEntry.key,
|
|
1057
1100
|
wallet,
|
|
1101
|
+
payment,
|
|
1058
1102
|
account,
|
|
1059
1103
|
alert(level, message, alertMeta) {
|
|
1060
1104
|
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
@@ -1067,11 +1111,20 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1067
1111
|
setVerifiedWallet: (addr) => pluginCtx.setVerifiedWallet(addr)
|
|
1068
1112
|
};
|
|
1069
1113
|
let rawResult;
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1114
|
+
let handlerError;
|
|
1115
|
+
const response = await safeCallHandler(
|
|
1116
|
+
async (c) => {
|
|
1117
|
+
rawResult = await handler(c);
|
|
1118
|
+
return rawResult;
|
|
1119
|
+
},
|
|
1120
|
+
ctx,
|
|
1121
|
+
{
|
|
1122
|
+
onError(error) {
|
|
1123
|
+
handlerError = error;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
);
|
|
1127
|
+
return { response, rawResult, handlerError };
|
|
1075
1128
|
}
|
|
1076
1129
|
function finalize(response, rawResult, meta, pluginCtx, requestBody) {
|
|
1077
1130
|
fireProviderQuota(routeEntry, response, rawResult, deps, pluginCtx);
|
|
@@ -1082,6 +1135,87 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1082
1135
|
firePluginResponse(deps, pluginCtx, meta, response, requestBody);
|
|
1083
1136
|
return response;
|
|
1084
1137
|
}
|
|
1138
|
+
function settlementContext(scope) {
|
|
1139
|
+
return {
|
|
1140
|
+
route: routeEntry.key,
|
|
1141
|
+
request: scope.request,
|
|
1142
|
+
body: scope.parsedBody,
|
|
1143
|
+
wallet: scope.wallet,
|
|
1144
|
+
account: scope.account,
|
|
1145
|
+
payment: scope.payment,
|
|
1146
|
+
response: scope.response,
|
|
1147
|
+
result: scope.rawResult
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
async function runBeforeSettle(scope) {
|
|
1151
|
+
const hook = routeEntry.settlement?.beforeSettle;
|
|
1152
|
+
if (!hook) return null;
|
|
1153
|
+
try {
|
|
1154
|
+
await hook(settlementContext(scope));
|
|
1155
|
+
return null;
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
return fail(
|
|
1158
|
+
errorStatus(error, 500),
|
|
1159
|
+
errorMessage(error, "Pre-settlement validation failed"),
|
|
1160
|
+
scope.meta,
|
|
1161
|
+
scope.pluginCtx,
|
|
1162
|
+
scope.parsedBody
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
async function runSettlementError(scope, error, phase) {
|
|
1167
|
+
const hook = routeEntry.settlement?.onSettlementError;
|
|
1168
|
+
if (!hook) return;
|
|
1169
|
+
try {
|
|
1170
|
+
await hook({
|
|
1171
|
+
...settlementContext(scope),
|
|
1172
|
+
error,
|
|
1173
|
+
phase
|
|
1174
|
+
});
|
|
1175
|
+
} catch (hookError) {
|
|
1176
|
+
const message = errorMessage(hookError, "Settlement error hook failed");
|
|
1177
|
+
console.error(`[router] ${routeEntry.key}: onSettlementError failed: ${message}`);
|
|
1178
|
+
firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
|
|
1179
|
+
level: "error",
|
|
1180
|
+
message: `Settlement error hook failed: ${message}`,
|
|
1181
|
+
route: routeEntry.key
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async function runAfterSettle(scope) {
|
|
1186
|
+
const hook = routeEntry.settlement?.afterSettle;
|
|
1187
|
+
if (!hook) return;
|
|
1188
|
+
try {
|
|
1189
|
+
await hook(settlementContext(scope));
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
const message = errorMessage(error, "Post-settlement hook failed");
|
|
1192
|
+
console.error(`[router] ${routeEntry.key}: afterSettle failed: ${message}`);
|
|
1193
|
+
firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
|
|
1194
|
+
level: "error",
|
|
1195
|
+
message: `Post-settlement hook failed: ${message}`,
|
|
1196
|
+
route: routeEntry.key
|
|
1197
|
+
});
|
|
1198
|
+
await runSettlementError(scope, error, "afterSettle");
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
async function runSettledHandlerError(scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
|
|
1202
|
+
const hook = routeEntry.settlement?.onSettledHandlerError;
|
|
1203
|
+
if (!hook) return;
|
|
1204
|
+
try {
|
|
1205
|
+
await hook({
|
|
1206
|
+
...settlementContext(scope),
|
|
1207
|
+
error
|
|
1208
|
+
});
|
|
1209
|
+
} catch (hookError) {
|
|
1210
|
+
const message = errorMessage(hookError, "Settled handler error hook failed");
|
|
1211
|
+
console.error(`[router] ${routeEntry.key}: onSettledHandlerError failed: ${message}`);
|
|
1212
|
+
firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
|
|
1213
|
+
level: "error",
|
|
1214
|
+
message: `Settled handler error hook failed: ${message}`,
|
|
1215
|
+
route: routeEntry.key
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1085
1219
|
return async (request) => {
|
|
1086
1220
|
await deps.initPromise;
|
|
1087
1221
|
const meta = buildMeta(request, routeEntry);
|
|
@@ -1107,7 +1241,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1107
1241
|
pluginCtx,
|
|
1108
1242
|
wallet,
|
|
1109
1243
|
account2,
|
|
1110
|
-
body2.data
|
|
1244
|
+
body2.data,
|
|
1245
|
+
null
|
|
1111
1246
|
);
|
|
1112
1247
|
finalize(response, rawResult, meta, pluginCtx, body2.data);
|
|
1113
1248
|
return response;
|
|
@@ -1336,18 +1471,9 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1336
1471
|
return fail(status, message, meta, pluginCtx, body.data);
|
|
1337
1472
|
}
|
|
1338
1473
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
} catch (err) {
|
|
1343
|
-
return fail(
|
|
1344
|
-
err.status ?? 500,
|
|
1345
|
-
err instanceof Error ? err.message : "Price resolution failed",
|
|
1346
|
-
meta,
|
|
1347
|
-
pluginCtx,
|
|
1348
|
-
body.data
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1474
|
+
const priceResult = await resolveDynamicPrice(body.data, routeEntry, deps, pluginCtx, meta);
|
|
1475
|
+
if ("error" in priceResult) return priceResult.error;
|
|
1476
|
+
const price = priceResult.price;
|
|
1351
1477
|
if (!routeEntry.protocols.includes(protocol)) {
|
|
1352
1478
|
const accepted = routeEntry.protocols.join(", ") || "none";
|
|
1353
1479
|
console.warn(
|
|
@@ -1384,7 +1510,16 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1384
1510
|
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1385
1511
|
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
1386
1512
|
const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
|
|
1513
|
+
const matchedRecipient = getRequirementRecipient(verifyRequirements);
|
|
1387
1514
|
const wallet = normalizeWalletAddress(verify.payer);
|
|
1515
|
+
const payment = {
|
|
1516
|
+
protocol: "x402",
|
|
1517
|
+
status: "verified",
|
|
1518
|
+
payer: wallet,
|
|
1519
|
+
amount: price,
|
|
1520
|
+
network: matchedNetwork,
|
|
1521
|
+
...matchedRecipient ? { recipient: matchedRecipient } : {}
|
|
1522
|
+
};
|
|
1388
1523
|
pluginCtx.setVerifiedWallet(wallet);
|
|
1389
1524
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1390
1525
|
protocol: "x402",
|
|
@@ -1392,15 +1527,30 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1392
1527
|
amount: price,
|
|
1393
1528
|
network: matchedNetwork
|
|
1394
1529
|
});
|
|
1395
|
-
const { response, rawResult } = await invoke(
|
|
1530
|
+
const { response, rawResult, handlerError } = await invoke(
|
|
1396
1531
|
request,
|
|
1397
1532
|
meta,
|
|
1398
1533
|
pluginCtx,
|
|
1399
1534
|
wallet,
|
|
1400
1535
|
account,
|
|
1401
|
-
body.data
|
|
1536
|
+
body.data,
|
|
1537
|
+
payment
|
|
1402
1538
|
);
|
|
1539
|
+
const settleScope = {
|
|
1540
|
+
request,
|
|
1541
|
+
meta,
|
|
1542
|
+
pluginCtx,
|
|
1543
|
+
wallet,
|
|
1544
|
+
account,
|
|
1545
|
+
parsedBody: body.data,
|
|
1546
|
+
payment,
|
|
1547
|
+
response,
|
|
1548
|
+
rawResult,
|
|
1549
|
+
handlerError
|
|
1550
|
+
};
|
|
1403
1551
|
if (response.status < 400) {
|
|
1552
|
+
const validationFailure = await runBeforeSettle(settleScope);
|
|
1553
|
+
if (validationFailure) return validationFailure;
|
|
1404
1554
|
try {
|
|
1405
1555
|
const settle = await settleX402Payment(
|
|
1406
1556
|
deps.x402Server,
|
|
@@ -1426,13 +1576,21 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1426
1576
|
}
|
|
1427
1577
|
response.headers.set("PAYMENT-RESPONSE", settle.encoded);
|
|
1428
1578
|
response.headers.set("Cache-Control", "private");
|
|
1579
|
+
const transaction = String(settle.result?.transaction ?? "");
|
|
1580
|
+
const settledPayment = {
|
|
1581
|
+
...payment,
|
|
1582
|
+
status: "settled",
|
|
1583
|
+
...transaction ? { transaction } : {}
|
|
1584
|
+
};
|
|
1429
1585
|
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1430
1586
|
protocol: "x402",
|
|
1431
|
-
payer:
|
|
1432
|
-
transaction
|
|
1587
|
+
payer: wallet,
|
|
1588
|
+
transaction,
|
|
1433
1589
|
network: matchedNetwork
|
|
1434
1590
|
});
|
|
1591
|
+
await runAfterSettle({ ...settleScope, payment: settledPayment });
|
|
1435
1592
|
} catch (err) {
|
|
1593
|
+
await runSettlementError(settleScope, err, "settle");
|
|
1436
1594
|
const errObj = err;
|
|
1437
1595
|
console.error("Settlement failed", {
|
|
1438
1596
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -1491,19 +1649,44 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1491
1649
|
amount: price,
|
|
1492
1650
|
network: "tempo:4217"
|
|
1493
1651
|
});
|
|
1494
|
-
const
|
|
1652
|
+
const mppRecipient2 = deps.mppRecipient ?? deps.payeeAddress;
|
|
1653
|
+
const payment2 = {
|
|
1654
|
+
protocol: "mpp",
|
|
1655
|
+
status: "verified",
|
|
1656
|
+
payer: wallet,
|
|
1657
|
+
amount: price,
|
|
1658
|
+
network: "tempo:4217",
|
|
1659
|
+
...mppRecipient2 ? { recipient: mppRecipient2 } : {}
|
|
1660
|
+
};
|
|
1661
|
+
const { response: response2, rawResult: rawResult2, handlerError: handlerError2 } = await invoke(
|
|
1495
1662
|
request,
|
|
1496
1663
|
meta,
|
|
1497
1664
|
pluginCtx,
|
|
1498
1665
|
wallet,
|
|
1499
1666
|
account,
|
|
1500
|
-
body.data
|
|
1667
|
+
body.data,
|
|
1668
|
+
payment2
|
|
1501
1669
|
);
|
|
1670
|
+
const settleScope2 = {
|
|
1671
|
+
request,
|
|
1672
|
+
meta,
|
|
1673
|
+
pluginCtx,
|
|
1674
|
+
wallet,
|
|
1675
|
+
account,
|
|
1676
|
+
parsedBody: body.data,
|
|
1677
|
+
payment: payment2,
|
|
1678
|
+
response: response2,
|
|
1679
|
+
rawResult: rawResult2,
|
|
1680
|
+
handlerError: handlerError2
|
|
1681
|
+
};
|
|
1502
1682
|
if (response2.status < 400) {
|
|
1683
|
+
const validationFailure = await runBeforeSettle(settleScope2);
|
|
1684
|
+
if (validationFailure) return validationFailure;
|
|
1503
1685
|
let mppResult2;
|
|
1504
1686
|
try {
|
|
1505
1687
|
mppResult2 = await deps.mppx.charge({ amount: price })(request);
|
|
1506
1688
|
} catch (err) {
|
|
1689
|
+
await runSettlementError(settleScope2, err, "settle");
|
|
1507
1690
|
const message = err instanceof Error ? err.message : String(err);
|
|
1508
1691
|
console.error(
|
|
1509
1692
|
`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`
|
|
@@ -1532,6 +1715,13 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1532
1715
|
} catch {
|
|
1533
1716
|
}
|
|
1534
1717
|
const detail = rejectReason || "transaction reverted on-chain after handler execution";
|
|
1718
|
+
const settlementError = Object.assign(new Error(detail), {
|
|
1719
|
+
status: 402,
|
|
1720
|
+
detail,
|
|
1721
|
+
mppResult: mppResult2,
|
|
1722
|
+
challenge: mppResult2.challenge
|
|
1723
|
+
});
|
|
1724
|
+
await runSettlementError(settleScope2, settlementError, "settle");
|
|
1535
1725
|
console.error(
|
|
1536
1726
|
`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`
|
|
1537
1727
|
);
|
|
@@ -1569,6 +1759,17 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1569
1759
|
transaction: txHash2,
|
|
1570
1760
|
network: "tempo:4217"
|
|
1571
1761
|
});
|
|
1762
|
+
const settledPayment = {
|
|
1763
|
+
...payment2,
|
|
1764
|
+
status: "settled",
|
|
1765
|
+
...txHash2 ? { transaction: txHash2 } : {},
|
|
1766
|
+
...receiptHeader2 ? { receipt: receiptHeader2 } : {}
|
|
1767
|
+
};
|
|
1768
|
+
await runAfterSettle({
|
|
1769
|
+
...settleScope2,
|
|
1770
|
+
payment: settledPayment,
|
|
1771
|
+
response: receiptResponse
|
|
1772
|
+
});
|
|
1572
1773
|
finalize(receiptResponse, rawResult2, meta, pluginCtx, body.data);
|
|
1573
1774
|
return receiptResponse;
|
|
1574
1775
|
}
|
|
@@ -1624,14 +1825,38 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1624
1825
|
amount: price,
|
|
1625
1826
|
network: "tempo:4217"
|
|
1626
1827
|
});
|
|
1627
|
-
const
|
|
1828
|
+
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1829
|
+
const payment = {
|
|
1830
|
+
protocol: "mpp",
|
|
1831
|
+
status: "settled",
|
|
1832
|
+
payer: wallet,
|
|
1833
|
+
amount: price,
|
|
1834
|
+
network: "tempo:4217",
|
|
1835
|
+
...mppRecipient ? { recipient: mppRecipient } : {},
|
|
1836
|
+
...txHash ? { transaction: txHash } : {},
|
|
1837
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1838
|
+
};
|
|
1839
|
+
const { response, rawResult, handlerError } = await invoke(
|
|
1628
1840
|
request,
|
|
1629
1841
|
meta,
|
|
1630
1842
|
pluginCtx,
|
|
1631
1843
|
wallet,
|
|
1632
1844
|
account,
|
|
1633
|
-
body.data
|
|
1845
|
+
body.data,
|
|
1846
|
+
payment
|
|
1634
1847
|
);
|
|
1848
|
+
const settleScope = {
|
|
1849
|
+
request,
|
|
1850
|
+
meta,
|
|
1851
|
+
pluginCtx,
|
|
1852
|
+
wallet,
|
|
1853
|
+
account,
|
|
1854
|
+
parsedBody: body.data,
|
|
1855
|
+
payment,
|
|
1856
|
+
response,
|
|
1857
|
+
rawResult,
|
|
1858
|
+
handlerError
|
|
1859
|
+
};
|
|
1635
1860
|
if (response.status < 400) {
|
|
1636
1861
|
if (routeEntry.siwxEnabled) {
|
|
1637
1862
|
try {
|
|
@@ -1652,9 +1877,11 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1652
1877
|
transaction: txHash,
|
|
1653
1878
|
network: "tempo:4217"
|
|
1654
1879
|
});
|
|
1880
|
+
await runAfterSettle({ ...settleScope, response: receiptResponse });
|
|
1655
1881
|
finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
|
|
1656
1882
|
return receiptResponse;
|
|
1657
1883
|
}
|
|
1884
|
+
await runSettledHandlerError(settleScope);
|
|
1658
1885
|
finalize(response, rawResult, meta, pluginCtx, body.data);
|
|
1659
1886
|
return response;
|
|
1660
1887
|
}
|
|
@@ -1727,9 +1954,10 @@ async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta)
|
|
|
1727
1954
|
});
|
|
1728
1955
|
return { price: routeEntry.maxPrice };
|
|
1729
1956
|
} else {
|
|
1957
|
+
const message = errorMessage(err, "Price calculation failed");
|
|
1730
1958
|
const errorResponse = import_server2.NextResponse.json(
|
|
1731
|
-
{ success: false, error:
|
|
1732
|
-
{ status: 500 }
|
|
1959
|
+
{ success: false, error: message },
|
|
1960
|
+
{ status: errorStatus(err, 500) }
|
|
1733
1961
|
);
|
|
1734
1962
|
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
1735
1963
|
return { error: errorResponse };
|
|
@@ -1902,22 +2130,13 @@ function fireProviderQuota(routeEntry, response, handlerResult, deps, pluginCtx)
|
|
|
1902
2130
|
|
|
1903
2131
|
// src/validate-examples.ts
|
|
1904
2132
|
function validateExamples(key, bodySchema, querySchema, outputSchema, inputExample, hasInputExample, outputExample, hasOutputExample) {
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
);
|
|
1909
|
-
}
|
|
1910
|
-
if (querySchema && !hasInputExample) {
|
|
1911
|
-
throw new Error(
|
|
1912
|
-
`route '${key}': .query() requires a matching .inputExample() \u2014 the bazaar discovery extension needs a conforming sample query to advertise.`
|
|
1913
|
-
);
|
|
2133
|
+
const inputSchema = bodySchema ?? querySchema;
|
|
2134
|
+
if (hasInputExample && !inputSchema) {
|
|
2135
|
+
throw new Error(`route '${key}': .inputExample() requires .body() or .query()`);
|
|
1914
2136
|
}
|
|
1915
|
-
if (
|
|
1916
|
-
throw new Error(
|
|
1917
|
-
`route '${key}': .output() requires a matching .outputExample() \u2014 the bazaar discovery extension needs a conforming sample response to advertise.`
|
|
1918
|
-
);
|
|
2137
|
+
if (hasOutputExample && !outputSchema) {
|
|
2138
|
+
throw new Error(`route '${key}': .outputExample() requires .output()`);
|
|
1919
2139
|
}
|
|
1920
|
-
const inputSchema = bodySchema ?? querySchema;
|
|
1921
2140
|
if (inputSchema && hasInputExample) {
|
|
1922
2141
|
const result = inputSchema.safeParse(inputExample);
|
|
1923
2142
|
if (!result.success) {
|
|
@@ -1991,6 +2210,8 @@ var RouteBuilder = class {
|
|
|
1991
2210
|
/** @internal */
|
|
1992
2211
|
_validateFn;
|
|
1993
2212
|
/** @internal */
|
|
2213
|
+
_settlement;
|
|
2214
|
+
/** @internal */
|
|
1994
2215
|
_mppInfo;
|
|
1995
2216
|
constructor(key, registry, deps) {
|
|
1996
2217
|
this._key = key;
|
|
@@ -2004,11 +2225,21 @@ var RouteBuilder = class {
|
|
|
2004
2225
|
return next;
|
|
2005
2226
|
}
|
|
2006
2227
|
paid(pricing, options) {
|
|
2228
|
+
if (this._authMode === "unprotected") {
|
|
2229
|
+
throw new Error(
|
|
2230
|
+
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
if (this._pricing !== void 0) {
|
|
2234
|
+
throw new Error(
|
|
2235
|
+
`route '${this._key}': Cannot call .paid() more than once on the same route.`
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2007
2238
|
const next = this.fork();
|
|
2008
2239
|
next._authMode = "paid";
|
|
2009
2240
|
next._pricing = pricing;
|
|
2010
2241
|
if (options?.protocols) {
|
|
2011
|
-
next._protocols = options.protocols;
|
|
2242
|
+
next._protocols = [...options.protocols];
|
|
2012
2243
|
} else if (next._protocols.length === 0) {
|
|
2013
2244
|
next._protocols = ["x402"];
|
|
2014
2245
|
}
|
|
@@ -2073,6 +2304,16 @@ var RouteBuilder = class {
|
|
|
2073
2304
|
return next;
|
|
2074
2305
|
}
|
|
2075
2306
|
unprotected() {
|
|
2307
|
+
if (this._authMode && this._authMode !== "unprotected") {
|
|
2308
|
+
throw new Error(
|
|
2309
|
+
`route '${this._key}': Cannot combine .unprotected() and .${this._authMode}() on the same route.`
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
if (this._pricing) {
|
|
2313
|
+
throw new Error(
|
|
2314
|
+
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2076
2317
|
const next = this.fork();
|
|
2077
2318
|
next._authMode = "unprotected";
|
|
2078
2319
|
next._protocols = [];
|
|
@@ -2087,32 +2328,43 @@ var RouteBuilder = class {
|
|
|
2087
2328
|
next._providerConfig = config ?? {};
|
|
2088
2329
|
return next;
|
|
2089
2330
|
}
|
|
2090
|
-
|
|
2091
|
-
// Schema methods
|
|
2092
|
-
// -------------------------------------------------------------------------
|
|
2093
|
-
body(schema) {
|
|
2331
|
+
body(schema, example) {
|
|
2094
2332
|
const next = this.fork();
|
|
2095
2333
|
next._bodySchema = schema;
|
|
2334
|
+
if (example !== void 0) {
|
|
2335
|
+
next._inputExample = example;
|
|
2336
|
+
next._hasInputExample = true;
|
|
2337
|
+
}
|
|
2096
2338
|
return next;
|
|
2097
2339
|
}
|
|
2098
|
-
query(schema) {
|
|
2340
|
+
query(schema, example) {
|
|
2099
2341
|
const next = this.fork();
|
|
2100
2342
|
next._querySchema = schema;
|
|
2343
|
+
if (example !== void 0) {
|
|
2344
|
+
next._inputExample = example;
|
|
2345
|
+
next._hasInputExample = true;
|
|
2346
|
+
}
|
|
2101
2347
|
next._method = "GET";
|
|
2102
2348
|
return next;
|
|
2103
2349
|
}
|
|
2104
|
-
output(schema) {
|
|
2350
|
+
output(schema, example) {
|
|
2105
2351
|
const next = this.fork();
|
|
2106
2352
|
next._outputSchema = schema;
|
|
2353
|
+
if (example !== void 0) {
|
|
2354
|
+
next._outputExample = example;
|
|
2355
|
+
next._hasOutputExample = true;
|
|
2356
|
+
}
|
|
2107
2357
|
return next;
|
|
2108
2358
|
}
|
|
2109
2359
|
/**
|
|
2110
2360
|
* Provide a conforming example of the request input (body or query params).
|
|
2111
2361
|
*
|
|
2112
|
-
*
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2362
|
+
* Optional. When provided, the example is validated against the request schema
|
|
2363
|
+
* at route registration and embedded in the bazaar discovery extension so
|
|
2364
|
+
* indexers can advertise a working sample call.
|
|
2365
|
+
*
|
|
2366
|
+
* For the common case, pass the example directly to `.body(schema, example)` or
|
|
2367
|
+
* `.query(schema, example)` instead.
|
|
2116
2368
|
*
|
|
2117
2369
|
* @example
|
|
2118
2370
|
* ```ts
|
|
@@ -2132,10 +2384,11 @@ var RouteBuilder = class {
|
|
|
2132
2384
|
/**
|
|
2133
2385
|
* Provide a conforming example of the response output.
|
|
2134
2386
|
*
|
|
2135
|
-
*
|
|
2136
|
-
*
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2387
|
+
* Optional. When provided, the example is validated against the output schema
|
|
2388
|
+
* at route registration and embedded in the bazaar discovery extension so
|
|
2389
|
+
* indexers can advertise the response shape.
|
|
2390
|
+
*
|
|
2391
|
+
* For the common case, pass the example directly to `.output(schema, example)` instead.
|
|
2139
2392
|
*
|
|
2140
2393
|
* Accepts any JSON value (objects, arrays, or primitives) — top-level array
|
|
2141
2394
|
* or primitive responses (e.g. `z.array(...)`) are supported alongside the
|
|
@@ -2208,15 +2461,39 @@ var RouteBuilder = class {
|
|
|
2208
2461
|
return next;
|
|
2209
2462
|
}
|
|
2210
2463
|
// -------------------------------------------------------------------------
|
|
2464
|
+
// Settlement lifecycle
|
|
2465
|
+
// -------------------------------------------------------------------------
|
|
2466
|
+
/**
|
|
2467
|
+
* Add route-specific settlement hooks.
|
|
2468
|
+
*
|
|
2469
|
+
* `beforeSettle` runs after a successful handler response but before
|
|
2470
|
+
* router-controlled settlement/broadcast, so it can still prevent the charge
|
|
2471
|
+
* for x402 and MPP transaction-payload flows. `afterSettle` runs after
|
|
2472
|
+
* settlement and is intended for durable ledgers or app-owned refund queues.
|
|
2473
|
+
*/
|
|
2474
|
+
settlement(lifecycle) {
|
|
2475
|
+
const next = this.fork();
|
|
2476
|
+
next._settlement = lifecycle;
|
|
2477
|
+
return next;
|
|
2478
|
+
}
|
|
2479
|
+
// -------------------------------------------------------------------------
|
|
2211
2480
|
// Terminal method
|
|
2212
2481
|
// -------------------------------------------------------------------------
|
|
2213
2482
|
handler(fn) {
|
|
2214
2483
|
const handlerFn = fn;
|
|
2484
|
+
if (!this._authMode) {
|
|
2485
|
+
throw new Error(
|
|
2486
|
+
`route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2215
2489
|
if (this._validateFn && !this._bodySchema) {
|
|
2216
2490
|
throw new Error(
|
|
2217
2491
|
`route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
|
|
2218
2492
|
);
|
|
2219
2493
|
}
|
|
2494
|
+
if (this._settlement && !this._pricing) {
|
|
2495
|
+
throw new Error(`route '${this._key}': .settlement() requires a paid route`);
|
|
2496
|
+
}
|
|
2220
2497
|
validateExamples(
|
|
2221
2498
|
this._key,
|
|
2222
2499
|
this._bodySchema,
|
|
@@ -2248,6 +2525,7 @@ var RouteBuilder = class {
|
|
|
2248
2525
|
providerName: this._providerName,
|
|
2249
2526
|
providerConfig: this._providerConfig,
|
|
2250
2527
|
validateFn: this._validateFn,
|
|
2528
|
+
settlement: this._settlement,
|
|
2251
2529
|
mppInfo: this._mppInfo
|
|
2252
2530
|
};
|
|
2253
2531
|
this._registry.register(entry);
|
|
@@ -2384,6 +2662,7 @@ function toDiscoveryResource(method, url, mode) {
|
|
|
2384
2662
|
|
|
2385
2663
|
// src/discovery/openapi.ts
|
|
2386
2664
|
var import_server4 = require("next/server");
|
|
2665
|
+
init_constants();
|
|
2387
2666
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
2388
2667
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
2389
2668
|
let cached = null;
|
|
@@ -2525,7 +2804,7 @@ function toProtocolObject(protocol, mppInfo) {
|
|
|
2525
2804
|
mpp: {
|
|
2526
2805
|
method: mppInfo?.method ?? "tempo",
|
|
2527
2806
|
intent: mppInfo?.intent ?? "charge",
|
|
2528
|
-
currency: mppInfo?.currency ??
|
|
2807
|
+
currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
|
|
2529
2808
|
}
|
|
2530
2809
|
};
|
|
2531
2810
|
}
|
|
@@ -2590,61 +2869,319 @@ function createLlmsTxtHandler(discovery) {
|
|
|
2590
2869
|
|
|
2591
2870
|
// src/index.ts
|
|
2592
2871
|
init_x402_config();
|
|
2872
|
+
init_constants();
|
|
2873
|
+
|
|
2874
|
+
// src/config.ts
|
|
2875
|
+
init_constants();
|
|
2593
2876
|
init_evm();
|
|
2594
2877
|
init_solana();
|
|
2878
|
+
init_x402_config();
|
|
2879
|
+
var RouterConfigError = class extends Error {
|
|
2880
|
+
issues;
|
|
2881
|
+
constructor(issues) {
|
|
2882
|
+
super(formatRouterConfigIssues(issues));
|
|
2883
|
+
this.name = "RouterConfigError";
|
|
2884
|
+
this.issues = issues;
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
function validateRouterConfig(config, options = {}) {
|
|
2888
|
+
const issues = getRouterConfigIssues(config, options);
|
|
2889
|
+
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
2890
|
+
}
|
|
2891
|
+
function getRouterConfigIssues(config, options = {}) {
|
|
2892
|
+
const env = options.env ?? process.env;
|
|
2893
|
+
const issues = [];
|
|
2894
|
+
const protocols = config.protocols ?? ["x402"];
|
|
2895
|
+
if (!config.baseUrl) {
|
|
2896
|
+
issues.push({
|
|
2897
|
+
code: "missing_base_url",
|
|
2898
|
+
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.'
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
if (config.protocols && config.protocols.length === 0) {
|
|
2902
|
+
issues.push({
|
|
2903
|
+
code: "empty_protocols",
|
|
2904
|
+
message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
if (protocols.includes("x402")) {
|
|
2908
|
+
issues.push(...validateX402Config(config, env, options));
|
|
2909
|
+
}
|
|
2910
|
+
if (protocols.includes("mpp")) {
|
|
2911
|
+
issues.push(...validateMppConfig(config, env));
|
|
2912
|
+
}
|
|
2913
|
+
return issues;
|
|
2914
|
+
}
|
|
2915
|
+
function formatRouterConfigIssues(issues) {
|
|
2916
|
+
return issues.map((issue) => issue.message).join("\n");
|
|
2917
|
+
}
|
|
2918
|
+
function mppFromEnv(env, options = {}) {
|
|
2919
|
+
const secretKey = env.MPP_SECRET_KEY;
|
|
2920
|
+
const currency = env.MPP_CURRENCY;
|
|
2921
|
+
const rpcUrl = env.TEMPO_RPC_URL;
|
|
2922
|
+
const feePayerKey = options.feePayerKey ?? env.MPP_FEE_PAYER_KEY;
|
|
2923
|
+
const feePayerKeySource = options.feePayerKey !== void 0 ? "feePayerKey" : "MPP_FEE_PAYER_KEY";
|
|
2924
|
+
const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
|
|
2925
|
+
if (!hasAnyMppEnv) return void 0;
|
|
2926
|
+
const missing = [
|
|
2927
|
+
secretKey ? null : "MPP_SECRET_KEY",
|
|
2928
|
+
currency ? null : "MPP_CURRENCY",
|
|
2929
|
+
rpcUrl ? null : "TEMPO_RPC_URL"
|
|
2930
|
+
].filter(Boolean);
|
|
2931
|
+
if (missing.length > 0) {
|
|
2932
|
+
throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
|
|
2933
|
+
}
|
|
2934
|
+
if (!isEvmAddress(currency)) {
|
|
2935
|
+
throw new Error("MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address");
|
|
2936
|
+
}
|
|
2937
|
+
if (options.recipient && !isEvmAddress(options.recipient)) {
|
|
2938
|
+
throw new Error("MPP recipient must be a 0x-prefixed EVM address");
|
|
2939
|
+
}
|
|
2940
|
+
if (feePayerKey && !isEvmPrivateKey(feePayerKey)) {
|
|
2941
|
+
throw new Error(`${feePayerKeySource} must be a 0x-prefixed 32-byte EVM private key`);
|
|
2942
|
+
}
|
|
2943
|
+
return {
|
|
2944
|
+
secretKey,
|
|
2945
|
+
currency,
|
|
2946
|
+
rpcUrl,
|
|
2947
|
+
...options.recipient ? { recipient: options.recipient } : {},
|
|
2948
|
+
...feePayerKey ? { feePayerKey } : {},
|
|
2949
|
+
...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
function x402AcceptsFromEnv(env, options = {}) {
|
|
2953
|
+
const payeeEnv = options.payeeEnv ?? "X402_WALLET_ADDRESS";
|
|
2954
|
+
const solanaPayeeEnv = options.solanaPayeeEnv ?? "SOLANA_PAYEE_ADDRESS";
|
|
2955
|
+
const payeeAddress = options.payeeAddress ?? env[payeeEnv];
|
|
2956
|
+
if (!payeeAddress) {
|
|
2957
|
+
throw new Error(`${payeeEnv} is required to build x402 accepts`);
|
|
2958
|
+
}
|
|
2959
|
+
const accepts = [
|
|
2960
|
+
{
|
|
2961
|
+
scheme: "exact",
|
|
2962
|
+
network: options.network ?? BASE_NETWORK,
|
|
2963
|
+
payTo: payeeAddress
|
|
2964
|
+
}
|
|
2965
|
+
];
|
|
2966
|
+
const solanaPayeeAddress = options.solanaPayeeAddress ?? env[solanaPayeeEnv];
|
|
2967
|
+
if (solanaPayeeAddress) {
|
|
2968
|
+
accepts.push({
|
|
2969
|
+
scheme: "exact",
|
|
2970
|
+
network: SOLANA_MAINNET_NETWORK,
|
|
2971
|
+
payTo: solanaPayeeAddress
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
return accepts;
|
|
2975
|
+
}
|
|
2976
|
+
function paidOptionsForProtocols(protocols) {
|
|
2977
|
+
return { protocols: [...protocols] };
|
|
2978
|
+
}
|
|
2979
|
+
function validateX402Config(config, env, options) {
|
|
2980
|
+
const issues = [];
|
|
2981
|
+
const accepts = getConfiguredX402Accepts(config);
|
|
2982
|
+
if (accepts.length === 0) {
|
|
2983
|
+
issues.push({
|
|
2984
|
+
code: "missing_x402_accepts",
|
|
2985
|
+
protocol: "x402",
|
|
2986
|
+
message: "x402 requires at least one accept configuration."
|
|
2987
|
+
});
|
|
2988
|
+
return issues;
|
|
2989
|
+
}
|
|
2990
|
+
const acceptWithoutNetwork = accepts.find((accept) => !accept.network);
|
|
2991
|
+
if (acceptWithoutNetwork) {
|
|
2992
|
+
issues.push({
|
|
2993
|
+
code: "missing_x402_network",
|
|
2994
|
+
protocol: "x402",
|
|
2995
|
+
message: "x402 accepts require a network."
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
const unsupported = accepts.find(
|
|
2999
|
+
(accept) => accept.network && !isSupportedX402Network(accept.network)
|
|
3000
|
+
);
|
|
3001
|
+
if (unsupported) {
|
|
3002
|
+
issues.push({
|
|
3003
|
+
code: "unsupported_x402_network",
|
|
3004
|
+
protocol: "x402",
|
|
3005
|
+
message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
const missingAsset = accepts.find(
|
|
3009
|
+
(accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
|
|
3010
|
+
);
|
|
3011
|
+
if (missingAsset) {
|
|
3012
|
+
issues.push({
|
|
3013
|
+
code: "missing_x402_asset",
|
|
3014
|
+
protocol: "x402",
|
|
3015
|
+
message: "non-exact x402 accepts require an asset."
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
const invalidDecimals = accepts.find(
|
|
3019
|
+
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
3020
|
+
);
|
|
3021
|
+
if (invalidDecimals) {
|
|
3022
|
+
issues.push({
|
|
3023
|
+
code: "invalid_x402_decimals",
|
|
3024
|
+
protocol: "x402",
|
|
3025
|
+
message: "x402 accept decimals must be a non-negative integer."
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
3029
|
+
issues.push({
|
|
3030
|
+
code: "missing_x402_payee",
|
|
3031
|
+
protocol: "x402",
|
|
3032
|
+
message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
const placeholder = findPlaceholderPayee([
|
|
3036
|
+
config.payeeAddress,
|
|
3037
|
+
...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
|
|
3038
|
+
]);
|
|
3039
|
+
if (placeholder) {
|
|
3040
|
+
issues.push({
|
|
3041
|
+
code: "placeholder_payee",
|
|
3042
|
+
protocol: "x402",
|
|
3043
|
+
message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
if (options.requireCdpKeys !== false && usesDefaultEvmFacilitator(config)) {
|
|
3047
|
+
const missing = [
|
|
3048
|
+
env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
|
|
3049
|
+
env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
|
|
3050
|
+
].filter(Boolean);
|
|
3051
|
+
if (missing.length > 0) {
|
|
3052
|
+
issues.push({
|
|
3053
|
+
code: "missing_cdp_keys",
|
|
3054
|
+
protocol: "x402",
|
|
3055
|
+
message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
return issues;
|
|
3060
|
+
}
|
|
3061
|
+
function validateMppConfig(config, env) {
|
|
3062
|
+
const issues = [];
|
|
3063
|
+
const mpp = config.mpp;
|
|
3064
|
+
if (!mpp) {
|
|
3065
|
+
return [
|
|
3066
|
+
{
|
|
3067
|
+
code: "missing_mpp_config",
|
|
3068
|
+
protocol: "mpp",
|
|
3069
|
+
message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
3070
|
+
}
|
|
3071
|
+
];
|
|
3072
|
+
}
|
|
3073
|
+
if (!mpp.secretKey) {
|
|
3074
|
+
issues.push({
|
|
3075
|
+
code: "missing_mpp_secret_key",
|
|
3076
|
+
protocol: "mpp",
|
|
3077
|
+
message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
if (!mpp.currency) {
|
|
3081
|
+
issues.push({
|
|
3082
|
+
code: "missing_mpp_currency",
|
|
3083
|
+
protocol: "mpp",
|
|
3084
|
+
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
3085
|
+
});
|
|
3086
|
+
} else if (!isEvmAddress(mpp.currency)) {
|
|
3087
|
+
issues.push({
|
|
3088
|
+
code: "invalid_mpp_currency",
|
|
3089
|
+
protocol: "mpp",
|
|
3090
|
+
message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
3093
|
+
const mppRecipient = mpp.recipient ?? config.payeeAddress;
|
|
3094
|
+
if (!mppRecipient) {
|
|
3095
|
+
issues.push({
|
|
3096
|
+
code: "missing_mpp_recipient",
|
|
3097
|
+
protocol: "mpp",
|
|
3098
|
+
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
3099
|
+
});
|
|
3100
|
+
} else if (!isEvmAddress(mppRecipient)) {
|
|
3101
|
+
issues.push({
|
|
3102
|
+
code: "invalid_mpp_recipient",
|
|
3103
|
+
protocol: "mpp",
|
|
3104
|
+
message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
3108
|
+
if (placeholder) {
|
|
3109
|
+
issues.push({
|
|
3110
|
+
code: "placeholder_payee",
|
|
3111
|
+
protocol: "mpp",
|
|
3112
|
+
message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
|
|
3116
|
+
issues.push({
|
|
3117
|
+
code: "missing_mpp_rpc_url",
|
|
3118
|
+
protocol: "mpp",
|
|
3119
|
+
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
|
|
3123
|
+
issues.push({
|
|
3124
|
+
code: "invalid_mpp_fee_payer_key",
|
|
3125
|
+
protocol: "mpp",
|
|
3126
|
+
message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
|
|
3130
|
+
issues.push({
|
|
3131
|
+
code: "missing_mpp_default_store_env",
|
|
3132
|
+
protocol: "mpp",
|
|
3133
|
+
message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
return issues;
|
|
3137
|
+
}
|
|
3138
|
+
function usesDefaultEvmFacilitator(config) {
|
|
3139
|
+
return getConfiguredX402Networks(config).some(
|
|
3140
|
+
(network) => typeof network === "string" && isEvmNetwork(network)
|
|
3141
|
+
) && config.x402?.facilitators?.evm === void 0;
|
|
3142
|
+
}
|
|
3143
|
+
function isSupportedX402Network(network) {
|
|
3144
|
+
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
3145
|
+
}
|
|
3146
|
+
function isEvmAddress(value) {
|
|
3147
|
+
return /^0x[a-fA-F0-9]{40}$/.test(value);
|
|
3148
|
+
}
|
|
3149
|
+
function isEvmPrivateKey(value) {
|
|
3150
|
+
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
3151
|
+
}
|
|
3152
|
+
function findPlaceholderPayee(values) {
|
|
3153
|
+
return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// src/index.ts
|
|
3157
|
+
init_constants();
|
|
2595
3158
|
function createRouter(config) {
|
|
2596
3159
|
const registry = new RouteRegistry();
|
|
2597
3160
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
2598
3161
|
const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
|
|
2599
|
-
const network = config.network ??
|
|
3162
|
+
const network = config.network ?? BASE_NETWORK;
|
|
2600
3163
|
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}`);
|
|
3164
|
+
const configIssues = getRouterConfigIssues(config, {
|
|
3165
|
+
requireCdpKeys: process.env.NODE_ENV === "production"
|
|
3166
|
+
});
|
|
3167
|
+
const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
|
|
3168
|
+
if (baseUrlIssue) throw new RouterConfigError([baseUrlIssue]);
|
|
3169
|
+
const emptyProtocolsIssue = configIssues.find((issue) => issue.code === "empty_protocols");
|
|
3170
|
+
if (emptyProtocolsIssue) throw new RouterConfigError([emptyProtocolsIssue]);
|
|
3171
|
+
const protocolConfigIssues = configIssues.filter(
|
|
3172
|
+
(issue) => issue.code !== "missing_base_url" && issue.code !== "empty_protocols"
|
|
3173
|
+
);
|
|
3174
|
+
const x402ConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "x402");
|
|
3175
|
+
const mppConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "mpp");
|
|
3176
|
+
const x402ConfigError = x402ConfigIssues.length > 0 ? formatRouterConfigIssues(x402ConfigIssues) : void 0;
|
|
3177
|
+
const mppConfigError = mppConfigIssues.length > 0 ? formatRouterConfigIssues(mppConfigIssues) : void 0;
|
|
3178
|
+
if (protocolConfigIssues.length > 0) {
|
|
3179
|
+
for (const issue of protocolConfigIssues) console.error(`[router] ${issue.message}`);
|
|
2644
3180
|
if (process.env.NODE_ENV === "production") {
|
|
2645
|
-
throw new
|
|
3181
|
+
throw new RouterConfigError(protocolConfigIssues);
|
|
2646
3182
|
}
|
|
2647
3183
|
}
|
|
3184
|
+
const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2648
3185
|
if (config.plugin?.init) {
|
|
2649
3186
|
try {
|
|
2650
3187
|
const result = config.plugin.init({ origin: resolvedBaseUrl });
|
|
@@ -2662,6 +3199,7 @@ function createRouter(config) {
|
|
|
2662
3199
|
nonceStore,
|
|
2663
3200
|
entitlementStore,
|
|
2664
3201
|
payeeAddress: config.payeeAddress ?? "",
|
|
3202
|
+
mppRecipient: config.mpp?.recipient ?? config.payeeAddress,
|
|
2665
3203
|
network,
|
|
2666
3204
|
x402FacilitatorsByNetwork: void 0,
|
|
2667
3205
|
x402Accepts,
|
|
@@ -2789,9 +3327,6 @@ function createRouter(config) {
|
|
|
2789
3327
|
registry
|
|
2790
3328
|
};
|
|
2791
3329
|
}
|
|
2792
|
-
function isSupportedX402Network(network) {
|
|
2793
|
-
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2794
|
-
}
|
|
2795
3330
|
function normalizePath(path) {
|
|
2796
3331
|
let normalized = path.trim();
|
|
2797
3332
|
normalized = normalized.replace(/^\/+/, "");
|
|
@@ -2800,15 +3335,26 @@ function normalizePath(path) {
|
|
|
2800
3335
|
}
|
|
2801
3336
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2802
3337
|
0 && (module.exports = {
|
|
3338
|
+
BASE_NETWORK,
|
|
2803
3339
|
HttpError,
|
|
2804
3340
|
MemoryEntitlementStore,
|
|
2805
3341
|
MemoryNonceStore,
|
|
2806
3342
|
RouteBuilder,
|
|
2807
3343
|
RouteRegistry,
|
|
3344
|
+
RouterConfigError,
|
|
2808
3345
|
SIWX_CHALLENGE_EXPIRY_MS,
|
|
2809
3346
|
SIWX_ERROR_MESSAGES,
|
|
3347
|
+
SOLANA_MAINNET_NETWORK,
|
|
3348
|
+
TEMPO_USDC_CURRENCY,
|
|
3349
|
+
ZERO_EVM_ADDRESS,
|
|
2810
3350
|
consolePlugin,
|
|
2811
3351
|
createRedisEntitlementStore,
|
|
2812
3352
|
createRedisNonceStore,
|
|
2813
|
-
createRouter
|
|
3353
|
+
createRouter,
|
|
3354
|
+
formatRouterConfigIssues,
|
|
3355
|
+
getRouterConfigIssues,
|
|
3356
|
+
mppFromEnv,
|
|
3357
|
+
paidOptionsForProtocols,
|
|
3358
|
+
validateRouterConfig,
|
|
3359
|
+
x402AcceptsFromEnv
|
|
2814
3360
|
});
|