@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.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
|
|
|
@@ -407,7 +420,7 @@ import { NextResponse as NextResponse2 } from "next/server";
|
|
|
407
420
|
|
|
408
421
|
// src/auth/normalize-wallet.ts
|
|
409
422
|
function normalizeWalletAddress(address) {
|
|
410
|
-
return
|
|
423
|
+
return /^0x/i.test(address) ? address.toLowerCase() : address;
|
|
411
424
|
}
|
|
412
425
|
|
|
413
426
|
// src/plugin.ts
|
|
@@ -572,12 +585,13 @@ var HttpError = class extends Error {
|
|
|
572
585
|
};
|
|
573
586
|
|
|
574
587
|
// src/handler.ts
|
|
575
|
-
async function safeCallHandler(handler, ctx) {
|
|
588
|
+
async function safeCallHandler(handler, ctx, options = {}) {
|
|
576
589
|
try {
|
|
577
590
|
const result = await handler(ctx);
|
|
578
591
|
if (result instanceof Response) return result;
|
|
579
592
|
return NextResponse.json(result);
|
|
580
593
|
} catch (error) {
|
|
594
|
+
options.onError?.(error);
|
|
581
595
|
const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
|
|
582
596
|
const message = error instanceof Error ? error.message : "Internal error";
|
|
583
597
|
return NextResponse.json({ success: false, error: message }, { status });
|
|
@@ -700,6 +714,9 @@ async function verifyX402Payment(opts) {
|
|
|
700
714
|
throw err;
|
|
701
715
|
}
|
|
702
716
|
if (!verify.isValid) return invalidPaymentVerification();
|
|
717
|
+
if (typeof verify.payer !== "string" || verify.payer.length === 0) {
|
|
718
|
+
throw new Error("x402 verification succeeded without a payer address");
|
|
719
|
+
}
|
|
703
720
|
return {
|
|
704
721
|
valid: true,
|
|
705
722
|
payer: verify.payer,
|
|
@@ -990,6 +1007,21 @@ function getRequirementNetwork(requirements, fallback) {
|
|
|
990
1007
|
const network = requirements?.network;
|
|
991
1008
|
return typeof network === "string" ? network : fallback;
|
|
992
1009
|
}
|
|
1010
|
+
function getRequirementRecipient(requirements) {
|
|
1011
|
+
const payTo = requirements?.payTo;
|
|
1012
|
+
return typeof payTo === "string" ? payTo : void 0;
|
|
1013
|
+
}
|
|
1014
|
+
function errorStatus(error, fallback) {
|
|
1015
|
+
const status = error?.status;
|
|
1016
|
+
return typeof status === "number" ? status : fallback;
|
|
1017
|
+
}
|
|
1018
|
+
function errorMessage(error, fallback) {
|
|
1019
|
+
return error instanceof Error ? error.message : fallback;
|
|
1020
|
+
}
|
|
1021
|
+
function handlerFailureError(response) {
|
|
1022
|
+
const message = response.statusText || `Handler returned HTTP ${response.status}`;
|
|
1023
|
+
return Object.assign(new Error(message), { status: response.status });
|
|
1024
|
+
}
|
|
993
1025
|
function siwxSignatureType(network) {
|
|
994
1026
|
return network.startsWith("solana:") ? "ed25519" : "eip191";
|
|
995
1027
|
}
|
|
@@ -1008,7 +1040,7 @@ function getSupportedChains(x402Accepts, fallbackNetwork) {
|
|
|
1008
1040
|
return chains;
|
|
1009
1041
|
}
|
|
1010
1042
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
1011
|
-
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
|
|
1043
|
+
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody, payment) {
|
|
1012
1044
|
const ctx = {
|
|
1013
1045
|
body: parsedBody,
|
|
1014
1046
|
query: parseQuery(request, routeEntry),
|
|
@@ -1016,6 +1048,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1016
1048
|
requestId: meta.requestId,
|
|
1017
1049
|
route: routeEntry.key,
|
|
1018
1050
|
wallet,
|
|
1051
|
+
payment,
|
|
1019
1052
|
account,
|
|
1020
1053
|
alert(level, message, alertMeta) {
|
|
1021
1054
|
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
@@ -1028,11 +1061,20 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1028
1061
|
setVerifiedWallet: (addr) => pluginCtx.setVerifiedWallet(addr)
|
|
1029
1062
|
};
|
|
1030
1063
|
let rawResult;
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1064
|
+
let handlerError;
|
|
1065
|
+
const response = await safeCallHandler(
|
|
1066
|
+
async (c) => {
|
|
1067
|
+
rawResult = await handler(c);
|
|
1068
|
+
return rawResult;
|
|
1069
|
+
},
|
|
1070
|
+
ctx,
|
|
1071
|
+
{
|
|
1072
|
+
onError(error) {
|
|
1073
|
+
handlerError = error;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
);
|
|
1077
|
+
return { response, rawResult, handlerError };
|
|
1036
1078
|
}
|
|
1037
1079
|
function finalize(response, rawResult, meta, pluginCtx, requestBody) {
|
|
1038
1080
|
fireProviderQuota(routeEntry, response, rawResult, deps, pluginCtx);
|
|
@@ -1043,6 +1085,87 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1043
1085
|
firePluginResponse(deps, pluginCtx, meta, response, requestBody);
|
|
1044
1086
|
return response;
|
|
1045
1087
|
}
|
|
1088
|
+
function settlementContext(scope) {
|
|
1089
|
+
return {
|
|
1090
|
+
route: routeEntry.key,
|
|
1091
|
+
request: scope.request,
|
|
1092
|
+
body: scope.parsedBody,
|
|
1093
|
+
wallet: scope.wallet,
|
|
1094
|
+
account: scope.account,
|
|
1095
|
+
payment: scope.payment,
|
|
1096
|
+
response: scope.response,
|
|
1097
|
+
result: scope.rawResult
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
async function runBeforeSettle(scope) {
|
|
1101
|
+
const hook = routeEntry.settlement?.beforeSettle;
|
|
1102
|
+
if (!hook) return null;
|
|
1103
|
+
try {
|
|
1104
|
+
await hook(settlementContext(scope));
|
|
1105
|
+
return null;
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
return fail(
|
|
1108
|
+
errorStatus(error, 500),
|
|
1109
|
+
errorMessage(error, "Pre-settlement validation failed"),
|
|
1110
|
+
scope.meta,
|
|
1111
|
+
scope.pluginCtx,
|
|
1112
|
+
scope.parsedBody
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
async function runSettlementError(scope, error, phase) {
|
|
1117
|
+
const hook = routeEntry.settlement?.onSettlementError;
|
|
1118
|
+
if (!hook) return;
|
|
1119
|
+
try {
|
|
1120
|
+
await hook({
|
|
1121
|
+
...settlementContext(scope),
|
|
1122
|
+
error,
|
|
1123
|
+
phase
|
|
1124
|
+
});
|
|
1125
|
+
} catch (hookError) {
|
|
1126
|
+
const message = errorMessage(hookError, "Settlement error hook failed");
|
|
1127
|
+
console.error(`[router] ${routeEntry.key}: onSettlementError failed: ${message}`);
|
|
1128
|
+
firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
|
|
1129
|
+
level: "error",
|
|
1130
|
+
message: `Settlement error hook failed: ${message}`,
|
|
1131
|
+
route: routeEntry.key
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async function runAfterSettle(scope) {
|
|
1136
|
+
const hook = routeEntry.settlement?.afterSettle;
|
|
1137
|
+
if (!hook) return;
|
|
1138
|
+
try {
|
|
1139
|
+
await hook(settlementContext(scope));
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
const message = errorMessage(error, "Post-settlement hook failed");
|
|
1142
|
+
console.error(`[router] ${routeEntry.key}: afterSettle failed: ${message}`);
|
|
1143
|
+
firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
|
|
1144
|
+
level: "error",
|
|
1145
|
+
message: `Post-settlement hook failed: ${message}`,
|
|
1146
|
+
route: routeEntry.key
|
|
1147
|
+
});
|
|
1148
|
+
await runSettlementError(scope, error, "afterSettle");
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async function runSettledHandlerError(scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
|
|
1152
|
+
const hook = routeEntry.settlement?.onSettledHandlerError;
|
|
1153
|
+
if (!hook) return;
|
|
1154
|
+
try {
|
|
1155
|
+
await hook({
|
|
1156
|
+
...settlementContext(scope),
|
|
1157
|
+
error
|
|
1158
|
+
});
|
|
1159
|
+
} catch (hookError) {
|
|
1160
|
+
const message = errorMessage(hookError, "Settled handler error hook failed");
|
|
1161
|
+
console.error(`[router] ${routeEntry.key}: onSettledHandlerError failed: ${message}`);
|
|
1162
|
+
firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
|
|
1163
|
+
level: "error",
|
|
1164
|
+
message: `Settled handler error hook failed: ${message}`,
|
|
1165
|
+
route: routeEntry.key
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1046
1169
|
return async (request) => {
|
|
1047
1170
|
await deps.initPromise;
|
|
1048
1171
|
const meta = buildMeta(request, routeEntry);
|
|
@@ -1068,7 +1191,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1068
1191
|
pluginCtx,
|
|
1069
1192
|
wallet,
|
|
1070
1193
|
account2,
|
|
1071
|
-
body2.data
|
|
1194
|
+
body2.data,
|
|
1195
|
+
null
|
|
1072
1196
|
);
|
|
1073
1197
|
finalize(response, rawResult, meta, pluginCtx, body2.data);
|
|
1074
1198
|
return response;
|
|
@@ -1297,18 +1421,9 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1297
1421
|
return fail(status, message, meta, pluginCtx, body.data);
|
|
1298
1422
|
}
|
|
1299
1423
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
} catch (err) {
|
|
1304
|
-
return fail(
|
|
1305
|
-
err.status ?? 500,
|
|
1306
|
-
err instanceof Error ? err.message : "Price resolution failed",
|
|
1307
|
-
meta,
|
|
1308
|
-
pluginCtx,
|
|
1309
|
-
body.data
|
|
1310
|
-
);
|
|
1311
|
-
}
|
|
1424
|
+
const priceResult = await resolveDynamicPrice(body.data, routeEntry, deps, pluginCtx, meta);
|
|
1425
|
+
if ("error" in priceResult) return priceResult.error;
|
|
1426
|
+
const price = priceResult.price;
|
|
1312
1427
|
if (!routeEntry.protocols.includes(protocol)) {
|
|
1313
1428
|
const accepted = routeEntry.protocols.join(", ") || "none";
|
|
1314
1429
|
console.warn(
|
|
@@ -1345,7 +1460,16 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1345
1460
|
return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
|
|
1346
1461
|
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
1347
1462
|
const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
|
|
1463
|
+
const matchedRecipient = getRequirementRecipient(verifyRequirements);
|
|
1348
1464
|
const wallet = normalizeWalletAddress(verify.payer);
|
|
1465
|
+
const payment = {
|
|
1466
|
+
protocol: "x402",
|
|
1467
|
+
status: "verified",
|
|
1468
|
+
payer: wallet,
|
|
1469
|
+
amount: price,
|
|
1470
|
+
network: matchedNetwork,
|
|
1471
|
+
...matchedRecipient ? { recipient: matchedRecipient } : {}
|
|
1472
|
+
};
|
|
1349
1473
|
pluginCtx.setVerifiedWallet(wallet);
|
|
1350
1474
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
1351
1475
|
protocol: "x402",
|
|
@@ -1353,15 +1477,30 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1353
1477
|
amount: price,
|
|
1354
1478
|
network: matchedNetwork
|
|
1355
1479
|
});
|
|
1356
|
-
const { response, rawResult } = await invoke(
|
|
1480
|
+
const { response, rawResult, handlerError } = await invoke(
|
|
1357
1481
|
request,
|
|
1358
1482
|
meta,
|
|
1359
1483
|
pluginCtx,
|
|
1360
1484
|
wallet,
|
|
1361
1485
|
account,
|
|
1362
|
-
body.data
|
|
1486
|
+
body.data,
|
|
1487
|
+
payment
|
|
1363
1488
|
);
|
|
1489
|
+
const settleScope = {
|
|
1490
|
+
request,
|
|
1491
|
+
meta,
|
|
1492
|
+
pluginCtx,
|
|
1493
|
+
wallet,
|
|
1494
|
+
account,
|
|
1495
|
+
parsedBody: body.data,
|
|
1496
|
+
payment,
|
|
1497
|
+
response,
|
|
1498
|
+
rawResult,
|
|
1499
|
+
handlerError
|
|
1500
|
+
};
|
|
1364
1501
|
if (response.status < 400) {
|
|
1502
|
+
const validationFailure = await runBeforeSettle(settleScope);
|
|
1503
|
+
if (validationFailure) return validationFailure;
|
|
1365
1504
|
try {
|
|
1366
1505
|
const settle = await settleX402Payment(
|
|
1367
1506
|
deps.x402Server,
|
|
@@ -1387,13 +1526,21 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1387
1526
|
}
|
|
1388
1527
|
response.headers.set("PAYMENT-RESPONSE", settle.encoded);
|
|
1389
1528
|
response.headers.set("Cache-Control", "private");
|
|
1529
|
+
const transaction = String(settle.result?.transaction ?? "");
|
|
1530
|
+
const settledPayment = {
|
|
1531
|
+
...payment,
|
|
1532
|
+
status: "settled",
|
|
1533
|
+
...transaction ? { transaction } : {}
|
|
1534
|
+
};
|
|
1390
1535
|
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
1391
1536
|
protocol: "x402",
|
|
1392
|
-
payer:
|
|
1393
|
-
transaction
|
|
1537
|
+
payer: wallet,
|
|
1538
|
+
transaction,
|
|
1394
1539
|
network: matchedNetwork
|
|
1395
1540
|
});
|
|
1541
|
+
await runAfterSettle({ ...settleScope, payment: settledPayment });
|
|
1396
1542
|
} catch (err) {
|
|
1543
|
+
await runSettlementError(settleScope, err, "settle");
|
|
1397
1544
|
const errObj = err;
|
|
1398
1545
|
console.error("Settlement failed", {
|
|
1399
1546
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -1452,19 +1599,44 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1452
1599
|
amount: price,
|
|
1453
1600
|
network: "tempo:4217"
|
|
1454
1601
|
});
|
|
1455
|
-
const
|
|
1602
|
+
const mppRecipient2 = deps.mppRecipient ?? deps.payeeAddress;
|
|
1603
|
+
const payment2 = {
|
|
1604
|
+
protocol: "mpp",
|
|
1605
|
+
status: "verified",
|
|
1606
|
+
payer: wallet,
|
|
1607
|
+
amount: price,
|
|
1608
|
+
network: "tempo:4217",
|
|
1609
|
+
...mppRecipient2 ? { recipient: mppRecipient2 } : {}
|
|
1610
|
+
};
|
|
1611
|
+
const { response: response2, rawResult: rawResult2, handlerError: handlerError2 } = await invoke(
|
|
1456
1612
|
request,
|
|
1457
1613
|
meta,
|
|
1458
1614
|
pluginCtx,
|
|
1459
1615
|
wallet,
|
|
1460
1616
|
account,
|
|
1461
|
-
body.data
|
|
1617
|
+
body.data,
|
|
1618
|
+
payment2
|
|
1462
1619
|
);
|
|
1620
|
+
const settleScope2 = {
|
|
1621
|
+
request,
|
|
1622
|
+
meta,
|
|
1623
|
+
pluginCtx,
|
|
1624
|
+
wallet,
|
|
1625
|
+
account,
|
|
1626
|
+
parsedBody: body.data,
|
|
1627
|
+
payment: payment2,
|
|
1628
|
+
response: response2,
|
|
1629
|
+
rawResult: rawResult2,
|
|
1630
|
+
handlerError: handlerError2
|
|
1631
|
+
};
|
|
1463
1632
|
if (response2.status < 400) {
|
|
1633
|
+
const validationFailure = await runBeforeSettle(settleScope2);
|
|
1634
|
+
if (validationFailure) return validationFailure;
|
|
1464
1635
|
let mppResult2;
|
|
1465
1636
|
try {
|
|
1466
1637
|
mppResult2 = await deps.mppx.charge({ amount: price })(request);
|
|
1467
1638
|
} catch (err) {
|
|
1639
|
+
await runSettlementError(settleScope2, err, "settle");
|
|
1468
1640
|
const message = err instanceof Error ? err.message : String(err);
|
|
1469
1641
|
console.error(
|
|
1470
1642
|
`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`
|
|
@@ -1493,6 +1665,13 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1493
1665
|
} catch {
|
|
1494
1666
|
}
|
|
1495
1667
|
const detail = rejectReason || "transaction reverted on-chain after handler execution";
|
|
1668
|
+
const settlementError = Object.assign(new Error(detail), {
|
|
1669
|
+
status: 402,
|
|
1670
|
+
detail,
|
|
1671
|
+
mppResult: mppResult2,
|
|
1672
|
+
challenge: mppResult2.challenge
|
|
1673
|
+
});
|
|
1674
|
+
await runSettlementError(settleScope2, settlementError, "settle");
|
|
1496
1675
|
console.error(
|
|
1497
1676
|
`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`
|
|
1498
1677
|
);
|
|
@@ -1530,6 +1709,17 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1530
1709
|
transaction: txHash2,
|
|
1531
1710
|
network: "tempo:4217"
|
|
1532
1711
|
});
|
|
1712
|
+
const settledPayment = {
|
|
1713
|
+
...payment2,
|
|
1714
|
+
status: "settled",
|
|
1715
|
+
...txHash2 ? { transaction: txHash2 } : {},
|
|
1716
|
+
...receiptHeader2 ? { receipt: receiptHeader2 } : {}
|
|
1717
|
+
};
|
|
1718
|
+
await runAfterSettle({
|
|
1719
|
+
...settleScope2,
|
|
1720
|
+
payment: settledPayment,
|
|
1721
|
+
response: receiptResponse
|
|
1722
|
+
});
|
|
1533
1723
|
finalize(receiptResponse, rawResult2, meta, pluginCtx, body.data);
|
|
1534
1724
|
return receiptResponse;
|
|
1535
1725
|
}
|
|
@@ -1585,14 +1775,38 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1585
1775
|
amount: price,
|
|
1586
1776
|
network: "tempo:4217"
|
|
1587
1777
|
});
|
|
1588
|
-
const
|
|
1778
|
+
const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
|
|
1779
|
+
const payment = {
|
|
1780
|
+
protocol: "mpp",
|
|
1781
|
+
status: "settled",
|
|
1782
|
+
payer: wallet,
|
|
1783
|
+
amount: price,
|
|
1784
|
+
network: "tempo:4217",
|
|
1785
|
+
...mppRecipient ? { recipient: mppRecipient } : {},
|
|
1786
|
+
...txHash ? { transaction: txHash } : {},
|
|
1787
|
+
...receiptHeader ? { receipt: receiptHeader } : {}
|
|
1788
|
+
};
|
|
1789
|
+
const { response, rawResult, handlerError } = await invoke(
|
|
1589
1790
|
request,
|
|
1590
1791
|
meta,
|
|
1591
1792
|
pluginCtx,
|
|
1592
1793
|
wallet,
|
|
1593
1794
|
account,
|
|
1594
|
-
body.data
|
|
1795
|
+
body.data,
|
|
1796
|
+
payment
|
|
1595
1797
|
);
|
|
1798
|
+
const settleScope = {
|
|
1799
|
+
request,
|
|
1800
|
+
meta,
|
|
1801
|
+
pluginCtx,
|
|
1802
|
+
wallet,
|
|
1803
|
+
account,
|
|
1804
|
+
parsedBody: body.data,
|
|
1805
|
+
payment,
|
|
1806
|
+
response,
|
|
1807
|
+
rawResult,
|
|
1808
|
+
handlerError
|
|
1809
|
+
};
|
|
1596
1810
|
if (response.status < 400) {
|
|
1597
1811
|
if (routeEntry.siwxEnabled) {
|
|
1598
1812
|
try {
|
|
@@ -1613,9 +1827,11 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
1613
1827
|
transaction: txHash,
|
|
1614
1828
|
network: "tempo:4217"
|
|
1615
1829
|
});
|
|
1830
|
+
await runAfterSettle({ ...settleScope, response: receiptResponse });
|
|
1616
1831
|
finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
|
|
1617
1832
|
return receiptResponse;
|
|
1618
1833
|
}
|
|
1834
|
+
await runSettledHandlerError(settleScope);
|
|
1619
1835
|
finalize(response, rawResult, meta, pluginCtx, body.data);
|
|
1620
1836
|
return response;
|
|
1621
1837
|
}
|
|
@@ -1688,9 +1904,10 @@ async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta)
|
|
|
1688
1904
|
});
|
|
1689
1905
|
return { price: routeEntry.maxPrice };
|
|
1690
1906
|
} else {
|
|
1907
|
+
const message = errorMessage(err, "Price calculation failed");
|
|
1691
1908
|
const errorResponse = NextResponse2.json(
|
|
1692
|
-
{ success: false, error:
|
|
1693
|
-
{ status: 500 }
|
|
1909
|
+
{ success: false, error: message },
|
|
1910
|
+
{ status: errorStatus(err, 500) }
|
|
1694
1911
|
);
|
|
1695
1912
|
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
1696
1913
|
return { error: errorResponse };
|
|
@@ -1863,22 +2080,13 @@ function fireProviderQuota(routeEntry, response, handlerResult, deps, pluginCtx)
|
|
|
1863
2080
|
|
|
1864
2081
|
// src/validate-examples.ts
|
|
1865
2082
|
function validateExamples(key, bodySchema, querySchema, outputSchema, inputExample, hasInputExample, outputExample, hasOutputExample) {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
);
|
|
1870
|
-
}
|
|
1871
|
-
if (querySchema && !hasInputExample) {
|
|
1872
|
-
throw new Error(
|
|
1873
|
-
`route '${key}': .query() requires a matching .inputExample() \u2014 the bazaar discovery extension needs a conforming sample query to advertise.`
|
|
1874
|
-
);
|
|
2083
|
+
const inputSchema = bodySchema ?? querySchema;
|
|
2084
|
+
if (hasInputExample && !inputSchema) {
|
|
2085
|
+
throw new Error(`route '${key}': .inputExample() requires .body() or .query()`);
|
|
1875
2086
|
}
|
|
1876
|
-
if (
|
|
1877
|
-
throw new Error(
|
|
1878
|
-
`route '${key}': .output() requires a matching .outputExample() \u2014 the bazaar discovery extension needs a conforming sample response to advertise.`
|
|
1879
|
-
);
|
|
2087
|
+
if (hasOutputExample && !outputSchema) {
|
|
2088
|
+
throw new Error(`route '${key}': .outputExample() requires .output()`);
|
|
1880
2089
|
}
|
|
1881
|
-
const inputSchema = bodySchema ?? querySchema;
|
|
1882
2090
|
if (inputSchema && hasInputExample) {
|
|
1883
2091
|
const result = inputSchema.safeParse(inputExample);
|
|
1884
2092
|
if (!result.success) {
|
|
@@ -1952,6 +2160,8 @@ var RouteBuilder = class {
|
|
|
1952
2160
|
/** @internal */
|
|
1953
2161
|
_validateFn;
|
|
1954
2162
|
/** @internal */
|
|
2163
|
+
_settlement;
|
|
2164
|
+
/** @internal */
|
|
1955
2165
|
_mppInfo;
|
|
1956
2166
|
constructor(key, registry, deps) {
|
|
1957
2167
|
this._key = key;
|
|
@@ -1965,11 +2175,21 @@ var RouteBuilder = class {
|
|
|
1965
2175
|
return next;
|
|
1966
2176
|
}
|
|
1967
2177
|
paid(pricing, options) {
|
|
2178
|
+
if (this._authMode === "unprotected") {
|
|
2179
|
+
throw new Error(
|
|
2180
|
+
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
if (this._pricing !== void 0) {
|
|
2184
|
+
throw new Error(
|
|
2185
|
+
`route '${this._key}': Cannot call .paid() more than once on the same route.`
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
1968
2188
|
const next = this.fork();
|
|
1969
2189
|
next._authMode = "paid";
|
|
1970
2190
|
next._pricing = pricing;
|
|
1971
2191
|
if (options?.protocols) {
|
|
1972
|
-
next._protocols = options.protocols;
|
|
2192
|
+
next._protocols = [...options.protocols];
|
|
1973
2193
|
} else if (next._protocols.length === 0) {
|
|
1974
2194
|
next._protocols = ["x402"];
|
|
1975
2195
|
}
|
|
@@ -2034,6 +2254,16 @@ var RouteBuilder = class {
|
|
|
2034
2254
|
return next;
|
|
2035
2255
|
}
|
|
2036
2256
|
unprotected() {
|
|
2257
|
+
if (this._authMode && this._authMode !== "unprotected") {
|
|
2258
|
+
throw new Error(
|
|
2259
|
+
`route '${this._key}': Cannot combine .unprotected() and .${this._authMode}() on the same route.`
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
if (this._pricing) {
|
|
2263
|
+
throw new Error(
|
|
2264
|
+
`route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2037
2267
|
const next = this.fork();
|
|
2038
2268
|
next._authMode = "unprotected";
|
|
2039
2269
|
next._protocols = [];
|
|
@@ -2048,32 +2278,43 @@ var RouteBuilder = class {
|
|
|
2048
2278
|
next._providerConfig = config ?? {};
|
|
2049
2279
|
return next;
|
|
2050
2280
|
}
|
|
2051
|
-
|
|
2052
|
-
// Schema methods
|
|
2053
|
-
// -------------------------------------------------------------------------
|
|
2054
|
-
body(schema) {
|
|
2281
|
+
body(schema, example) {
|
|
2055
2282
|
const next = this.fork();
|
|
2056
2283
|
next._bodySchema = schema;
|
|
2284
|
+
if (example !== void 0) {
|
|
2285
|
+
next._inputExample = example;
|
|
2286
|
+
next._hasInputExample = true;
|
|
2287
|
+
}
|
|
2057
2288
|
return next;
|
|
2058
2289
|
}
|
|
2059
|
-
query(schema) {
|
|
2290
|
+
query(schema, example) {
|
|
2060
2291
|
const next = this.fork();
|
|
2061
2292
|
next._querySchema = schema;
|
|
2293
|
+
if (example !== void 0) {
|
|
2294
|
+
next._inputExample = example;
|
|
2295
|
+
next._hasInputExample = true;
|
|
2296
|
+
}
|
|
2062
2297
|
next._method = "GET";
|
|
2063
2298
|
return next;
|
|
2064
2299
|
}
|
|
2065
|
-
output(schema) {
|
|
2300
|
+
output(schema, example) {
|
|
2066
2301
|
const next = this.fork();
|
|
2067
2302
|
next._outputSchema = schema;
|
|
2303
|
+
if (example !== void 0) {
|
|
2304
|
+
next._outputExample = example;
|
|
2305
|
+
next._hasOutputExample = true;
|
|
2306
|
+
}
|
|
2068
2307
|
return next;
|
|
2069
2308
|
}
|
|
2070
2309
|
/**
|
|
2071
2310
|
* Provide a conforming example of the request input (body or query params).
|
|
2072
2311
|
*
|
|
2073
|
-
*
|
|
2074
|
-
*
|
|
2075
|
-
*
|
|
2076
|
-
*
|
|
2312
|
+
* Optional. When provided, the example is validated against the request schema
|
|
2313
|
+
* at route registration and embedded in the bazaar discovery extension so
|
|
2314
|
+
* indexers can advertise a working sample call.
|
|
2315
|
+
*
|
|
2316
|
+
* For the common case, pass the example directly to `.body(schema, example)` or
|
|
2317
|
+
* `.query(schema, example)` instead.
|
|
2077
2318
|
*
|
|
2078
2319
|
* @example
|
|
2079
2320
|
* ```ts
|
|
@@ -2093,10 +2334,11 @@ var RouteBuilder = class {
|
|
|
2093
2334
|
/**
|
|
2094
2335
|
* Provide a conforming example of the response output.
|
|
2095
2336
|
*
|
|
2096
|
-
*
|
|
2097
|
-
*
|
|
2098
|
-
*
|
|
2099
|
-
*
|
|
2337
|
+
* Optional. When provided, the example is validated against the output schema
|
|
2338
|
+
* at route registration and embedded in the bazaar discovery extension so
|
|
2339
|
+
* indexers can advertise the response shape.
|
|
2340
|
+
*
|
|
2341
|
+
* For the common case, pass the example directly to `.output(schema, example)` instead.
|
|
2100
2342
|
*
|
|
2101
2343
|
* Accepts any JSON value (objects, arrays, or primitives) — top-level array
|
|
2102
2344
|
* or primitive responses (e.g. `z.array(...)`) are supported alongside the
|
|
@@ -2169,15 +2411,39 @@ var RouteBuilder = class {
|
|
|
2169
2411
|
return next;
|
|
2170
2412
|
}
|
|
2171
2413
|
// -------------------------------------------------------------------------
|
|
2414
|
+
// Settlement lifecycle
|
|
2415
|
+
// -------------------------------------------------------------------------
|
|
2416
|
+
/**
|
|
2417
|
+
* Add route-specific settlement hooks.
|
|
2418
|
+
*
|
|
2419
|
+
* `beforeSettle` runs after a successful handler response but before
|
|
2420
|
+
* router-controlled settlement/broadcast, so it can still prevent the charge
|
|
2421
|
+
* for x402 and MPP transaction-payload flows. `afterSettle` runs after
|
|
2422
|
+
* settlement and is intended for durable ledgers or app-owned refund queues.
|
|
2423
|
+
*/
|
|
2424
|
+
settlement(lifecycle) {
|
|
2425
|
+
const next = this.fork();
|
|
2426
|
+
next._settlement = lifecycle;
|
|
2427
|
+
return next;
|
|
2428
|
+
}
|
|
2429
|
+
// -------------------------------------------------------------------------
|
|
2172
2430
|
// Terminal method
|
|
2173
2431
|
// -------------------------------------------------------------------------
|
|
2174
2432
|
handler(fn) {
|
|
2175
2433
|
const handlerFn = fn;
|
|
2434
|
+
if (!this._authMode) {
|
|
2435
|
+
throw new Error(
|
|
2436
|
+
`route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2176
2439
|
if (this._validateFn && !this._bodySchema) {
|
|
2177
2440
|
throw new Error(
|
|
2178
2441
|
`route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
|
|
2179
2442
|
);
|
|
2180
2443
|
}
|
|
2444
|
+
if (this._settlement && !this._pricing) {
|
|
2445
|
+
throw new Error(`route '${this._key}': .settlement() requires a paid route`);
|
|
2446
|
+
}
|
|
2181
2447
|
validateExamples(
|
|
2182
2448
|
this._key,
|
|
2183
2449
|
this._bodySchema,
|
|
@@ -2209,6 +2475,7 @@ var RouteBuilder = class {
|
|
|
2209
2475
|
providerName: this._providerName,
|
|
2210
2476
|
providerConfig: this._providerConfig,
|
|
2211
2477
|
validateFn: this._validateFn,
|
|
2478
|
+
settlement: this._settlement,
|
|
2212
2479
|
mppInfo: this._mppInfo
|
|
2213
2480
|
};
|
|
2214
2481
|
this._registry.register(entry);
|
|
@@ -2344,6 +2611,7 @@ function toDiscoveryResource(method, url, mode) {
|
|
|
2344
2611
|
}
|
|
2345
2612
|
|
|
2346
2613
|
// src/discovery/openapi.ts
|
|
2614
|
+
init_constants();
|
|
2347
2615
|
import { NextResponse as NextResponse4 } from "next/server";
|
|
2348
2616
|
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
2349
2617
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
@@ -2486,7 +2754,7 @@ function toProtocolObject(protocol, mppInfo) {
|
|
|
2486
2754
|
mpp: {
|
|
2487
2755
|
method: mppInfo?.method ?? "tempo",
|
|
2488
2756
|
intent: mppInfo?.intent ?? "charge",
|
|
2489
|
-
currency: mppInfo?.currency ??
|
|
2757
|
+
currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
|
|
2490
2758
|
}
|
|
2491
2759
|
};
|
|
2492
2760
|
}
|
|
@@ -2551,61 +2819,319 @@ function createLlmsTxtHandler(discovery) {
|
|
|
2551
2819
|
|
|
2552
2820
|
// src/index.ts
|
|
2553
2821
|
init_x402_config();
|
|
2822
|
+
init_constants();
|
|
2823
|
+
|
|
2824
|
+
// src/config.ts
|
|
2825
|
+
init_constants();
|
|
2554
2826
|
init_evm();
|
|
2555
2827
|
init_solana();
|
|
2828
|
+
init_x402_config();
|
|
2829
|
+
var RouterConfigError = class extends Error {
|
|
2830
|
+
issues;
|
|
2831
|
+
constructor(issues) {
|
|
2832
|
+
super(formatRouterConfigIssues(issues));
|
|
2833
|
+
this.name = "RouterConfigError";
|
|
2834
|
+
this.issues = issues;
|
|
2835
|
+
}
|
|
2836
|
+
};
|
|
2837
|
+
function validateRouterConfig(config, options = {}) {
|
|
2838
|
+
const issues = getRouterConfigIssues(config, options);
|
|
2839
|
+
if (issues.length > 0) throw new RouterConfigError(issues);
|
|
2840
|
+
}
|
|
2841
|
+
function getRouterConfigIssues(config, options = {}) {
|
|
2842
|
+
const env = options.env ?? process.env;
|
|
2843
|
+
const issues = [];
|
|
2844
|
+
const protocols = config.protocols ?? ["x402"];
|
|
2845
|
+
if (!config.baseUrl) {
|
|
2846
|
+
issues.push({
|
|
2847
|
+
code: "missing_base_url",
|
|
2848
|
+
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.'
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
if (config.protocols && config.protocols.length === 0) {
|
|
2852
|
+
issues.push({
|
|
2853
|
+
code: "empty_protocols",
|
|
2854
|
+
message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
if (protocols.includes("x402")) {
|
|
2858
|
+
issues.push(...validateX402Config(config, env, options));
|
|
2859
|
+
}
|
|
2860
|
+
if (protocols.includes("mpp")) {
|
|
2861
|
+
issues.push(...validateMppConfig(config, env));
|
|
2862
|
+
}
|
|
2863
|
+
return issues;
|
|
2864
|
+
}
|
|
2865
|
+
function formatRouterConfigIssues(issues) {
|
|
2866
|
+
return issues.map((issue) => issue.message).join("\n");
|
|
2867
|
+
}
|
|
2868
|
+
function mppFromEnv(env, options = {}) {
|
|
2869
|
+
const secretKey = env.MPP_SECRET_KEY;
|
|
2870
|
+
const currency = env.MPP_CURRENCY;
|
|
2871
|
+
const rpcUrl = env.TEMPO_RPC_URL;
|
|
2872
|
+
const feePayerKey = options.feePayerKey ?? env.MPP_FEE_PAYER_KEY;
|
|
2873
|
+
const feePayerKeySource = options.feePayerKey !== void 0 ? "feePayerKey" : "MPP_FEE_PAYER_KEY";
|
|
2874
|
+
const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
|
|
2875
|
+
if (!hasAnyMppEnv) return void 0;
|
|
2876
|
+
const missing = [
|
|
2877
|
+
secretKey ? null : "MPP_SECRET_KEY",
|
|
2878
|
+
currency ? null : "MPP_CURRENCY",
|
|
2879
|
+
rpcUrl ? null : "TEMPO_RPC_URL"
|
|
2880
|
+
].filter(Boolean);
|
|
2881
|
+
if (missing.length > 0) {
|
|
2882
|
+
throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
|
|
2883
|
+
}
|
|
2884
|
+
if (!isEvmAddress(currency)) {
|
|
2885
|
+
throw new Error("MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address");
|
|
2886
|
+
}
|
|
2887
|
+
if (options.recipient && !isEvmAddress(options.recipient)) {
|
|
2888
|
+
throw new Error("MPP recipient must be a 0x-prefixed EVM address");
|
|
2889
|
+
}
|
|
2890
|
+
if (feePayerKey && !isEvmPrivateKey(feePayerKey)) {
|
|
2891
|
+
throw new Error(`${feePayerKeySource} must be a 0x-prefixed 32-byte EVM private key`);
|
|
2892
|
+
}
|
|
2893
|
+
return {
|
|
2894
|
+
secretKey,
|
|
2895
|
+
currency,
|
|
2896
|
+
rpcUrl,
|
|
2897
|
+
...options.recipient ? { recipient: options.recipient } : {},
|
|
2898
|
+
...feePayerKey ? { feePayerKey } : {},
|
|
2899
|
+
...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
function x402AcceptsFromEnv(env, options = {}) {
|
|
2903
|
+
const payeeEnv = options.payeeEnv ?? "X402_WALLET_ADDRESS";
|
|
2904
|
+
const solanaPayeeEnv = options.solanaPayeeEnv ?? "SOLANA_PAYEE_ADDRESS";
|
|
2905
|
+
const payeeAddress = options.payeeAddress ?? env[payeeEnv];
|
|
2906
|
+
if (!payeeAddress) {
|
|
2907
|
+
throw new Error(`${payeeEnv} is required to build x402 accepts`);
|
|
2908
|
+
}
|
|
2909
|
+
const accepts = [
|
|
2910
|
+
{
|
|
2911
|
+
scheme: "exact",
|
|
2912
|
+
network: options.network ?? BASE_NETWORK,
|
|
2913
|
+
payTo: payeeAddress
|
|
2914
|
+
}
|
|
2915
|
+
];
|
|
2916
|
+
const solanaPayeeAddress = options.solanaPayeeAddress ?? env[solanaPayeeEnv];
|
|
2917
|
+
if (solanaPayeeAddress) {
|
|
2918
|
+
accepts.push({
|
|
2919
|
+
scheme: "exact",
|
|
2920
|
+
network: SOLANA_MAINNET_NETWORK,
|
|
2921
|
+
payTo: solanaPayeeAddress
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
return accepts;
|
|
2925
|
+
}
|
|
2926
|
+
function paidOptionsForProtocols(protocols) {
|
|
2927
|
+
return { protocols: [...protocols] };
|
|
2928
|
+
}
|
|
2929
|
+
function validateX402Config(config, env, options) {
|
|
2930
|
+
const issues = [];
|
|
2931
|
+
const accepts = getConfiguredX402Accepts(config);
|
|
2932
|
+
if (accepts.length === 0) {
|
|
2933
|
+
issues.push({
|
|
2934
|
+
code: "missing_x402_accepts",
|
|
2935
|
+
protocol: "x402",
|
|
2936
|
+
message: "x402 requires at least one accept configuration."
|
|
2937
|
+
});
|
|
2938
|
+
return issues;
|
|
2939
|
+
}
|
|
2940
|
+
const acceptWithoutNetwork = accepts.find((accept) => !accept.network);
|
|
2941
|
+
if (acceptWithoutNetwork) {
|
|
2942
|
+
issues.push({
|
|
2943
|
+
code: "missing_x402_network",
|
|
2944
|
+
protocol: "x402",
|
|
2945
|
+
message: "x402 accepts require a network."
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
const unsupported = accepts.find(
|
|
2949
|
+
(accept) => accept.network && !isSupportedX402Network(accept.network)
|
|
2950
|
+
);
|
|
2951
|
+
if (unsupported) {
|
|
2952
|
+
issues.push({
|
|
2953
|
+
code: "unsupported_x402_network",
|
|
2954
|
+
protocol: "x402",
|
|
2955
|
+
message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
const missingAsset = accepts.find(
|
|
2959
|
+
(accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
|
|
2960
|
+
);
|
|
2961
|
+
if (missingAsset) {
|
|
2962
|
+
issues.push({
|
|
2963
|
+
code: "missing_x402_asset",
|
|
2964
|
+
protocol: "x402",
|
|
2965
|
+
message: "non-exact x402 accepts require an asset."
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
const invalidDecimals = accepts.find(
|
|
2969
|
+
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2970
|
+
);
|
|
2971
|
+
if (invalidDecimals) {
|
|
2972
|
+
issues.push({
|
|
2973
|
+
code: "invalid_x402_decimals",
|
|
2974
|
+
protocol: "x402",
|
|
2975
|
+
message: "x402 accept decimals must be a non-negative integer."
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2979
|
+
issues.push({
|
|
2980
|
+
code: "missing_x402_payee",
|
|
2981
|
+
protocol: "x402",
|
|
2982
|
+
message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
const placeholder = findPlaceholderPayee([
|
|
2986
|
+
config.payeeAddress,
|
|
2987
|
+
...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
|
|
2988
|
+
]);
|
|
2989
|
+
if (placeholder) {
|
|
2990
|
+
issues.push({
|
|
2991
|
+
code: "placeholder_payee",
|
|
2992
|
+
protocol: "x402",
|
|
2993
|
+
message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
2994
|
+
});
|
|
2995
|
+
}
|
|
2996
|
+
if (options.requireCdpKeys !== false && usesDefaultEvmFacilitator(config)) {
|
|
2997
|
+
const missing = [
|
|
2998
|
+
env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
|
|
2999
|
+
env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
|
|
3000
|
+
].filter(Boolean);
|
|
3001
|
+
if (missing.length > 0) {
|
|
3002
|
+
issues.push({
|
|
3003
|
+
code: "missing_cdp_keys",
|
|
3004
|
+
protocol: "x402",
|
|
3005
|
+
message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
return issues;
|
|
3010
|
+
}
|
|
3011
|
+
function validateMppConfig(config, env) {
|
|
3012
|
+
const issues = [];
|
|
3013
|
+
const mpp = config.mpp;
|
|
3014
|
+
if (!mpp) {
|
|
3015
|
+
return [
|
|
3016
|
+
{
|
|
3017
|
+
code: "missing_mpp_config",
|
|
3018
|
+
protocol: "mpp",
|
|
3019
|
+
message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
3020
|
+
}
|
|
3021
|
+
];
|
|
3022
|
+
}
|
|
3023
|
+
if (!mpp.secretKey) {
|
|
3024
|
+
issues.push({
|
|
3025
|
+
code: "missing_mpp_secret_key",
|
|
3026
|
+
protocol: "mpp",
|
|
3027
|
+
message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
if (!mpp.currency) {
|
|
3031
|
+
issues.push({
|
|
3032
|
+
code: "missing_mpp_currency",
|
|
3033
|
+
protocol: "mpp",
|
|
3034
|
+
message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
|
|
3035
|
+
});
|
|
3036
|
+
} else if (!isEvmAddress(mpp.currency)) {
|
|
3037
|
+
issues.push({
|
|
3038
|
+
code: "invalid_mpp_currency",
|
|
3039
|
+
protocol: "mpp",
|
|
3040
|
+
message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
const mppRecipient = mpp.recipient ?? config.payeeAddress;
|
|
3044
|
+
if (!mppRecipient) {
|
|
3045
|
+
issues.push({
|
|
3046
|
+
code: "missing_mpp_recipient",
|
|
3047
|
+
protocol: "mpp",
|
|
3048
|
+
message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
|
|
3049
|
+
});
|
|
3050
|
+
} else if (!isEvmAddress(mppRecipient)) {
|
|
3051
|
+
issues.push({
|
|
3052
|
+
code: "invalid_mpp_recipient",
|
|
3053
|
+
protocol: "mpp",
|
|
3054
|
+
message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
|
|
3058
|
+
if (placeholder) {
|
|
3059
|
+
issues.push({
|
|
3060
|
+
code: "placeholder_payee",
|
|
3061
|
+
protocol: "mpp",
|
|
3062
|
+
message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
|
|
3066
|
+
issues.push({
|
|
3067
|
+
code: "missing_mpp_rpc_url",
|
|
3068
|
+
protocol: "mpp",
|
|
3069
|
+
message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
|
|
3073
|
+
issues.push({
|
|
3074
|
+
code: "invalid_mpp_fee_payer_key",
|
|
3075
|
+
protocol: "mpp",
|
|
3076
|
+
message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
|
|
3080
|
+
issues.push({
|
|
3081
|
+
code: "missing_mpp_default_store_env",
|
|
3082
|
+
protocol: "mpp",
|
|
3083
|
+
message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
return issues;
|
|
3087
|
+
}
|
|
3088
|
+
function usesDefaultEvmFacilitator(config) {
|
|
3089
|
+
return getConfiguredX402Networks(config).some(
|
|
3090
|
+
(network) => typeof network === "string" && isEvmNetwork(network)
|
|
3091
|
+
) && config.x402?.facilitators?.evm === void 0;
|
|
3092
|
+
}
|
|
3093
|
+
function isSupportedX402Network(network) {
|
|
3094
|
+
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
3095
|
+
}
|
|
3096
|
+
function isEvmAddress(value) {
|
|
3097
|
+
return /^0x[a-fA-F0-9]{40}$/.test(value);
|
|
3098
|
+
}
|
|
3099
|
+
function isEvmPrivateKey(value) {
|
|
3100
|
+
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
3101
|
+
}
|
|
3102
|
+
function findPlaceholderPayee(values) {
|
|
3103
|
+
return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// src/index.ts
|
|
3107
|
+
init_constants();
|
|
2556
3108
|
function createRouter(config) {
|
|
2557
3109
|
const registry = new RouteRegistry();
|
|
2558
3110
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
2559
3111
|
const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
|
|
2560
|
-
const network = config.network ??
|
|
3112
|
+
const network = config.network ?? BASE_NETWORK;
|
|
2561
3113
|
const x402Accepts = getConfiguredX402Accepts(config);
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
)
|
|
2571
|
-
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
} else if (x402Accepts.some((accept) => !accept.network)) {
|
|
2579
|
-
x402ConfigError = "x402 accepts require a network.";
|
|
2580
|
-
} else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
|
|
2581
|
-
const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
|
|
2582
|
-
x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
|
|
2583
|
-
} else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
|
|
2584
|
-
x402ConfigError = "non-exact x402 accepts require an asset.";
|
|
2585
|
-
} else if (x402Accepts.some(
|
|
2586
|
-
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2587
|
-
)) {
|
|
2588
|
-
x402ConfigError = "x402 accept decimals must be a non-negative integer.";
|
|
2589
|
-
} else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2590
|
-
x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2593
|
-
if (config.protocols?.includes("mpp")) {
|
|
2594
|
-
if (!config.mpp) {
|
|
2595
|
-
mppConfigError = 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.';
|
|
2596
|
-
} else if (!config.mpp.recipient && !config.payeeAddress) {
|
|
2597
|
-
mppConfigError = "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config.";
|
|
2598
|
-
} else if (!(config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL)) {
|
|
2599
|
-
mppConfigError = "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object.";
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
const allConfigErrors = [x402ConfigError, mppConfigError].filter(Boolean);
|
|
2603
|
-
if (allConfigErrors.length > 0) {
|
|
2604
|
-
for (const err of allConfigErrors) console.error(`[router] ${err}`);
|
|
3114
|
+
const configIssues = getRouterConfigIssues(config, {
|
|
3115
|
+
requireCdpKeys: process.env.NODE_ENV === "production"
|
|
3116
|
+
});
|
|
3117
|
+
const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
|
|
3118
|
+
if (baseUrlIssue) throw new RouterConfigError([baseUrlIssue]);
|
|
3119
|
+
const emptyProtocolsIssue = configIssues.find((issue) => issue.code === "empty_protocols");
|
|
3120
|
+
if (emptyProtocolsIssue) throw new RouterConfigError([emptyProtocolsIssue]);
|
|
3121
|
+
const protocolConfigIssues = configIssues.filter(
|
|
3122
|
+
(issue) => issue.code !== "missing_base_url" && issue.code !== "empty_protocols"
|
|
3123
|
+
);
|
|
3124
|
+
const x402ConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "x402");
|
|
3125
|
+
const mppConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "mpp");
|
|
3126
|
+
const x402ConfigError = x402ConfigIssues.length > 0 ? formatRouterConfigIssues(x402ConfigIssues) : void 0;
|
|
3127
|
+
const mppConfigError = mppConfigIssues.length > 0 ? formatRouterConfigIssues(mppConfigIssues) : void 0;
|
|
3128
|
+
if (protocolConfigIssues.length > 0) {
|
|
3129
|
+
for (const issue of protocolConfigIssues) console.error(`[router] ${issue.message}`);
|
|
2605
3130
|
if (process.env.NODE_ENV === "production") {
|
|
2606
|
-
throw new
|
|
3131
|
+
throw new RouterConfigError(protocolConfigIssues);
|
|
2607
3132
|
}
|
|
2608
3133
|
}
|
|
3134
|
+
const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2609
3135
|
if (config.plugin?.init) {
|
|
2610
3136
|
try {
|
|
2611
3137
|
const result = config.plugin.init({ origin: resolvedBaseUrl });
|
|
@@ -2623,6 +3149,7 @@ function createRouter(config) {
|
|
|
2623
3149
|
nonceStore,
|
|
2624
3150
|
entitlementStore,
|
|
2625
3151
|
payeeAddress: config.payeeAddress ?? "",
|
|
3152
|
+
mppRecipient: config.mpp?.recipient ?? config.payeeAddress,
|
|
2626
3153
|
network,
|
|
2627
3154
|
x402FacilitatorsByNetwork: void 0,
|
|
2628
3155
|
x402Accepts,
|
|
@@ -2750,9 +3277,6 @@ function createRouter(config) {
|
|
|
2750
3277
|
registry
|
|
2751
3278
|
};
|
|
2752
3279
|
}
|
|
2753
|
-
function isSupportedX402Network(network) {
|
|
2754
|
-
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2755
|
-
}
|
|
2756
3280
|
function normalizePath(path) {
|
|
2757
3281
|
let normalized = path.trim();
|
|
2758
3282
|
normalized = normalized.replace(/^\/+/, "");
|
|
@@ -2760,15 +3284,26 @@ function normalizePath(path) {
|
|
|
2760
3284
|
return normalized.replace(/\/+$/, "");
|
|
2761
3285
|
}
|
|
2762
3286
|
export {
|
|
3287
|
+
BASE_NETWORK,
|
|
2763
3288
|
HttpError,
|
|
2764
3289
|
MemoryEntitlementStore,
|
|
2765
3290
|
MemoryNonceStore,
|
|
2766
3291
|
RouteBuilder,
|
|
2767
3292
|
RouteRegistry,
|
|
3293
|
+
RouterConfigError,
|
|
2768
3294
|
SIWX_CHALLENGE_EXPIRY_MS,
|
|
2769
3295
|
SIWX_ERROR_MESSAGES,
|
|
3296
|
+
SOLANA_MAINNET_NETWORK,
|
|
3297
|
+
TEMPO_USDC_CURRENCY,
|
|
3298
|
+
ZERO_EVM_ADDRESS,
|
|
2770
3299
|
consolePlugin,
|
|
2771
3300
|
createRedisEntitlementStore,
|
|
2772
3301
|
createRedisNonceStore,
|
|
2773
|
-
createRouter
|
|
3302
|
+
createRouter,
|
|
3303
|
+
formatRouterConfigIssues,
|
|
3304
|
+
getRouterConfigIssues,
|
|
3305
|
+
mppFromEnv,
|
|
3306
|
+
paidOptionsForProtocols,
|
|
3307
|
+
validateRouterConfig,
|
|
3308
|
+
x402AcceptsFromEnv
|
|
2774
3309
|
};
|