@bankofai/x402-cli 1.0.1-beta.7 → 1.0.1-beta.8
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/dist/cli.js +112 -22
- package/dist/gateway/config.js +48 -3
- package/dist/gateway/server.js +95 -18
- package/dist/tokens.js +2 -2
- package/dist/x402.js +8 -2
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import net from "node:net";
|
|
4
5
|
import fs from "node:fs";
|
|
5
6
|
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
@@ -11,6 +12,7 @@ import YAML from "yaml";
|
|
|
11
12
|
import { createPaymentPayload, decodeRequired, decodeResponse, decodeSignature, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
|
|
12
13
|
import { assertRawAmount, findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
|
|
13
14
|
const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "force", "help", "human", "include-blocked", "json", "raw", "version"]);
|
|
15
|
+
const MAX_HTTP_BODY_BYTES = 10 * 1024 * 1024;
|
|
14
16
|
const require = createRequire(import.meta.url);
|
|
15
17
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
16
18
|
const CATALOG_UPDATE_RETRIES = 3;
|
|
@@ -71,7 +73,7 @@ function parseArgs(argv) {
|
|
|
71
73
|
options[key] = true;
|
|
72
74
|
}
|
|
73
75
|
else if (!next || next.startsWith("--")) {
|
|
74
|
-
|
|
76
|
+
throw new CliError("MISSING_ARGUMENT", `--${key} requires a value`, `Pass --${key} <value>.`, 2);
|
|
75
77
|
}
|
|
76
78
|
else {
|
|
77
79
|
if (key === "header") {
|
|
@@ -653,7 +655,36 @@ async function readText(source, options) {
|
|
|
653
655
|
const response = await fetchWithTimeout(source, {}, timeoutMs(options), `fetch ${source}`);
|
|
654
656
|
if (!response.ok)
|
|
655
657
|
throw new Error(`failed to fetch ${source}: ${response.status}`);
|
|
656
|
-
return response
|
|
658
|
+
return readBoundedText(response, `response from ${source}`);
|
|
659
|
+
}
|
|
660
|
+
async function readBoundedText(response, label) {
|
|
661
|
+
const declared = Number(response.headers.get("content-length"));
|
|
662
|
+
if (Number.isFinite(declared) && declared > MAX_HTTP_BODY_BYTES) {
|
|
663
|
+
throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
|
|
664
|
+
}
|
|
665
|
+
if (!response.body)
|
|
666
|
+
return "";
|
|
667
|
+
const reader = response.body.getReader();
|
|
668
|
+
const chunks = [];
|
|
669
|
+
let size = 0;
|
|
670
|
+
for (;;) {
|
|
671
|
+
const { done, value } = await reader.read();
|
|
672
|
+
if (done)
|
|
673
|
+
break;
|
|
674
|
+
size += value.byteLength;
|
|
675
|
+
if (size > MAX_HTTP_BODY_BYTES) {
|
|
676
|
+
await reader.cancel();
|
|
677
|
+
throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
|
|
678
|
+
}
|
|
679
|
+
chunks.push(value);
|
|
680
|
+
}
|
|
681
|
+
const bytes = new Uint8Array(size);
|
|
682
|
+
let offset = 0;
|
|
683
|
+
for (const chunk of chunks) {
|
|
684
|
+
bytes.set(chunk, offset);
|
|
685
|
+
offset += chunk.byteLength;
|
|
686
|
+
}
|
|
687
|
+
return new TextDecoder().decode(bytes);
|
|
657
688
|
}
|
|
658
689
|
async function readJson(source, options) {
|
|
659
690
|
return JSON.parse(await readText(source, options));
|
|
@@ -663,7 +694,7 @@ function writeJson(file, value) {
|
|
|
663
694
|
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
664
695
|
}
|
|
665
696
|
async function responsePayload(response) {
|
|
666
|
-
const text = await response
|
|
697
|
+
const text = await readBoundedText(response, "HTTP response");
|
|
667
698
|
const contentType = response.headers.get("content-type") ?? "";
|
|
668
699
|
if (contentType.toLowerCase().includes("json")) {
|
|
669
700
|
try {
|
|
@@ -696,7 +727,7 @@ function cachedCatalogPath() {
|
|
|
696
727
|
return path.join(cacheDir(), "catalog.json");
|
|
697
728
|
}
|
|
698
729
|
function providerFilename(fqn) {
|
|
699
|
-
return `${fqn.replace(/\//g, "__")}.json`;
|
|
730
|
+
return `${sanitizeProviderName(fqn).replace(/\//g, "__")}.json`;
|
|
700
731
|
}
|
|
701
732
|
function sanitizeProviderName(name) {
|
|
702
733
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(name) || name.includes("..")) {
|
|
@@ -758,8 +789,11 @@ function remoteBaseFromCatalogPayload(payload) {
|
|
|
758
789
|
}
|
|
759
790
|
async function remoteBaseFromSource(source, payload, options) {
|
|
760
791
|
const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
|
|
761
|
-
if (fromPayload)
|
|
792
|
+
if (fromPayload) {
|
|
793
|
+
if (/^https?:\/\//.test(source) && new URL(fromPayload).origin !== new URL(source).origin)
|
|
794
|
+
throw new Error("catalog base_url must use the same origin as the catalog source");
|
|
762
795
|
return fromPayload;
|
|
796
|
+
}
|
|
763
797
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
764
798
|
return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
|
|
765
799
|
}
|
|
@@ -771,6 +805,7 @@ async function remoteBaseFromSource(source, payload, options) {
|
|
|
771
805
|
}
|
|
772
806
|
}
|
|
773
807
|
function catalogDetailSource(source, section, name) {
|
|
808
|
+
name = sanitizeProviderName(name);
|
|
774
809
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
775
810
|
const base = new URL(source);
|
|
776
811
|
const pathname = base.pathname.endsWith("/catalog.json")
|
|
@@ -994,8 +1029,8 @@ function buildRequirement(options) {
|
|
|
994
1029
|
if (!explicitAsset && !registryToken)
|
|
995
1030
|
throw new Error(`unknown token ${tokenSymbol} on ${network}`);
|
|
996
1031
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : registryToken.decimals;
|
|
997
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
998
|
-
throw new Error("--decimals must be
|
|
1032
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
1033
|
+
throw new Error("--decimals must be an integer between 0 and 255");
|
|
999
1034
|
const rawAmount = opt(options, "rawAmount") ?? opt(options, "raw-amount");
|
|
1000
1035
|
const humanAmount = opt(options, "amount");
|
|
1001
1036
|
if (rawAmount && humanAmount)
|
|
@@ -1003,13 +1038,17 @@ function buildRequirement(options) {
|
|
|
1003
1038
|
const amount = rawAmount ? assertRawAmount(rawAmount, "--raw-amount") : toSmallestUnit(humanAmount ?? "0.0001", decimals);
|
|
1004
1039
|
const assetAddress = explicitAsset ?? registryToken.address;
|
|
1005
1040
|
const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
|
|
1041
|
+
const maxTimeoutSeconds = Number(opt(options, "valid-for-seconds", "300"));
|
|
1042
|
+
if (!Number.isInteger(maxTimeoutSeconds) || maxTimeoutSeconds <= 0 || maxTimeoutSeconds > 86400) {
|
|
1043
|
+
throw new Error("--valid-for-seconds must be an integer between 1 and 86400");
|
|
1044
|
+
}
|
|
1006
1045
|
return {
|
|
1007
1046
|
scheme,
|
|
1008
1047
|
network,
|
|
1009
1048
|
amount,
|
|
1010
1049
|
asset: assetAddress,
|
|
1011
1050
|
payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
|
|
1012
|
-
maxTimeoutSeconds
|
|
1051
|
+
maxTimeoutSeconds,
|
|
1013
1052
|
extra: scheme === "exact" && assetTransferMethod ? { assetTransferMethod } : {},
|
|
1014
1053
|
};
|
|
1015
1054
|
}
|
|
@@ -1019,10 +1058,16 @@ async function facilitatorPost(baseUrl, path, body, options) {
|
|
|
1019
1058
|
headers: { "content-type": "application/json" },
|
|
1020
1059
|
body: JSON.stringify(body),
|
|
1021
1060
|
}, timeoutMs(options), `facilitator ${path}`);
|
|
1022
|
-
const text = await response
|
|
1023
|
-
|
|
1061
|
+
const text = await readBoundedText(response, `facilitator ${path} response`);
|
|
1062
|
+
let data = {};
|
|
1063
|
+
try {
|
|
1064
|
+
data = text ? JSON.parse(text) : {};
|
|
1065
|
+
}
|
|
1066
|
+
catch {
|
|
1067
|
+
throw new Error(`facilitator ${path} returned invalid JSON`);
|
|
1068
|
+
}
|
|
1024
1069
|
if (!response.ok)
|
|
1025
|
-
throw new Error(`facilitator ${path} failed
|
|
1070
|
+
throw new Error(`facilitator ${path} failed with HTTP ${response.status}`);
|
|
1026
1071
|
return data;
|
|
1027
1072
|
}
|
|
1028
1073
|
function requestHeaders(options) {
|
|
@@ -1038,7 +1083,23 @@ function requestHeaders(options) {
|
|
|
1038
1083
|
function stripFlag(argv, flag) {
|
|
1039
1084
|
return argv.filter(item => item !== flag);
|
|
1040
1085
|
}
|
|
1041
|
-
function
|
|
1086
|
+
async function waitForPort(host, port, timeout = 5_000) {
|
|
1087
|
+
const deadline = Date.now() + timeout;
|
|
1088
|
+
while (Date.now() < deadline) {
|
|
1089
|
+
const connected = await new Promise(resolve => {
|
|
1090
|
+
const socket = net.createConnection({ host, port });
|
|
1091
|
+
socket.setTimeout(250);
|
|
1092
|
+
socket.once("connect", () => { socket.destroy(); resolve(true); });
|
|
1093
|
+
socket.once("error", () => resolve(false));
|
|
1094
|
+
socket.once("timeout", () => { socket.destroy(); resolve(false); });
|
|
1095
|
+
});
|
|
1096
|
+
if (connected)
|
|
1097
|
+
return;
|
|
1098
|
+
await delay(50);
|
|
1099
|
+
}
|
|
1100
|
+
throw new Error(`daemon did not become ready on ${host}:${port}`);
|
|
1101
|
+
}
|
|
1102
|
+
async function serveDaemon(argv, options) {
|
|
1042
1103
|
const requirement = buildRequirement(options);
|
|
1043
1104
|
if (!requirement.payTo)
|
|
1044
1105
|
throw new Error("--pay-to is required");
|
|
@@ -1053,6 +1114,18 @@ function serveDaemon(argv, options) {
|
|
|
1053
1114
|
const host = opt(options, "host", "127.0.0.1");
|
|
1054
1115
|
const port = Number(opt(options, "port", "4020"));
|
|
1055
1116
|
const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
|
|
1117
|
+
try {
|
|
1118
|
+
await waitForPort(host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "::1" : host, port);
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
if (child.pid) {
|
|
1122
|
+
try {
|
|
1123
|
+
process.kill(child.pid);
|
|
1124
|
+
}
|
|
1125
|
+
catch { /* child already exited */ }
|
|
1126
|
+
}
|
|
1127
|
+
throw error;
|
|
1128
|
+
}
|
|
1056
1129
|
emit({
|
|
1057
1130
|
command: "server",
|
|
1058
1131
|
mode: outputMode(options),
|
|
@@ -1079,8 +1152,8 @@ function validateAmountLimits(selected, options) {
|
|
|
1079
1152
|
throw new Error("cannot evaluate --max-amount for an unknown asset; pass --max-raw-amount or --decimals");
|
|
1080
1153
|
}
|
|
1081
1154
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
|
|
1082
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
1083
|
-
throw new Error("--decimals must be
|
|
1155
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
1156
|
+
throw new Error("--decimals must be an integer between 0 and 255");
|
|
1084
1157
|
if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, decimals))) {
|
|
1085
1158
|
throw new Error(`payment amount exceeds --max-amount ${maxAmount}`);
|
|
1086
1159
|
}
|
|
@@ -1107,8 +1180,8 @@ function gasfreeFeeLimitRaw(selected, options) {
|
|
|
1107
1180
|
throw new Error("cannot evaluate --max-gasfree-fee for an unknown asset; pass --max-gasfree-fee-raw or --decimals");
|
|
1108
1181
|
}
|
|
1109
1182
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
|
|
1110
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
1111
|
-
throw new Error("--decimals must be
|
|
1183
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
1184
|
+
throw new Error("--decimals must be an integer between 0 and 255");
|
|
1112
1185
|
return toSmallestUnit(maxHuman, decimals);
|
|
1113
1186
|
}
|
|
1114
1187
|
async function serve(options) {
|
|
@@ -1173,7 +1246,7 @@ async function serve(options) {
|
|
|
1173
1246
|
paymentPayload: payload,
|
|
1174
1247
|
paymentRequirements: requirement,
|
|
1175
1248
|
}, options);
|
|
1176
|
-
if (!(settle?.success === true
|
|
1249
|
+
if (!(settle?.success === true && typeof settle?.transaction === "string" && settle.transaction.length > 0 && typeof settle?.network === "string" && settle.network.length > 0)) {
|
|
1177
1250
|
response.writeHead(502, { "content-type": "application/json" });
|
|
1178
1251
|
response.end(JSON.stringify({ error: "settlement failed" }));
|
|
1179
1252
|
return;
|
|
@@ -1182,7 +1255,7 @@ async function serve(options) {
|
|
|
1182
1255
|
"content-type": "application/json",
|
|
1183
1256
|
[headers.response]: encodeResponse(settle),
|
|
1184
1257
|
});
|
|
1185
|
-
response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction
|
|
1258
|
+
response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction }));
|
|
1186
1259
|
}
|
|
1187
1260
|
catch (error) {
|
|
1188
1261
|
response.writeHead(500, { "content-type": "application/json" });
|
|
@@ -1209,6 +1282,17 @@ function selectRequirement(accepts, options) {
|
|
|
1209
1282
|
const scheme = opt(options, "scheme");
|
|
1210
1283
|
const token = opt(options, "token");
|
|
1211
1284
|
const selected = accepts.find(req => {
|
|
1285
|
+
if (!req || !["exact", "exact_gasfree"].includes(req.scheme) || typeof req.network !== "string" || typeof req.asset !== "string")
|
|
1286
|
+
return false;
|
|
1287
|
+
if (req.scheme === "exact_gasfree" && !req.network.startsWith("tron:"))
|
|
1288
|
+
return false;
|
|
1289
|
+
try {
|
|
1290
|
+
if (!findTokenByAddress(req.network, req.asset))
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1212
1296
|
if (network && normalizeNetwork(network) !== req.network)
|
|
1213
1297
|
return false;
|
|
1214
1298
|
if (scheme && scheme !== req.scheme)
|
|
@@ -1233,6 +1317,9 @@ function selectRequirement(accepts, options) {
|
|
|
1233
1317
|
async function pay(url, options) {
|
|
1234
1318
|
requireArgument(url, "URL", "x402-cli pay <url> [options]");
|
|
1235
1319
|
const method = opt(options, "method", "GET");
|
|
1320
|
+
if (!/^[A-Z]+$/.test(method) || !["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"].includes(method)) {
|
|
1321
|
+
throw new CliError("INVALID_ARGUMENT", `unsupported HTTP method ${method}`, "Use an uppercase standard HTTP method.", 2);
|
|
1322
|
+
}
|
|
1236
1323
|
const baseHeaders = requestHeaders(options);
|
|
1237
1324
|
const probe = await fetchWithTimeout(url, {
|
|
1238
1325
|
method,
|
|
@@ -1298,7 +1385,7 @@ async function pay(url, options) {
|
|
|
1298
1385
|
const body = await responsePayload(paid);
|
|
1299
1386
|
const paymentResponse = paid.headers.get(headers.response);
|
|
1300
1387
|
const settlement = paymentResponse ? decodeResponse(paymentResponse) : undefined;
|
|
1301
|
-
const settled = settlement
|
|
1388
|
+
const settled = settlement?.success === true && typeof settlement?.transaction === "string" && settlement.transaction.length > 0 && typeof settlement?.network === "string" && settlement.network.length > 0;
|
|
1302
1389
|
const result = {
|
|
1303
1390
|
url,
|
|
1304
1391
|
status: paid.status,
|
|
@@ -1312,6 +1399,9 @@ async function pay(url, options) {
|
|
|
1312
1399
|
} : {}),
|
|
1313
1400
|
...(creation.gasfreeEstimate ? { gasfreeEstimate: creation.gasfreeEstimate } : {}),
|
|
1314
1401
|
};
|
|
1402
|
+
if (paymentResponse && !settled) {
|
|
1403
|
+
throw new CliError("INVALID_SETTLEMENT", "gateway returned an invalid or unsuccessful PAYMENT-RESPONSE", "Do not treat this request as paid; contact the gateway operator.", 1, result);
|
|
1404
|
+
}
|
|
1315
1405
|
if (!paid.ok) {
|
|
1316
1406
|
const retryAfter = paid.headers.get("retry-after");
|
|
1317
1407
|
throw new CliError(paid.status === 429 ? "RATE_LIMITED" : "HTTP_ERROR", `HTTP ${paid.status} from ${url}${retryAfter ? ` (retry after ${retryAfter}s)` : ""}: ${typeof body === "string" ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`, paymentResponse
|
|
@@ -1664,7 +1754,7 @@ async function catalogPayJson(source, name, options) {
|
|
|
1664
1754
|
}
|
|
1665
1755
|
async function handleGateway(args) {
|
|
1666
1756
|
const { command, positional, options } = parseArgs(args);
|
|
1667
|
-
if (hasFlag(options, "help") ||
|
|
1757
|
+
if (hasFlag(options, "help") || ["help", "--help", "-h"].includes(command)) {
|
|
1668
1758
|
process.stdout.write(helpText(command === "catalog" || positional[0] === "catalog" ? "gateway-catalog" : "gateway"));
|
|
1669
1759
|
return;
|
|
1670
1760
|
}
|
|
@@ -1697,7 +1787,7 @@ async function handleGatewayCatalog(positional, options) {
|
|
|
1697
1787
|
}
|
|
1698
1788
|
async function handleCatalog(args) {
|
|
1699
1789
|
const { command, positional, options } = parseArgs(args);
|
|
1700
|
-
if (hasFlag(options, "help") ||
|
|
1790
|
+
if (hasFlag(options, "help") || ["help", "--help", "-h"].includes(command)) {
|
|
1701
1791
|
const topic = command === "help" ? positional[0] : command;
|
|
1702
1792
|
process.stdout.write(helpText(topic ? `catalog-${topic}` : "catalog"));
|
|
1703
1793
|
return;
|
|
@@ -1742,7 +1832,7 @@ async function main() {
|
|
|
1742
1832
|
}
|
|
1743
1833
|
if (command === "serve") {
|
|
1744
1834
|
if (hasFlag(options, "daemon"))
|
|
1745
|
-
serveDaemon(argv, options);
|
|
1835
|
+
await serveDaemon(argv, options);
|
|
1746
1836
|
else
|
|
1747
1837
|
await serve(options);
|
|
1748
1838
|
}
|
package/dist/gateway/config.js
CHANGED
|
@@ -30,6 +30,11 @@ function assertHttpUrl(value, name) {
|
|
|
30
30
|
const url = new URL(value);
|
|
31
31
|
if (!["http:", "https:"].includes(url.protocol))
|
|
32
32
|
throw new Error("unsupported protocol");
|
|
33
|
+
if (url.username || url.password || url.search || url.hash)
|
|
34
|
+
throw new Error("credentials, query, and fragment are not allowed");
|
|
35
|
+
if (url.protocol === "http:" && !["localhost", "127.0.0.1", "::1"].includes(url.hostname) && process.env.X402_GATEWAY_ALLOW_INSECURE_HTTP !== "true") {
|
|
36
|
+
throw new Error("remote HTTP is not allowed");
|
|
37
|
+
}
|
|
33
38
|
}
|
|
34
39
|
catch {
|
|
35
40
|
throw new Error(`${name} must be a valid http(s) URL`);
|
|
@@ -80,6 +85,13 @@ function validateProvider(config, file) {
|
|
|
80
85
|
if (authMethod && !["header", "query_param", "access_token", "oauth2"].includes(authMethod)) {
|
|
81
86
|
throw new Error(`${file}: unsupported routing.auth.method ${authMethod}`);
|
|
82
87
|
}
|
|
88
|
+
const auth = config.routing?.auth;
|
|
89
|
+
if (auth) {
|
|
90
|
+
if (!auth.value && !auth.value_from_env)
|
|
91
|
+
throw new Error(`${file}: routing.auth requires value or value_from_env`);
|
|
92
|
+
if (auth.value_from_env && !process.env[auth.value_from_env])
|
|
93
|
+
throw new Error(`${file}: environment variable ${auth.value_from_env} is not set`);
|
|
94
|
+
}
|
|
83
95
|
if (!config.endpoints?.length)
|
|
84
96
|
throw new Error(`${file}: endpoints must contain at least one endpoint`);
|
|
85
97
|
const seen = new Set();
|
|
@@ -104,6 +116,7 @@ export function loadProvider(file) {
|
|
|
104
116
|
validateProvider(config, file);
|
|
105
117
|
config.operator.network = normalizeNetwork(config.operator.network);
|
|
106
118
|
normalizePaymentProtocol(config, file);
|
|
119
|
+
validatePaymentCapabilities(config, file);
|
|
107
120
|
const facilitatorUrl = config.operator.facilitator_url ||
|
|
108
121
|
process.env.X402_FACILITATOR_URL ||
|
|
109
122
|
process.env.FACILITATOR_URL ||
|
|
@@ -125,10 +138,13 @@ export function loadProvider(file) {
|
|
|
125
138
|
};
|
|
126
139
|
}
|
|
127
140
|
function normalizePaymentProtocol(config, file) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
if (config.operator.schemes !== undefined && (!Array.isArray(config.operator.schemes) || !config.operator.schemes.length)) {
|
|
142
|
+
throw new Error(`${file}: operator.schemes must be a non-empty string array`);
|
|
143
|
+
}
|
|
144
|
+
const rawSchemes = config.operator.schemes ?? [config.operator.protocol || config.operator.scheme || "exact"];
|
|
131
145
|
const schemes = [...new Set(rawSchemes.map(value => {
|
|
146
|
+
if (typeof value !== "string" || !value.trim())
|
|
147
|
+
throw new Error(`${file}: operator.schemes must contain non-empty strings`);
|
|
132
148
|
const raw = String(value).toLowerCase();
|
|
133
149
|
const normalized = raw.replace(/[-:\s]/g, "_");
|
|
134
150
|
if (!["exact", "exact_gasfree", "exact_permit", "permit2", "exact_permit2"].includes(normalized)) {
|
|
@@ -151,6 +167,33 @@ function normalizePaymentProtocol(config, file) {
|
|
|
151
167
|
delete config.operator.assetTransferMethod;
|
|
152
168
|
}
|
|
153
169
|
}
|
|
170
|
+
function validatePaymentCapabilities(config, file) {
|
|
171
|
+
const symbols = config.operator.currencies?.usd ?? ["USDT"];
|
|
172
|
+
if (!Array.isArray(symbols) || !symbols.length || symbols.some(symbol => typeof symbol !== "string" || !symbol.trim())) {
|
|
173
|
+
throw new Error(`${file}: operator.currencies.usd must be a non-empty string array`);
|
|
174
|
+
}
|
|
175
|
+
if (new Set(symbols.map(symbol => symbol.toUpperCase())).size !== symbols.length)
|
|
176
|
+
throw new Error(`${file}: operator.currencies.usd must not contain duplicates`);
|
|
177
|
+
for (const symbol of symbols)
|
|
178
|
+
getToken(config.operator.network, symbol);
|
|
179
|
+
const recipient = config.recipients?.[config.operator.recipient]?.account ?? config.operator.recipient;
|
|
180
|
+
assertString(recipient, `${file}: resolved recipient`);
|
|
181
|
+
const validRecipient = config.operator.network.startsWith("tron:")
|
|
182
|
+
? /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(recipient)
|
|
183
|
+
: /^0x[0-9a-fA-F]{40}$/.test(recipient);
|
|
184
|
+
if (!validRecipient)
|
|
185
|
+
throw new Error(`${file}: operator.recipient must be a valid address or a resolvable recipient alias`);
|
|
186
|
+
for (const endpoint of config.endpoints ?? []) {
|
|
187
|
+
if (!endpoint.metering)
|
|
188
|
+
continue;
|
|
189
|
+
const prices = [endpoint.metering.dimensions?.[0]?.tiers?.[0]?.price_usd,
|
|
190
|
+
...(endpoint.metering.variants ?? []).map(variant => variant.dimensions?.[0]?.tiers?.[0]?.price_usd)]
|
|
191
|
+
.filter((price) => typeof price === "number" && price > 0);
|
|
192
|
+
for (const price of prices)
|
|
193
|
+
if (!paymentRequirements(config, price).length)
|
|
194
|
+
throw new Error(`${file}: paid endpoint cannot generate payment requirements`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
154
197
|
export function loadProviders(providerPath) {
|
|
155
198
|
const stat = fs.statSync(providerPath);
|
|
156
199
|
const files = stat.isDirectory()
|
|
@@ -174,6 +217,8 @@ export function endpointFor(provider, method, routePath) {
|
|
|
174
217
|
return provider.endpoints?.find(endpoint => endpoint.method.toUpperCase() === method.toUpperCase() && pathMatches(endpoint.path, routePath));
|
|
175
218
|
}
|
|
176
219
|
function pathMatches(template, routePath) {
|
|
220
|
+
if (!routePath.startsWith("/") || routePath.startsWith("//") || routePath.includes("\\") || routePath.includes("\0") || /%(?:2f|5c)/i.test(routePath))
|
|
221
|
+
return false;
|
|
177
222
|
const templateParts = template.split("/").filter(Boolean);
|
|
178
223
|
const routeParts = routePath.split("/").filter(Boolean);
|
|
179
224
|
if (templateParts.length !== routeParts.length)
|
package/dist/gateway/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
3
|
import { URL } from "node:url";
|
|
3
4
|
import { endpointFor, paymentRequirements, priceUsd } from "./config.js";
|
|
4
5
|
import { decodeSignature, encodeRequired, encodeResponse, headers, matchRequirement } from "./x402.js";
|
|
@@ -25,13 +26,16 @@ function positiveIntegerEnv(name, fallback) {
|
|
|
25
26
|
if (!/^\d+$/.test(raw))
|
|
26
27
|
throw new Error(`${name} must be a positive integer`);
|
|
27
28
|
const value = Number(raw);
|
|
28
|
-
if (!Number.isSafeInteger(value) || value <= 0)
|
|
29
|
-
throw new Error(`${name} must be
|
|
29
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 2_147_483_647)
|
|
30
|
+
throw new Error(`${name} must be an integer between 1 and 2147483647`);
|
|
30
31
|
return value;
|
|
31
32
|
}
|
|
32
33
|
const MAX_BODY_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_BODY_BYTES", 1_000_000);
|
|
33
34
|
const FACILITATOR_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_FACILITATOR_TIMEOUT_MS", 10_000);
|
|
34
35
|
const UPSTREAM_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_UPSTREAM_TIMEOUT_MS", 30_000);
|
|
36
|
+
const MAX_RESPONSE_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_RESPONSE_BYTES", 10_000_000);
|
|
37
|
+
const MAX_CONCURRENT_REQUESTS = positiveIntegerEnv("X402_GATEWAY_MAX_CONCURRENT_REQUESTS", 100);
|
|
38
|
+
const RATE_LIMIT_PER_MINUTE = positiveIntegerEnv("X402_GATEWAY_RATE_LIMIT_PER_MINUTE", 300);
|
|
35
39
|
const STRIP_REQUEST_HEADERS = new Set([
|
|
36
40
|
"host",
|
|
37
41
|
"connection",
|
|
@@ -49,6 +53,8 @@ const STRIP_REQUEST_HEADERS = new Set([
|
|
|
49
53
|
"payment-signature",
|
|
50
54
|
"payment-required",
|
|
51
55
|
"x-payment-required",
|
|
56
|
+
"payment-response",
|
|
57
|
+
"x-payment-response",
|
|
52
58
|
"accept-encoding",
|
|
53
59
|
]);
|
|
54
60
|
const STRIP_RESPONSE_HEADERS = new Set([
|
|
@@ -73,6 +79,7 @@ function createMetrics() {
|
|
|
73
79
|
verifyFailures: 0,
|
|
74
80
|
settleFailures: 0,
|
|
75
81
|
upstreamFailures: 0,
|
|
82
|
+
rejectedRequests: 0,
|
|
76
83
|
};
|
|
77
84
|
}
|
|
78
85
|
async function readBody(request) {
|
|
@@ -88,14 +95,17 @@ async function readBody(request) {
|
|
|
88
95
|
return Buffer.concat(chunks);
|
|
89
96
|
}
|
|
90
97
|
function json(response, status, body, extraHeaders = {}) {
|
|
91
|
-
response.writeHead(status, { "content-type": "application/json", ...extraHeaders });
|
|
98
|
+
response.writeHead(status, { "content-type": "application/json", "cache-control": "no-store", "x-content-type-options": "nosniff", ...extraHeaders });
|
|
92
99
|
response.end(JSON.stringify(body));
|
|
93
100
|
}
|
|
94
101
|
async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
95
102
|
const controller = new AbortController();
|
|
96
103
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
97
104
|
try {
|
|
98
|
-
|
|
105
|
+
const response = await fetch(url, { ...init, redirect: "manual", signal: controller.signal });
|
|
106
|
+
if (response.status >= 300 && response.status < 400)
|
|
107
|
+
throw new HttpError(502, `${label} redirect refused`);
|
|
108
|
+
return response;
|
|
99
109
|
}
|
|
100
110
|
catch (error) {
|
|
101
111
|
if (error instanceof Error && error.name === "AbortError")
|
|
@@ -106,13 +116,30 @@ async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
|
106
116
|
clearTimeout(timer);
|
|
107
117
|
}
|
|
108
118
|
}
|
|
119
|
+
async function readResponseBytes(response, limit = MAX_RESPONSE_BYTES) {
|
|
120
|
+
const declared = Number(response.headers.get("content-length"));
|
|
121
|
+
if (Number.isFinite(declared) && declared > limit)
|
|
122
|
+
throw new HttpError(502, "upstream response too large");
|
|
123
|
+
if (!response.body)
|
|
124
|
+
return Buffer.alloc(0);
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let total = 0;
|
|
127
|
+
for await (const chunk of response.body) {
|
|
128
|
+
const buffer = Buffer.from(chunk);
|
|
129
|
+
total += buffer.length;
|
|
130
|
+
if (total > limit)
|
|
131
|
+
throw new HttpError(502, "upstream response too large");
|
|
132
|
+
chunks.push(buffer);
|
|
133
|
+
}
|
|
134
|
+
return Buffer.concat(chunks);
|
|
135
|
+
}
|
|
109
136
|
async function facilitatorPost(entry, path, body) {
|
|
110
137
|
const headers = { "content-type": "application/json" };
|
|
111
138
|
if (entry.facilitatorApiKey) {
|
|
112
139
|
headers["x-api-key"] = entry.facilitatorApiKey;
|
|
113
140
|
}
|
|
114
|
-
const response = await fetchWithTimeout(new URL(path, entry.facilitatorUrl), { method: "POST", headers, body: JSON.stringify(body) }, FACILITATOR_TIMEOUT_MS, "facilitator");
|
|
115
|
-
const text = await response.
|
|
141
|
+
const response = await fetchWithTimeout(new URL(path.replace(/^\/+/, ""), `${entry.facilitatorUrl.replace(/\/+$/, "")}/`), { method: "POST", headers, body: JSON.stringify(body) }, FACILITATOR_TIMEOUT_MS, "facilitator");
|
|
142
|
+
const text = (await readResponseBytes(response, Math.min(MAX_RESPONSE_BYTES, 1_000_000))).toString("utf8");
|
|
116
143
|
let data = {};
|
|
117
144
|
try {
|
|
118
145
|
data = text ? JSON.parse(text) : {};
|
|
@@ -173,19 +200,19 @@ function isVerifySuccess(verify) {
|
|
|
173
200
|
return verify?.valid === true || verify?.isValid === true;
|
|
174
201
|
}
|
|
175
202
|
function isSettleSuccess(settle) {
|
|
176
|
-
|
|
177
|
-
return false;
|
|
178
|
-
return (settle?.success === true ||
|
|
179
|
-
settle?.settled === true ||
|
|
180
|
-
(typeof settle?.transaction === "string" && settle.transaction.length > 0) ||
|
|
181
|
-
(typeof settle?.txHash === "string" && settle.txHash.length > 0));
|
|
203
|
+
return settle?.success === true && typeof settle?.transaction === "string" && settle.transaction.length > 0 && typeof settle?.network === "string" && settle.network.length > 0;
|
|
182
204
|
}
|
|
183
205
|
function isAdminAllowed(request) {
|
|
184
206
|
const token = process.env.X402_GATEWAY_ADMIN_TOKEN;
|
|
185
207
|
if (!token)
|
|
186
208
|
return process.env.X402_GATEWAY_ADMIN_ALLOW_PUBLIC === "true";
|
|
187
209
|
const auth = request.headers.authorization ?? "";
|
|
188
|
-
|
|
210
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
211
|
+
const actual = Buffer.from(auth);
|
|
212
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
213
|
+
}
|
|
214
|
+
function clientAddress(request) {
|
|
215
|
+
return request.socket.remoteAddress ?? "unknown";
|
|
189
216
|
}
|
|
190
217
|
function requestParams(url, body, request) {
|
|
191
218
|
const params = {};
|
|
@@ -212,11 +239,12 @@ function requestParams(url, body, request) {
|
|
|
212
239
|
}
|
|
213
240
|
function upstreamHeaders(request, entry) {
|
|
214
241
|
const headersOut = new Headers();
|
|
242
|
+
const connectionHeaders = new Set(String(request.headers.connection ?? "").split(",").map(value => value.trim().toLowerCase()).filter(Boolean));
|
|
215
243
|
for (const [key, value] of Object.entries(request.headers)) {
|
|
216
244
|
if (!value)
|
|
217
245
|
continue;
|
|
218
246
|
const lower = key.toLowerCase();
|
|
219
|
-
if (STRIP_REQUEST_HEADERS.has(lower))
|
|
247
|
+
if (STRIP_REQUEST_HEADERS.has(lower) || connectionHeaders.has(lower))
|
|
220
248
|
continue;
|
|
221
249
|
headersOut.set(key, Array.isArray(value) ? value.join(",") : value);
|
|
222
250
|
}
|
|
@@ -231,7 +259,14 @@ function upstreamHeaders(request, entry) {
|
|
|
231
259
|
}
|
|
232
260
|
function upstreamUrl(entry, request, routePath) {
|
|
233
261
|
const sourceUrl = new URL(request.url ?? "/", "http://local");
|
|
234
|
-
|
|
262
|
+
if (!routePath.startsWith("/") || routePath.startsWith("//") || routePath.includes("\\") || routePath.includes("\0") || /%(?:2f|5c)/i.test(routePath))
|
|
263
|
+
throw new HttpError(400, "invalid provider path");
|
|
264
|
+
const base = new URL(entry.config.forward_url);
|
|
265
|
+
const upstream = new URL(base);
|
|
266
|
+
upstream.pathname = routePath;
|
|
267
|
+
upstream.search = sourceUrl.search;
|
|
268
|
+
if (upstream.origin !== base.origin)
|
|
269
|
+
throw new HttpError(400, "invalid provider path");
|
|
235
270
|
const auth = entry.config.routing?.auth;
|
|
236
271
|
const value = auth?.value ?? (auth?.value_from_env ? process.env[auth.value_from_env] : undefined);
|
|
237
272
|
if (auth?.method === "query_param" && value) {
|
|
@@ -263,14 +298,24 @@ async function forward(metrics, entry, request, response, routePath, body, payme
|
|
|
263
298
|
});
|
|
264
299
|
if (paymentResponse)
|
|
265
300
|
responseHeaders[headers.response] = encodeResponse(paymentResponse);
|
|
266
|
-
|
|
301
|
+
let responseBody;
|
|
302
|
+
try {
|
|
303
|
+
responseBody = await readResponseBytes(upstreamResponse);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
metrics.upstreamFailures += 1;
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
267
309
|
response.writeHead(upstreamResponse.status, responseHeaders);
|
|
268
310
|
response.end(responseBody);
|
|
269
311
|
}
|
|
270
312
|
export function createGatewayServer(providers) {
|
|
271
313
|
const publicBaseUrl = configuredPublicBaseUrl();
|
|
272
314
|
const metrics = createMetrics();
|
|
273
|
-
|
|
315
|
+
const rateLimits = new Map();
|
|
316
|
+
let activeRequests = 0;
|
|
317
|
+
const server = http.createServer(async (request, response) => {
|
|
318
|
+
let countedActive = false;
|
|
274
319
|
try {
|
|
275
320
|
metrics.requests += 1;
|
|
276
321
|
const url = new URL(request.url ?? "/", "http://local");
|
|
@@ -316,6 +361,7 @@ export function createGatewayServer(providers) {
|
|
|
316
361
|
`x402_gateway_verify_failures_total ${metrics.verifyFailures}`,
|
|
317
362
|
`x402_gateway_settle_failures_total ${metrics.settleFailures}`,
|
|
318
363
|
`x402_gateway_upstream_failures_total ${metrics.upstreamFailures}`,
|
|
364
|
+
`x402_gateway_rejected_requests_total ${metrics.rejectedRequests}`,
|
|
319
365
|
"",
|
|
320
366
|
].join("\n"));
|
|
321
367
|
return;
|
|
@@ -336,8 +382,30 @@ export function createGatewayServer(providers) {
|
|
|
336
382
|
json(response, 404, { error: "endpoint not found" });
|
|
337
383
|
return;
|
|
338
384
|
}
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
const address = clientAddress(request);
|
|
387
|
+
let rate = rateLimits.get(address);
|
|
388
|
+
if (!rate || rate.resetAt <= now) {
|
|
389
|
+
rate = { count: 0, resetAt: now + 60_000 };
|
|
390
|
+
rateLimits.set(address, rate);
|
|
391
|
+
}
|
|
392
|
+
rate.count += 1;
|
|
393
|
+
if (rate.count > RATE_LIMIT_PER_MINUTE) {
|
|
394
|
+
metrics.rejectedRequests += 1;
|
|
395
|
+
const retryAfter = Math.max(1, Math.ceil((rate.resetAt - now) / 1000));
|
|
396
|
+
throw new HttpError(429, "gateway rate limited", undefined, { "retry-after": String(retryAfter) });
|
|
397
|
+
}
|
|
398
|
+
if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
|
|
399
|
+
metrics.rejectedRequests += 1;
|
|
400
|
+
throw new HttpError(503, "gateway is busy", undefined, { "retry-after": "1" });
|
|
401
|
+
}
|
|
402
|
+
activeRequests += 1;
|
|
403
|
+
countedActive = true;
|
|
339
404
|
const body = await readBody(request);
|
|
340
|
-
const
|
|
405
|
+
const price = priceUsd(endpoint, requestParams(url, body, request));
|
|
406
|
+
const requirements = paymentRequirements(entry.config, price);
|
|
407
|
+
if (price > 0 && !requirements.length)
|
|
408
|
+
throw new HttpError(500, "paid endpoint has no payment requirements");
|
|
341
409
|
if (!requirements.length) {
|
|
342
410
|
await forward(metrics, entry, request, response, routePath, body);
|
|
343
411
|
return;
|
|
@@ -416,5 +484,14 @@ export function createGatewayServer(providers) {
|
|
|
416
484
|
console.error(error);
|
|
417
485
|
json(response, 500, { error: "internal server error" });
|
|
418
486
|
}
|
|
487
|
+
finally {
|
|
488
|
+
if (countedActive)
|
|
489
|
+
activeRequests -= 1;
|
|
490
|
+
}
|
|
419
491
|
});
|
|
492
|
+
server.requestTimeout = UPSTREAM_TIMEOUT_MS + FACILITATOR_TIMEOUT_MS * 2 + 5_000;
|
|
493
|
+
server.headersTimeout = Math.min(server.requestTimeout, 60_000);
|
|
494
|
+
server.keepAliveTimeout = 5_000;
|
|
495
|
+
server.maxConnections = MAX_CONCURRENT_REQUESTS * 2;
|
|
496
|
+
return server;
|
|
420
497
|
}
|
package/dist/tokens.js
CHANGED
|
@@ -110,8 +110,8 @@ export function assertRawAmount(value, name = "raw amount") {
|
|
|
110
110
|
return value.replace(/^0+(?=\d)/, "");
|
|
111
111
|
}
|
|
112
112
|
export function toSmallestUnit(amount, decimals) {
|
|
113
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
114
|
-
throw new Error("token decimals must be
|
|
113
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
114
|
+
throw new Error("token decimals must be an integer between 0 and 255");
|
|
115
115
|
if (!/^\d+(\.\d+)?$/.test(amount))
|
|
116
116
|
throw new Error("amount must be a non-negative decimal string");
|
|
117
117
|
const [whole, fraction = ""] = amount.split(".");
|
package/dist/x402.js
CHANGED
|
@@ -106,7 +106,7 @@ function evmRpcUrl(network, explicit) {
|
|
|
106
106
|
(chainId === "56" ? "https://bsc-dataseed.binance.org" : undefined) ||
|
|
107
107
|
(chainId === "97" ? "https://data-seed-prebsc-1-s1.binance.org:8545" : undefined));
|
|
108
108
|
}
|
|
109
|
-
async function createTronWallet(privateKey) {
|
|
109
|
+
async function createTronWallet(privateKey, maxGasfreeFeeRaw) {
|
|
110
110
|
const rawKey = privateKey.replace(/^0x/, "");
|
|
111
111
|
const tronWeb = new TronWeb({ fullHost: "https://api.trongrid.io" });
|
|
112
112
|
const address = TronWeb.address.fromPrivateKey(rawKey);
|
|
@@ -115,6 +115,12 @@ async function createTronWallet(privateKey) {
|
|
|
115
115
|
return {
|
|
116
116
|
getAddress: () => address,
|
|
117
117
|
async signTypedData(args) {
|
|
118
|
+
if (maxGasfreeFeeRaw !== undefined && args?.primaryType === "PermitTransfer") {
|
|
119
|
+
const maxFee = BigInt(args?.message?.maxFee ?? -1);
|
|
120
|
+
if (maxFee < 0n || maxFee > BigInt(maxGasfreeFeeRaw)) {
|
|
121
|
+
throw new Error(`final GasFree maxFee ${maxFee} exceeds --max-gasfree-fee limit ${maxGasfreeFeeRaw}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
118
124
|
const signature = await signTronTypedData(tronWeb, args, rawKey);
|
|
119
125
|
return signature.startsWith("0x") ? signature : `0x${signature}`;
|
|
120
126
|
},
|
|
@@ -149,7 +155,7 @@ export async function createPaymentPayload(args) {
|
|
|
149
155
|
}
|
|
150
156
|
if (selected.network.startsWith("tron:")) {
|
|
151
157
|
const privateKey = privateKeyFrom(["TRON_PRIVATE_KEY", "AGENT_WALLET_PRIVATE_KEY", "PRIVATE_KEY"], args.privateKey, ["tron_client", "payer", "default"]);
|
|
152
|
-
const wallet = await createTronWallet(privateKey);
|
|
158
|
+
const wallet = await createTronWallet(privateKey, selected.scheme === "exact_gasfree" ? args.maxGasfreeFeeRaw : undefined);
|
|
153
159
|
const signer = await createClientTronSigner(wallet, {
|
|
154
160
|
network: selected.network,
|
|
155
161
|
rpcUrl: args.rpcUrl || process.env.TRON_RPC_URL,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bankofai/x402-cli",
|
|
3
|
-
"version": "1.0.1-beta.
|
|
3
|
+
"version": "1.0.1-beta.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"bundle:gateway": "node scripts/bundle-gateway.mjs",
|
|
14
14
|
"prepack": "npm run build && npm run bundle:gateway",
|
|
15
15
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
16
|
+
"test:package": "node scripts/test-package.mjs",
|
|
16
17
|
"start": "node dist/cli.js",
|
|
17
18
|
"dev": "tsx src/cli.ts"
|
|
18
19
|
},
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@bankofai/x402-core": "1.0.1",
|
|
24
25
|
"@bankofai/x402-evm": "1.0.1",
|
|
25
|
-
"@bankofai/x402-gateway": "1.0.1-beta.
|
|
26
|
+
"@bankofai/x402-gateway": "1.0.1-beta.7",
|
|
26
27
|
"@bankofai/x402-tron": "1.0.1",
|
|
27
28
|
"tronweb": "6.4.0",
|
|
28
29
|
"viem": "^2.55.0",
|