@bankofai/x402-cli 1.0.0-beta.4 → 1.0.0-beta.5
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 +108 -31
- package/dist/gateway/cli.js +0 -0
- package/dist/tokens.js +12 -1
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,9 @@ import { spawn } from "node:child_process";
|
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import YAML from "yaml";
|
|
11
|
-
import { createPaymentPayload, decodeRequired, decodeResponse, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
|
|
12
|
-
import { findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
|
|
13
|
-
const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "help", "human", "include-blocked", "json", "version"]);
|
|
11
|
+
import { createPaymentPayload, decodeRequired, decodeResponse, decodeSignature, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
|
|
12
|
+
import { assertRawAmount, findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
|
|
13
|
+
const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "force", "help", "human", "include-blocked", "json", "version"]);
|
|
14
14
|
const require = createRequire(import.meta.url);
|
|
15
15
|
function parseArgs(argv) {
|
|
16
16
|
const [command = "help", ...rest] = argv;
|
|
@@ -55,7 +55,7 @@ function parseArgs(argv) {
|
|
|
55
55
|
else if (BOOLEAN_FLAGS.has(key)) {
|
|
56
56
|
options[key] = true;
|
|
57
57
|
}
|
|
58
|
-
else if (!next || next.startsWith("
|
|
58
|
+
else if (!next || next.startsWith("--")) {
|
|
59
59
|
options[key] = true;
|
|
60
60
|
}
|
|
61
61
|
else {
|
|
@@ -344,7 +344,11 @@ function readYaml(file) {
|
|
|
344
344
|
return YAML.parse(fs.readFileSync(file, "utf8"));
|
|
345
345
|
}
|
|
346
346
|
function expandEnv(value) {
|
|
347
|
-
return value.replace(/\$\{([^}]+)\}/g, (_, name) =>
|
|
347
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
348
|
+
if (!(name in process.env))
|
|
349
|
+
throw new Error(`environment variable \${${name}} is not set`);
|
|
350
|
+
return process.env[name] ?? "";
|
|
351
|
+
});
|
|
348
352
|
}
|
|
349
353
|
function expandDeep(value) {
|
|
350
354
|
if (typeof value === "string")
|
|
@@ -386,6 +390,14 @@ function validateProvider(provider, file = "provider.yml") {
|
|
|
386
390
|
if (typeof value !== "string" || !value.trim())
|
|
387
391
|
throw new Error(`${file}: ${name} is required`);
|
|
388
392
|
}
|
|
393
|
+
try {
|
|
394
|
+
const url = new URL(provider.forward_url);
|
|
395
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
396
|
+
throw new Error("unsupported protocol");
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
throw new Error(`${file}: forward_url must be a valid http(s) URL`);
|
|
400
|
+
}
|
|
389
401
|
if (!Array.isArray(provider.endpoints) || !provider.endpoints.length) {
|
|
390
402
|
throw new Error(`${file}: endpoints must contain at least one endpoint`);
|
|
391
403
|
}
|
|
@@ -394,6 +406,12 @@ function validateProvider(provider, file = "provider.yml") {
|
|
|
394
406
|
if (typeof endpoint.method !== "string" || typeof endpoint.path !== "string") {
|
|
395
407
|
throw new Error(`${file}: each endpoint needs method and path`);
|
|
396
408
|
}
|
|
409
|
+
if (!endpoint.method.trim() || !endpoint.path.trim() || !endpoint.path.startsWith("/")) {
|
|
410
|
+
throw new Error(`${file}: endpoint method/path must be non-empty and path must start with /`);
|
|
411
|
+
}
|
|
412
|
+
const price = providerPrice(endpoint);
|
|
413
|
+
if (!Number.isFinite(price) || price < 0)
|
|
414
|
+
throw new Error(`${file}: endpoint price_usd must be a finite number >= 0`);
|
|
397
415
|
const key = `${endpoint.method.toUpperCase()} ${endpoint.path}`;
|
|
398
416
|
if (seen.has(key))
|
|
399
417
|
throw new Error(`${file}: duplicate endpoint ${key}`);
|
|
@@ -568,11 +586,42 @@ function cachedCatalogPath() {
|
|
|
568
586
|
function providerFilename(fqn) {
|
|
569
587
|
return `${fqn.replace(/\//g, "__")}.json`;
|
|
570
588
|
}
|
|
589
|
+
function sanitizeProviderName(name) {
|
|
590
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(name) || name.includes("..")) {
|
|
591
|
+
throw new Error("provider name must be a safe FQN using letters, numbers, dots, underscores, dashes, or slashes");
|
|
592
|
+
}
|
|
593
|
+
return name;
|
|
594
|
+
}
|
|
595
|
+
function safeOutputPath(baseDir, ...parts) {
|
|
596
|
+
const root = path.resolve(baseDir);
|
|
597
|
+
const target = path.resolve(root, ...parts);
|
|
598
|
+
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
|
|
599
|
+
throw new Error(`refusing to write outside output directory: ${target}`);
|
|
600
|
+
}
|
|
601
|
+
return target;
|
|
602
|
+
}
|
|
603
|
+
function ensureWritable(file, options) {
|
|
604
|
+
if (fs.existsSync(file) && !hasFlag(options, "force")) {
|
|
605
|
+
throw new Error(`${file} already exists; pass --force to overwrite`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
571
608
|
function defaultCatalogSource() {
|
|
609
|
+
const envSource = process.env.X402_CATALOG || process.env.X402_GATEWAY_CATALOG;
|
|
610
|
+
if (envSource)
|
|
611
|
+
return envSource;
|
|
572
612
|
return fs.existsSync(cachedCatalogPath())
|
|
573
613
|
? cachedCatalogPath()
|
|
574
614
|
: "https://x402-catalog.bankofai.io/api/catalog.json";
|
|
575
615
|
}
|
|
616
|
+
function positiveIntegerOption(options, key, fallback) {
|
|
617
|
+
const value = opt(options, key, String(fallback));
|
|
618
|
+
if (!/^\d+$/.test(value))
|
|
619
|
+
throw new Error(`--${key} must be a positive integer`);
|
|
620
|
+
const parsed = Number(value);
|
|
621
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0)
|
|
622
|
+
throw new Error(`--${key} must be a positive integer`);
|
|
623
|
+
return parsed;
|
|
624
|
+
}
|
|
576
625
|
function remoteBaseFromCatalogPayload(payload) {
|
|
577
626
|
const base = payload.base_url ?? payload.baseUrl;
|
|
578
627
|
return typeof base === "string" && /^https?:\/\//.test(base) ? `${base.replace(/\/+$/, "")}/` : undefined;
|
|
@@ -599,7 +648,7 @@ function catalogDetailSource(source, section, name) {
|
|
|
599
648
|
: base.pathname.endsWith("/")
|
|
600
649
|
? base.pathname
|
|
601
650
|
: `${base.pathname}/`;
|
|
602
|
-
base.pathname = `${pathname}${section}/${name}
|
|
651
|
+
base.pathname = `${pathname}${section}/${providerFilename(name)}`;
|
|
603
652
|
base.search = "";
|
|
604
653
|
base.hash = "";
|
|
605
654
|
return base.toString();
|
|
@@ -616,8 +665,6 @@ async function readCatalogProvider(source, name) {
|
|
|
616
665
|
const summary = providers.find((item) => item.name === name || item.fqn === name);
|
|
617
666
|
if (!summary)
|
|
618
667
|
throw new Error(`provider not found: ${name}`);
|
|
619
|
-
if (Array.isArray(summary.endpoints) && summary.endpoints.length)
|
|
620
|
-
return summary;
|
|
621
668
|
const fqn = summary.fqn ?? summary.name ?? name;
|
|
622
669
|
try {
|
|
623
670
|
return await readJson(catalogDetailSource(source, "providers", fqn));
|
|
@@ -764,12 +811,18 @@ async function catalogExportGateway(gatewayUrl, options) {
|
|
|
764
811
|
const providerFqn = opt(options, "provider");
|
|
765
812
|
if (!providerFqn)
|
|
766
813
|
throw new Error("--provider is required");
|
|
814
|
+
sanitizeProviderName(providerFqn);
|
|
767
815
|
const base = gatewayUrl.replace(/\/+$/, "");
|
|
768
|
-
const detail = await readJson(`${base}/__402/catalog/providers/${providerFqn}
|
|
769
|
-
const
|
|
816
|
+
const detail = await readJson(`${base}/__402/catalog/providers/${providerFilename(providerFqn)}`);
|
|
817
|
+
const outputRoot = opt(options, "output-dir", "providers");
|
|
818
|
+
const target = opt(options, "output-dir")
|
|
819
|
+
? path.resolve(outputRoot)
|
|
820
|
+
: safeOutputPath("providers", providerFilename(providerFqn).replace(/\.json$/, ""));
|
|
770
821
|
fs.mkdirSync(target, { recursive: true });
|
|
771
822
|
const catalogPath = path.join(target, "catalog.json");
|
|
772
823
|
const payMdPath = path.join(target, "pay.md");
|
|
824
|
+
ensureWritable(catalogPath, options);
|
|
825
|
+
ensureWritable(payMdPath, options);
|
|
773
826
|
writeJson(catalogPath, submissionCatalog(detail));
|
|
774
827
|
fs.writeFileSync(payMdPath, payMarkdownFromDetail(detail));
|
|
775
828
|
emit({ command: "catalog export-gateway", mode: outputMode(options), result: { provider: providerFqn, catalog: catalogPath, payMd: payMdPath } });
|
|
@@ -790,7 +843,11 @@ function buildRequirement(options) {
|
|
|
790
843
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : registryToken.decimals;
|
|
791
844
|
if (!Number.isInteger(decimals) || decimals < 0)
|
|
792
845
|
throw new Error("--decimals must be a non-negative integer");
|
|
793
|
-
const
|
|
846
|
+
const rawAmount = opt(options, "rawAmount") ?? opt(options, "raw-amount");
|
|
847
|
+
const humanAmount = opt(options, "amount");
|
|
848
|
+
if (rawAmount && humanAmount)
|
|
849
|
+
throw new Error("--amount and --rawAmount are mutually exclusive");
|
|
850
|
+
const amount = rawAmount ? assertRawAmount(rawAmount, "--rawAmount") : toSmallestUnit(humanAmount ?? "0.0001", decimals);
|
|
794
851
|
const assetAddress = explicitAsset ?? registryToken.address;
|
|
795
852
|
const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
|
|
796
853
|
return {
|
|
@@ -829,6 +886,9 @@ function stripFlag(argv, flag) {
|
|
|
829
886
|
return argv.filter(item => item !== flag);
|
|
830
887
|
}
|
|
831
888
|
function serveDaemon(argv, options) {
|
|
889
|
+
const requirement = buildRequirement(options);
|
|
890
|
+
if (!requirement.payTo)
|
|
891
|
+
throw new Error("--pay-to is required");
|
|
832
892
|
const daemonArgs = stripFlag(stripFlag(argv, "--daemon"), "-d");
|
|
833
893
|
const script = fileURLToPath(import.meta.url);
|
|
834
894
|
const child = spawn(process.execPath, [script, ...daemonArgs], {
|
|
@@ -843,7 +903,7 @@ function serveDaemon(argv, options) {
|
|
|
843
903
|
emit({
|
|
844
904
|
command: "server",
|
|
845
905
|
mode: outputMode(options),
|
|
846
|
-
network:
|
|
906
|
+
network: requirement.network,
|
|
847
907
|
scheme: "exact",
|
|
848
908
|
result: {
|
|
849
909
|
pid: child.pid,
|
|
@@ -856,7 +916,7 @@ function serveDaemon(argv, options) {
|
|
|
856
916
|
function validateAmountLimits(selected, options) {
|
|
857
917
|
const maxRaw = opt(options, "max-rawAmount") ?? opt(options, "max-raw-amount");
|
|
858
918
|
const maxAmount = opt(options, "max-amount");
|
|
859
|
-
if (maxRaw && BigInt(selected.amount) > BigInt(maxRaw)) {
|
|
919
|
+
if (maxRaw && BigInt(selected.amount) > BigInt(assertRawAmount(maxRaw, "--max-rawAmount"))) {
|
|
860
920
|
throw new Error(`payment raw amount ${selected.amount} exceeds --max-rawAmount ${maxRaw}`);
|
|
861
921
|
}
|
|
862
922
|
if (maxAmount) {
|
|
@@ -889,17 +949,18 @@ async function serve(options) {
|
|
|
889
949
|
};
|
|
890
950
|
const server = http.createServer(async (request, response) => {
|
|
891
951
|
try {
|
|
892
|
-
|
|
952
|
+
const pathname = new URL(request.url ?? "/", `http://${host}:${port}`).pathname;
|
|
953
|
+
if (pathname === "/health") {
|
|
893
954
|
response.writeHead(200, { "content-type": "application/json" });
|
|
894
955
|
response.end(JSON.stringify({ ok: true }));
|
|
895
956
|
return;
|
|
896
957
|
}
|
|
897
|
-
if (
|
|
958
|
+
if (pathname === "/.well-known/x402") {
|
|
898
959
|
response.writeHead(200, { "content-type": "application/json" });
|
|
899
|
-
response.end(JSON.stringify({ network: requirement.network, scheme: "exact", asset: requirement.asset, amount: requirement.amount, payTo: requirement.payTo, pay_url: resourceUrl }));
|
|
960
|
+
response.end(JSON.stringify({ network: requirement.network, scheme: "exact", asset: requirement.asset, rawAmount: requirement.amount, amount: requirement.amount, payTo: requirement.payTo, pay_url: resourceUrl }));
|
|
900
961
|
return;
|
|
901
962
|
}
|
|
902
|
-
if (
|
|
963
|
+
if (pathname !== "/pay") {
|
|
903
964
|
response.writeHead(404).end("not found");
|
|
904
965
|
return;
|
|
905
966
|
}
|
|
@@ -912,22 +973,33 @@ async function serve(options) {
|
|
|
912
973
|
response.end(JSON.stringify(challenge));
|
|
913
974
|
return;
|
|
914
975
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
976
|
+
let payload;
|
|
977
|
+
try {
|
|
978
|
+
payload = decodeSignature(signature);
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
982
|
+
response.end(JSON.stringify({ error: "invalid payment signature" }));
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
918
985
|
const verify = await facilitatorPost(facilitatorUrl, "/verify", {
|
|
919
986
|
paymentPayload: payload,
|
|
920
987
|
paymentRequirements: requirement,
|
|
921
988
|
});
|
|
922
|
-
if (verify?.valid ===
|
|
989
|
+
if (!(verify?.valid === true || verify?.isValid === true)) {
|
|
923
990
|
response.writeHead(400, { "content-type": "application/json" });
|
|
924
|
-
response.end(JSON.stringify({ error: "payment verification failed"
|
|
991
|
+
response.end(JSON.stringify({ error: "payment verification failed" }));
|
|
925
992
|
return;
|
|
926
993
|
}
|
|
927
994
|
const settle = await facilitatorPost(facilitatorUrl, "/settle", {
|
|
928
995
|
paymentPayload: payload,
|
|
929
996
|
paymentRequirements: requirement,
|
|
930
997
|
});
|
|
998
|
+
if (!(settle?.success === true || settle?.settled === true || typeof settle?.transaction === "string" || typeof settle?.txHash === "string")) {
|
|
999
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
1000
|
+
response.end(JSON.stringify({ error: "settlement failed" }));
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
931
1003
|
response.writeHead(200, {
|
|
932
1004
|
"content-type": "application/json",
|
|
933
1005
|
[headers.response]: encodeResponse(settle),
|
|
@@ -939,13 +1011,18 @@ async function serve(options) {
|
|
|
939
1011
|
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
940
1012
|
}
|
|
941
1013
|
});
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1014
|
+
await new Promise((resolve, reject) => {
|
|
1015
|
+
server.once("error", reject);
|
|
1016
|
+
server.listen(port, host, () => {
|
|
1017
|
+
server.off("error", reject);
|
|
1018
|
+
emit({
|
|
1019
|
+
command: "server",
|
|
1020
|
+
network: requirement.network,
|
|
1021
|
+
scheme: "exact",
|
|
1022
|
+
mode: outputMode(options),
|
|
1023
|
+
result: { pay_url: resourceUrl, token: opt(options, "token", "USDT"), rawAmount: requirement.amount, pay_to: requirement.payTo },
|
|
1024
|
+
});
|
|
1025
|
+
resolve();
|
|
949
1026
|
});
|
|
950
1027
|
});
|
|
951
1028
|
}
|
|
@@ -1025,7 +1102,7 @@ async function pay(url, options) {
|
|
|
1025
1102
|
const paid = await fetch(url, {
|
|
1026
1103
|
method,
|
|
1027
1104
|
headers: retryHeaders,
|
|
1028
|
-
body: opt(options, "body"),
|
|
1105
|
+
body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : opt(options, "body"),
|
|
1029
1106
|
});
|
|
1030
1107
|
const body = await responsePayload(paid);
|
|
1031
1108
|
const paymentResponse = paid.headers.get(headers.response);
|
|
@@ -1281,7 +1358,7 @@ async function searchCatalog(source, query, options) {
|
|
|
1281
1358
|
}
|
|
1282
1359
|
return hits
|
|
1283
1360
|
.sort((a, b) => b.score - a.score || a.fqn.localeCompare(b.fqn))
|
|
1284
|
-
.slice(0,
|
|
1361
|
+
.slice(0, positiveIntegerOption(options, "limit", 10));
|
|
1285
1362
|
}
|
|
1286
1363
|
function searchHitToJson(hit) {
|
|
1287
1364
|
return {
|
package/dist/gateway/cli.js
CHANGED
|
File without changes
|
package/dist/tokens.js
CHANGED
|
@@ -92,8 +92,19 @@ export function findTokenByAddress(network, address) {
|
|
|
92
92
|
const lower = address.toLowerCase();
|
|
93
93
|
return Object.values(TOKENS[normalizeNetwork(network)] ?? {}).find(token => token.address.toLowerCase() === lower);
|
|
94
94
|
}
|
|
95
|
+
export function assertRawAmount(value, name = "raw amount") {
|
|
96
|
+
if (!/^\d+$/.test(value))
|
|
97
|
+
throw new Error(`${name} must be a non-negative integer string`);
|
|
98
|
+
return value.replace(/^0+(?=\d)/, "");
|
|
99
|
+
}
|
|
95
100
|
export function toSmallestUnit(amount, decimals) {
|
|
101
|
+
if (!Number.isInteger(decimals) || decimals < 0)
|
|
102
|
+
throw new Error("token decimals must be a non-negative integer");
|
|
103
|
+
if (!/^\d+(\.\d+)?$/.test(amount))
|
|
104
|
+
throw new Error("amount must be a non-negative decimal string");
|
|
96
105
|
const [whole, fraction = ""] = amount.split(".");
|
|
106
|
+
if (fraction.length > decimals)
|
|
107
|
+
throw new Error(`amount has more than ${decimals} decimal places`);
|
|
97
108
|
const padded = (fraction + "0".repeat(decimals)).slice(0, decimals);
|
|
98
|
-
return (BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(padded || "0")).toString();
|
|
109
|
+
return assertRawAmount((BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(padded || "0")).toString());
|
|
99
110
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bankofai/x402-cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@bankofai/x402-core": "1.0.0",
|
|
24
24
|
"@bankofai/x402-evm": "1.0.0",
|
|
25
|
+
"@bankofai/x402-gateway": "^1.0.0-beta.1",
|
|
25
26
|
"@bankofai/x402-tron": "1.0.0",
|
|
26
|
-
"tronweb": "
|
|
27
|
-
"viem": "^2.
|
|
27
|
+
"tronweb": "6.0.2",
|
|
28
|
+
"viem": "^2.55.0",
|
|
28
29
|
"yaml": "^2.8.2"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|