@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/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
+ }
@@ -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: config.operator.facilitator_url ||
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
- (config.operator.facilitator_api_key_env
115
- ? process.env[config.operator.facilitator_api_key_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
- const raw = String(config.operator.protocol || config.operator.scheme || "exact").toLowerCase();
123
- const normalized = raw.replace(/[-:\s]/g, "_");
124
- if (!["exact", "exact_permit", "permit2", "exact_permit2"].includes(normalized)) {
125
- throw new Error(`${file}: unsupported x402 protocol ${raw}; use exact + permit2`);
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
- return symbols.map(symbol => {
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: "exact",
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";