@bankofai/x402-cli 1.0.1-beta.0 → 1.0.1-beta.10
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 +56 -20
- package/dist/args.js +96 -0
- package/dist/catalog-commands.js +558 -0
- package/dist/cli.js +104 -1372
- package/dist/daemon.js +47 -0
- package/dist/gateway/catalog.js +56 -0
- package/dist/gateway/config.js +88 -18
- package/dist/gateway/index.js +3 -0
- package/dist/gateway/server.js +192 -53
- package/dist/gateway/tokens.js +19 -7
- package/dist/gateway-commands.js +145 -0
- package/dist/help.js +169 -0
- package/dist/http-client.js +82 -0
- package/dist/output.js +91 -0
- package/dist/tokens.js +22 -10
- package/dist/x402.js +25 -11
- package/package.json +12 -8
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { opt, outputMode } from "./args.js";
|
|
5
|
+
import { emit } from "./output.js";
|
|
6
|
+
function stripFlag(argv, flag) {
|
|
7
|
+
return argv.filter(item => item !== flag);
|
|
8
|
+
}
|
|
9
|
+
async function waitForPort(host, port, timeout = 5_000) {
|
|
10
|
+
const deadline = Date.now() + timeout;
|
|
11
|
+
while (Date.now() < deadline) {
|
|
12
|
+
const connected = await new Promise(resolve => {
|
|
13
|
+
const socket = net.createConnection({ host, port });
|
|
14
|
+
socket.setTimeout(250);
|
|
15
|
+
socket.once("connect", () => { socket.destroy(); resolve(true); });
|
|
16
|
+
socket.once("error", () => resolve(false));
|
|
17
|
+
socket.once("timeout", () => { socket.destroy(); resolve(false); });
|
|
18
|
+
});
|
|
19
|
+
if (connected)
|
|
20
|
+
return;
|
|
21
|
+
await delay(50);
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`daemon did not become ready on ${host}:${port}`);
|
|
24
|
+
}
|
|
25
|
+
export async function startServeDaemon(argv, options, requirement, script) {
|
|
26
|
+
if (!requirement.payTo)
|
|
27
|
+
throw new Error("--pay-to is required");
|
|
28
|
+
const daemonArgs = stripFlag(stripFlag(argv, "--daemon"), "-d");
|
|
29
|
+
const child = spawn(process.execPath, [script, ...daemonArgs], { detached: true, stdio: "ignore", env: process.env });
|
|
30
|
+
child.unref();
|
|
31
|
+
const host = opt(options, "host", "127.0.0.1");
|
|
32
|
+
const port = Number(opt(options, "port", "4020"));
|
|
33
|
+
const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
|
|
34
|
+
try {
|
|
35
|
+
await waitForPort(host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "::1" : host, port);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (child.pid)
|
|
39
|
+
try {
|
|
40
|
+
process.kill(child.pid);
|
|
41
|
+
}
|
|
42
|
+
catch { /* child already exited */ }
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
emit({ command: "server", mode: outputMode(options), network: requirement.network, scheme: requirement.scheme,
|
|
46
|
+
result: { pid: child.pid, pay_url: resourceUrl, resource_url: resourceUrl, daemon: true } });
|
|
47
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { paymentRequirements, priceUsd } from "./config.js";
|
|
2
|
+
function configs(input) {
|
|
3
|
+
return input instanceof Map ? [...input.values()].map(entry => entry.config) : [...input];
|
|
4
|
+
}
|
|
5
|
+
export function providerCatalogProjection(provider) {
|
|
6
|
+
return {
|
|
7
|
+
name: provider.name,
|
|
8
|
+
title: provider.title ?? provider.name,
|
|
9
|
+
description: "",
|
|
10
|
+
category: "other",
|
|
11
|
+
network: provider.operator.network,
|
|
12
|
+
currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
|
|
13
|
+
endpoints: (provider.endpoints ?? []).map(endpoint => {
|
|
14
|
+
const price = priceUsd(endpoint);
|
|
15
|
+
const requirements = paymentRequirements(provider, price);
|
|
16
|
+
return {
|
|
17
|
+
method: endpoint.method.toUpperCase(),
|
|
18
|
+
path: `/providers/${provider.name}${endpoint.path}`,
|
|
19
|
+
upstream_path: endpoint.path,
|
|
20
|
+
description: "",
|
|
21
|
+
paid: price > 0 ? {
|
|
22
|
+
scheme: requirements[0]?.scheme,
|
|
23
|
+
network: requirements[0]?.network,
|
|
24
|
+
currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
|
|
25
|
+
price_usd: price,
|
|
26
|
+
} : null,
|
|
27
|
+
x402_routes: requirements.map(requirement => ({
|
|
28
|
+
provider: provider.name,
|
|
29
|
+
network: requirement.network,
|
|
30
|
+
scheme: requirement.scheme,
|
|
31
|
+
...(requirement.extra?.assetTransferMethod ? { assetTransferMethod: requirement.extra.assetTransferMethod } : {}),
|
|
32
|
+
url: `/providers/${provider.name}${endpoint.path}`,
|
|
33
|
+
})),
|
|
34
|
+
};
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function buildGatewayCatalog(input) {
|
|
39
|
+
return { version: 1, generatedAt: new Date().toISOString(), providers: configs(input).map(providerCatalogProjection) };
|
|
40
|
+
}
|
|
41
|
+
export function providerPaymentAssets(input) {
|
|
42
|
+
return configs(input).flatMap(provider => (provider.endpoints ?? []).flatMap(endpoint => {
|
|
43
|
+
const price = priceUsd(endpoint);
|
|
44
|
+
const currencies = provider.operator.currencies?.usd ?? ["USDT"];
|
|
45
|
+
return paymentRequirements(provider, price).map((requirement, index) => ({
|
|
46
|
+
provider: provider.name,
|
|
47
|
+
method: endpoint.method,
|
|
48
|
+
path: `/providers/${provider.name}${endpoint.path}`,
|
|
49
|
+
network: requirement.network,
|
|
50
|
+
currency: currencies[index % currencies.length],
|
|
51
|
+
price_usd: price,
|
|
52
|
+
scheme: requirement.scheme,
|
|
53
|
+
...(requirement.extra?.assetTransferMethod ? { assetTransferMethod: requirement.extra.assetTransferMethod } : {}),
|
|
54
|
+
}));
|
|
55
|
+
}));
|
|
56
|
+
}
|
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,30 +116,83 @@ 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);
|
|
120
|
+
const facilitatorUrl = config.operator.facilitator_url ||
|
|
121
|
+
process.env.X402_FACILITATOR_URL ||
|
|
122
|
+
process.env.FACILITATOR_URL ||
|
|
123
|
+
"https://facilitator.bankofai.io";
|
|
124
|
+
assertHttpUrl(facilitatorUrl, `${file}: facilitator URL`);
|
|
125
|
+
const configuredApiKeyEnv = config.operator.facilitator_api_key_env;
|
|
126
|
+
if (configuredApiKeyEnv && !process.env[configuredApiKeyEnv]) {
|
|
127
|
+
throw new Error(`${file}: environment variable ${configuredApiKeyEnv} is not set`);
|
|
128
|
+
}
|
|
107
129
|
return {
|
|
108
130
|
config,
|
|
109
|
-
facilitatorUrl
|
|
110
|
-
process.env.X402_FACILITATOR_URL ||
|
|
111
|
-
process.env.FACILITATOR_URL ||
|
|
112
|
-
"https://facilitator.bankofai.io",
|
|
131
|
+
facilitatorUrl,
|
|
113
132
|
facilitatorApiKey: config.operator.facilitator_api_key ||
|
|
114
|
-
(
|
|
115
|
-
? process.env[
|
|
133
|
+
(configuredApiKeyEnv
|
|
134
|
+
? process.env[configuredApiKeyEnv]
|
|
116
135
|
: undefined) ||
|
|
117
136
|
process.env.X402_FACILITATOR_API_KEY ||
|
|
118
137
|
process.env.FACILITATOR_API_KEY,
|
|
119
138
|
};
|
|
120
139
|
}
|
|
121
140
|
function normalizePaymentProtocol(config, file) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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"];
|
|
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`);
|
|
148
|
+
const raw = String(value).toLowerCase();
|
|
149
|
+
const normalized = raw.replace(/[-:\s]/g, "_");
|
|
150
|
+
if (!["exact", "exact_gasfree", "exact_permit", "permit2", "exact_permit2"].includes(normalized)) {
|
|
151
|
+
throw new Error(`${file}: unsupported x402 protocol ${raw}; use exact or exact_gasfree`);
|
|
152
|
+
}
|
|
153
|
+
return normalized === "exact_gasfree" ? "exact_gasfree" : "exact";
|
|
154
|
+
}))];
|
|
155
|
+
if (schemes.includes("exact_gasfree") && !config.operator.network.startsWith("tron:")) {
|
|
156
|
+
throw new Error(`${file}: exact_gasfree is supported only on TRON networks`);
|
|
157
|
+
}
|
|
158
|
+
config.operator.schemes = schemes;
|
|
159
|
+
config.operator.scheme = schemes[0];
|
|
160
|
+
config.operator.protocol = schemes[0];
|
|
161
|
+
if (schemes.includes("exact")) {
|
|
162
|
+
config.operator.asset_transfer_method = "permit2";
|
|
163
|
+
config.operator.assetTransferMethod = "permit2";
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
delete config.operator.asset_transfer_method;
|
|
167
|
+
delete config.operator.assetTransferMethod;
|
|
168
|
+
}
|
|
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`);
|
|
126
195
|
}
|
|
127
|
-
config.operator.scheme = "exact";
|
|
128
|
-
config.operator.protocol = "exact";
|
|
129
|
-
config.operator.asset_transfer_method = "permit2";
|
|
130
|
-
config.operator.assetTransferMethod = "permit2";
|
|
131
196
|
}
|
|
132
197
|
export function loadProviders(providerPath) {
|
|
133
198
|
const stat = fs.statSync(providerPath);
|
|
@@ -152,6 +217,8 @@ export function endpointFor(provider, method, routePath) {
|
|
|
152
217
|
return provider.endpoints?.find(endpoint => endpoint.method.toUpperCase() === method.toUpperCase() && pathMatches(endpoint.path, routePath));
|
|
153
218
|
}
|
|
154
219
|
function pathMatches(template, routePath) {
|
|
220
|
+
if (!routePath.startsWith("/") || routePath.startsWith("//") || routePath.includes("\\") || routePath.includes("\0") || /%(?:2f|5c)/i.test(routePath))
|
|
221
|
+
return false;
|
|
155
222
|
const templateParts = template.split("/").filter(Boolean);
|
|
156
223
|
const routeParts = routePath.split("/").filter(Boolean);
|
|
157
224
|
if (templateParts.length !== routeParts.length)
|
|
@@ -178,20 +245,23 @@ export function paymentRequirements(provider, price) {
|
|
|
178
245
|
const network = normalizeNetwork(provider.operator.network);
|
|
179
246
|
const symbols = provider.operator.currencies?.usd ?? ["USDT"];
|
|
180
247
|
const payTo = provider.recipients?.[provider.operator.recipient]?.account ?? provider.operator.recipient;
|
|
181
|
-
|
|
248
|
+
const schemes = provider.operator.schemes?.length
|
|
249
|
+
? provider.operator.schemes.map(scheme => scheme === "exact_gasfree" ? "exact_gasfree" : "exact")
|
|
250
|
+
: [provider.operator.scheme === "exact_gasfree" ? "exact_gasfree" : "exact"];
|
|
251
|
+
return schemes.flatMap(scheme => symbols.map(symbol => {
|
|
182
252
|
const token = getToken(network, symbol);
|
|
183
253
|
const transferMethod = provider.operator.assetTransferMethod || provider.operator.asset_transfer_method || token.assetTransferMethod;
|
|
184
254
|
const amount = toSmallestUnit(price, token.decimals);
|
|
185
255
|
if (amount === "0")
|
|
186
256
|
throw new Error(`positive price produced zero amount for ${symbol} on ${network}`);
|
|
187
257
|
return {
|
|
188
|
-
scheme
|
|
258
|
+
scheme,
|
|
189
259
|
network,
|
|
190
260
|
amount,
|
|
191
261
|
asset: token.address,
|
|
192
262
|
payTo,
|
|
193
263
|
maxTimeoutSeconds: provider.operator.valid_for_seconds ?? 300,
|
|
194
|
-
extra: transferMethod === "permit2" ? { assetTransferMethod: "permit2" } : {},
|
|
264
|
+
extra: scheme === "exact" && transferMethod === "permit2" ? { assetTransferMethod: "permit2" } : {},
|
|
195
265
|
};
|
|
196
|
-
});
|
|
266
|
+
}));
|
|
197
267
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { endpointFor, loadProvider, loadProviders, paymentRequirements, priceUsd, } from "./config.js";
|
|
2
|
+
export { getToken, normalizeNetwork, toSmallestUnit, TOKENS, } from "./tokens.js";
|
|
3
|
+
export { buildGatewayCatalog, providerCatalogProjection, providerPaymentAssets } from "./catalog.js";
|