@bankofai/x402-cli 1.0.1-beta.8 → 1.0.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/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
+ }
@@ -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";
@@ -0,0 +1,34 @@
1
+ export class FixedWindowRateLimiter {
2
+ limit;
3
+ windowMs;
4
+ cleanupIntervalMs;
5
+ entries = new Map();
6
+ nextCleanupAt = 0;
7
+ constructor(limit, windowMs = 60_000, cleanupIntervalMs = 60_000) {
8
+ this.limit = limit;
9
+ this.windowMs = windowMs;
10
+ this.cleanupIntervalMs = cleanupIntervalMs;
11
+ }
12
+ consume(key, now = Date.now()) {
13
+ if (now >= this.nextCleanupAt) {
14
+ for (const [entryKey, entry] of this.entries) {
15
+ if (entry.resetAt <= now)
16
+ this.entries.delete(entryKey);
17
+ }
18
+ this.nextCleanupAt = now + this.cleanupIntervalMs;
19
+ }
20
+ let entry = this.entries.get(key);
21
+ if (!entry || entry.resetAt <= now) {
22
+ entry = { count: 0, resetAt: now + this.windowMs };
23
+ this.entries.set(key, entry);
24
+ }
25
+ entry.count += 1;
26
+ return {
27
+ allowed: entry.count <= this.limit,
28
+ retryAfter: Math.max(1, Math.ceil((entry.resetAt - now) / 1000)),
29
+ };
30
+ }
31
+ get size() {
32
+ return this.entries.size;
33
+ }
34
+ }
@@ -2,6 +2,7 @@ import http from "node:http";
2
2
  import { timingSafeEqual } from "node:crypto";
3
3
  import { URL } from "node:url";
4
4
  import { endpointFor, paymentRequirements, priceUsd } from "./config.js";
5
+ import { FixedWindowRateLimiter } from "./rate-limit.js";
5
6
  import { decodeSignature, encodeRequired, encodeResponse, headers, matchRequirement } from "./x402.js";
6
7
  class HttpError extends Error {
7
8
  status;
@@ -312,7 +313,7 @@ async function forward(metrics, entry, request, response, routePath, body, payme
312
313
  export function createGatewayServer(providers) {
313
314
  const publicBaseUrl = configuredPublicBaseUrl();
314
315
  const metrics = createMetrics();
315
- const rateLimits = new Map();
316
+ const rateLimiter = new FixedWindowRateLimiter(RATE_LIMIT_PER_MINUTE);
316
317
  let activeRequests = 0;
317
318
  const server = http.createServer(async (request, response) => {
318
319
  let countedActive = false;
@@ -384,16 +385,10 @@ export function createGatewayServer(providers) {
384
385
  }
385
386
  const now = Date.now();
386
387
  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) {
388
+ const rate = rateLimiter.consume(address, now);
389
+ if (!rate.allowed) {
394
390
  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) });
391
+ throw new HttpError(429, "gateway rate limited", undefined, { "retry-after": String(rate.retryAfter) });
397
392
  }
398
393
  if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
399
394
  metrics.rejectedRequests += 1;
@@ -0,0 +1,145 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import { fileURLToPath } from "node:url";
6
+ import { opt, outputMode } from "./args.js";
7
+ import { emit, printJson } from "./output.js";
8
+ import { buildGatewayCatalog, loadProviders, providerPaymentAssets } from "@bankofai/x402-gateway";
9
+ const require = createRequire(import.meta.url);
10
+ function executableInPath(name) {
11
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
12
+ if (!dir)
13
+ continue;
14
+ const candidate = path.join(dir, name);
15
+ try {
16
+ fs.accessSync(candidate, fs.constants.X_OK);
17
+ return candidate;
18
+ }
19
+ catch {
20
+ // Try the next PATH entry.
21
+ }
22
+ }
23
+ return undefined;
24
+ }
25
+ function resolveGatewayPackageRuntime() {
26
+ try {
27
+ return path.join(path.dirname(require.resolve("@bankofai/x402-gateway/package.json")), "dist", "cli.js");
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ function gatewayCommand(options) {
34
+ const explicit = opt(options, "gateway-bin");
35
+ const gatewayPackageRuntime = resolveGatewayPackageRuntime();
36
+ const candidates = [
37
+ explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
38
+ gatewayPackageRuntime ? { file: gatewayPackageRuntime, source: "@bankofai/x402-gateway dependency" } : undefined,
39
+ { file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
40
+ executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
41
+ { file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
42
+ ].filter(Boolean);
43
+ for (const candidate of candidates) {
44
+ try {
45
+ fs.accessSync(candidate.file, fs.constants.R_OK);
46
+ if (candidate.file.endsWith(".js")) {
47
+ return { command: process.execPath, argsPrefix: [candidate.file], source: candidate.source };
48
+ }
49
+ return { command: candidate.file, argsPrefix: [], source: candidate.source };
50
+ }
51
+ catch {
52
+ // Try the next candidate.
53
+ }
54
+ }
55
+ throw new Error("x402-gateway runtime not found. Install @bankofai/x402-gateway, run from a checkout with ../x402-gateway/dist/cli.js, or pass --gateway-bin <path>.");
56
+ }
57
+ export async function gatewayStart(args, options) {
58
+ const gateway = gatewayCommand(options);
59
+ const child = spawn(gateway.command, [...gateway.argsPrefix, ...args], {
60
+ stdio: "inherit",
61
+ env: process.env,
62
+ });
63
+ await new Promise((resolve, reject) => {
64
+ child.on("exit", code => code === 0 ? resolve() : reject(new Error(`gateway exited with ${code}`)));
65
+ child.on("error", reject);
66
+ });
67
+ }
68
+ export function gatewayCheck(target, options) {
69
+ const providers = loadProviders(target);
70
+ emit({
71
+ command: "gateway check",
72
+ mode: outputMode(options),
73
+ result: { providers: [...providers.keys()], count: providers.size },
74
+ });
75
+ }
76
+ export function gatewayScaffold(name, options) {
77
+ const outputDir = opt(options, "output-dir", path.join("providers", name));
78
+ const forwardUrl = opt(options, "forward-url", "https://api.example.com");
79
+ fs.mkdirSync(outputDir, { recursive: true });
80
+ const body = `name: ${name}
81
+ title: "${name}"
82
+ description: "x402 provider"
83
+ category: data
84
+ version: v1
85
+
86
+ forward_url: ${forwardUrl}
87
+
88
+ routing:
89
+ type: proxy
90
+
91
+ operator:
92
+ network: tron:0xcd8690dc
93
+ currencies:
94
+ usd: ["USDT"]
95
+ recipient: <provider-recipient-address>
96
+ scheme: exact
97
+ protocol: exact
98
+ asset_transfer_method: permit2
99
+ facilitator_url: https://facilitator.bankofai.io
100
+ facilitator_api_key: <facilitator-api-key>
101
+ valid_for_seconds: 300
102
+
103
+ endpoints:
104
+ - method: GET
105
+ path: /v1/ping
106
+ metering:
107
+ dimensions:
108
+ - tiers:
109
+ - price_usd: 0.0001
110
+ `;
111
+ fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
112
+ emit({
113
+ command: "gateway scaffold",
114
+ mode: outputMode(options),
115
+ result: { file: path.join(outputDir, "provider.yml") },
116
+ });
117
+ }
118
+ export function catalogBuild(target, options) {
119
+ const providers = loadProviders(target);
120
+ const catalog = buildGatewayCatalog(providers);
121
+ const output = opt(options, "output", opt(options, "dist-dir") ? path.join(opt(options, "dist-dir"), "catalog.json") : undefined);
122
+ if (output) {
123
+ fs.mkdirSync(path.dirname(output), { recursive: true });
124
+ fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
125
+ emit({
126
+ command: "catalog build",
127
+ mode: outputMode(options),
128
+ result: { output, count: providers.size },
129
+ });
130
+ }
131
+ else if (outputMode(options) === "json") {
132
+ emit({ command: "catalog build", mode: "json", result: catalog });
133
+ }
134
+ else {
135
+ printJson(catalog);
136
+ }
137
+ }
138
+ export function catalogPayAssets(target, options) {
139
+ const rows = providerPaymentAssets(loadProviders(target));
140
+ emit({
141
+ command: "gateway catalog pay-assets",
142
+ mode: outputMode(options),
143
+ result: { count: rows.length, assets: rows },
144
+ });
145
+ }
package/dist/help.js ADDED
@@ -0,0 +1,169 @@
1
+ import fs from "node:fs";
2
+ export function getVersion() {
3
+ try {
4
+ const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
5
+ return String(pkg.version ?? "0.0.0");
6
+ }
7
+ catch {
8
+ return "0.0.0";
9
+ }
10
+ }
11
+ export function helpText(topic = "root") {
12
+ const sections = {
13
+ root: `x402-cli ${getVersion()}
14
+
15
+ Usage:
16
+ x402-cli <command> [options]
17
+
18
+ Commands:
19
+ pay <url> Pay an x402-protected URL
20
+ serve Run a local x402 paywall endpoint
21
+ roundtrip Start serve, pay it, then exit
22
+ gateway <command> Manage local gateway provider files
23
+ catalog <command> Search, cache, and export provider catalog assets
24
+
25
+ Global options:
26
+ -h, --help Show help
27
+ -V, --version Show version
28
+ --json Print machine-readable JSON envelope
29
+ --human Print human-readable output (default)
30
+ `,
31
+ pay: `Usage:
32
+ x402-cli pay <url> [options]
33
+
34
+ Options:
35
+ --method <method> HTTP method (default: GET)
36
+ --header "Name: Value" Request header, repeatable
37
+ --body <body> Request body for non-GET/HEAD methods
38
+ --network <caip2> Require a specific network
39
+ --token <symbol> Require a specific token
40
+ --scheme <scheme> Require a specific x402 scheme
41
+ --gasfree-api-url <url> Override the TRON GasFree relayer API URL
42
+ --max-gasfree-fee <amt> Maximum GasFree relayer fee in token units
43
+ --max-gasfree-fee-raw <n> Maximum GasFree relayer fee in smallest units
44
+ --max-amount <amount> Maximum human-readable payment amount
45
+ --max-raw-amount <amount> Maximum smallest-unit payment amount
46
+ --dry-run Read requirements but do not sign or pay
47
+ --private-key <hex> Explicit payer private key (or PRIVATE_KEY/TRON_PRIVATE_KEY/EVM_PRIVATE_KEY)
48
+ --rpc-url <url> Explicit network RPC URL
49
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
50
+ --json Print JSON envelope
51
+
52
+ Examples:
53
+ x402-cli pay https://api.example.com/paid --dry-run --json
54
+ x402-cli pay https://api.example.com/paid --max-amount 0.01
55
+ `,
56
+ serve: `Usage:
57
+ x402-cli serve --pay-to <address> [options]
58
+
59
+ Options:
60
+ --pay-to <address> Recipient wallet address
61
+ --amount <amount> Human-readable token amount (default: 0.0001)
62
+ --raw-amount <amount> Smallest-unit amount
63
+ --network <caip2> Payment network (default: tron:0xcd8690dc)
64
+ --scheme <scheme> Payment scheme: exact or exact_gasfree (default: exact)
65
+ --token <symbol> Token symbol (default: USDT)
66
+ --asset <address> Explicit token address
67
+ --decimals <count> Token decimals for unregistered --asset
68
+ --host <host> Bind host (default: 127.0.0.1)
69
+ --port <port> Bind port (default: 4020)
70
+ --resource-url <url> URL advertised in payment requirements
71
+ --facilitator-url <url> Facilitator base URL
72
+ --timeout-ms <ms> Facilitator timeout in milliseconds (default: 30000)
73
+ --daemon Run in background and print the child pid
74
+ --json Print JSON envelope
75
+
76
+ Examples:
77
+ x402-cli serve --pay-to T... --network tron:0xcd8690dc --token USDT
78
+ x402-cli serve --pay-to 0x... --network eip155:97 --token USDT --amount 0.0001
79
+ `,
80
+ roundtrip: `Usage:
81
+ x402-cli roundtrip --pay-to <address> [serve/pay options]
82
+ `,
83
+ gateway: `Usage:
84
+ x402-cli gateway <search|start|check|scaffold|catalog> [options]
85
+
86
+ Commands:
87
+ search <query> Search a gateway/catalog artifact
88
+ start Start a local x402 gateway process
89
+ check <providers> Validate provider.yml files
90
+ scaffold <name> Write a starter provider.yml
91
+ catalog <command> Build/check/search gateway catalog assets
92
+ `,
93
+ "gateway-catalog": `Usage:
94
+ x402-cli gateway catalog <build|check|pay-assets|search> [options]
95
+
96
+ Commands:
97
+ build <providers> Build a local catalog from provider.yml files
98
+ check <providers> Validate local provider.yml files
99
+ pay-assets <providers> List payable endpoint assets
100
+ search <query> Search a catalog artifact
101
+ `,
102
+ catalog: `Usage:
103
+ x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build> [options]
104
+
105
+ Commands:
106
+ update Cache hosted/local catalog assets under ~/.cache
107
+ search <query> Search providers
108
+ show <provider> Show provider detail JSON
109
+ endpoints <provider> List provider endpoints
110
+ pay-json <provider> Print provider pay JSON
111
+ export-gateway <url> Export catalog.json and pay.md from a gateway
112
+ build <providers> Build catalog from provider.yml files
113
+
114
+ Options:
115
+ --catalog <source> catalog.json path or URL
116
+ --provider <fqn> Provider FQN for export-gateway
117
+ --output-dir <dir> Output directory for generated files
118
+ -n, --limit <count> Search result limit
119
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
120
+ --include-blocked Include blocked providers in search
121
+ --json Print JSON envelope
122
+ `,
123
+ "catalog-search": `Usage:
124
+ x402-cli catalog search <query> [--catalog <source>] [options]
125
+
126
+ Options:
127
+ --catalog <source> catalog.json path or URL
128
+ -n, --limit <count> Search result limit
129
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
130
+ --include-blocked Include blocked providers in search
131
+ --json Print JSON envelope
132
+ `,
133
+ "catalog-show": `Usage:
134
+ x402-cli catalog show <provider> [--catalog <source>] [options]
135
+
136
+ Options:
137
+ --catalog <source> catalog.json path or URL
138
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
139
+ --json Print JSON envelope
140
+ `,
141
+ "catalog-pay-json": `Usage:
142
+ x402-cli catalog pay-json <provider> [--catalog <source>] [options]
143
+
144
+ Options:
145
+ --catalog <source> catalog.json path or URL
146
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
147
+ --raw Print raw pay payload instead of JSON envelope
148
+ --json Print JSON envelope
149
+ `,
150
+ "catalog-endpoints": `Usage:
151
+ x402-cli catalog endpoints <provider> [--catalog <source>] [options]
152
+
153
+ Options:
154
+ --catalog <source> catalog.json path or URL
155
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
156
+ --json Print JSON envelope
157
+ `,
158
+ "catalog-export-gateway": `Usage:
159
+ x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]
160
+
161
+ Options:
162
+ --provider <fqn> Provider FQN to export
163
+ --output-dir <dir> Output directory for generated files
164
+ --force Overwrite existing files
165
+ --json Print JSON envelope
166
+ `,
167
+ };
168
+ return sections[topic] ?? sections.root;
169
+ }
@@ -0,0 +1,82 @@
1
+ import fs from "node:fs";
2
+ import { CliError, opt } from "./args.js";
3
+ const MAX_HTTP_BODY_BYTES = 10 * 1024 * 1024;
4
+ const DEFAULT_TIMEOUT_MS = 30_000;
5
+ export function positiveIntegerOption(options, key, fallback) {
6
+ const value = opt(options, key, String(fallback));
7
+ if (!/^\d+$/.test(value))
8
+ throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
9
+ const parsed = Number(value);
10
+ if (!Number.isSafeInteger(parsed) || parsed <= 0)
11
+ throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
12
+ return parsed;
13
+ }
14
+ export function timeoutMs(options) {
15
+ return options ? positiveIntegerOption(options, "timeout-ms", DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
16
+ }
17
+ export async function fetchWithTimeout(input, init = {}, timeout = DEFAULT_TIMEOUT_MS, label = "request") {
18
+ const controller = new AbortController();
19
+ const timer = setTimeout(() => controller.abort(), timeout);
20
+ try {
21
+ return await fetch(input, { ...init, signal: controller.signal });
22
+ }
23
+ catch (error) {
24
+ if (error instanceof Error && error.name === "AbortError")
25
+ throw new Error(`${label} timed out after ${timeout}ms`);
26
+ throw error;
27
+ }
28
+ finally {
29
+ clearTimeout(timer);
30
+ }
31
+ }
32
+ export async function readBoundedText(response, label) {
33
+ const declared = Number(response.headers.get("content-length"));
34
+ if (Number.isFinite(declared) && declared > MAX_HTTP_BODY_BYTES)
35
+ throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
36
+ if (!response.body)
37
+ return "";
38
+ const reader = response.body.getReader();
39
+ const chunks = [];
40
+ let size = 0;
41
+ for (;;) {
42
+ const { done, value } = await reader.read();
43
+ if (done)
44
+ break;
45
+ size += value.byteLength;
46
+ if (size > MAX_HTTP_BODY_BYTES) {
47
+ await reader.cancel();
48
+ throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
49
+ }
50
+ chunks.push(value);
51
+ }
52
+ const bytes = new Uint8Array(size);
53
+ let offset = 0;
54
+ for (const chunk of chunks) {
55
+ bytes.set(chunk, offset);
56
+ offset += chunk.byteLength;
57
+ }
58
+ return new TextDecoder().decode(bytes);
59
+ }
60
+ export async function readText(source, options) {
61
+ if (!source.startsWith("http://") && !source.startsWith("https://"))
62
+ return fs.readFileSync(source, "utf8");
63
+ const response = await fetchWithTimeout(source, {}, timeoutMs(options), `fetch ${source}`);
64
+ if (!response.ok)
65
+ throw new Error(`failed to fetch ${source}: ${response.status}`);
66
+ return readBoundedText(response, `response from ${source}`);
67
+ }
68
+ export async function readJson(source, options) {
69
+ return JSON.parse(await readText(source, options));
70
+ }
71
+ export async function responsePayload(response) {
72
+ const text = await readBoundedText(response, "HTTP response");
73
+ if ((response.headers.get("content-type") ?? "").toLowerCase().includes("json")) {
74
+ try {
75
+ return JSON.parse(text);
76
+ }
77
+ catch {
78
+ return text;
79
+ }
80
+ }
81
+ return text;
82
+ }
package/dist/output.js ADDED
@@ -0,0 +1,91 @@
1
+ import { CliError } from "./args.js";
2
+ export function printJson(value) {
3
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
4
+ }
5
+ export function emit(args) {
6
+ const mode = args.mode ?? "human";
7
+ if (mode === "json") {
8
+ const envelope = { ok: !args.error, command: args.command };
9
+ if (args.network)
10
+ envelope.network = args.network;
11
+ if (args.scheme)
12
+ envelope.scheme = args.scheme;
13
+ if (args.error)
14
+ envelope.error = args.error;
15
+ else
16
+ envelope.result = args.result ?? null;
17
+ printJson(envelope);
18
+ return;
19
+ }
20
+ if (args.error) {
21
+ process.stderr.write(`ERROR ${args.command}: ${args.error.code}\n`);
22
+ process.stderr.write(` ${args.error.message}\n`);
23
+ if (args.error.hint)
24
+ process.stderr.write(` hint: ${args.error.hint}\n`);
25
+ if (args.error.details !== undefined)
26
+ process.stderr.write(` details: ${JSON.stringify(args.error.details)}\n`);
27
+ return;
28
+ }
29
+ const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
30
+ process.stdout.write(`OK ${args.command}${suffix ? ` (${suffix})` : ""}\n`);
31
+ if (args.result && typeof args.result === "object" && !Array.isArray(args.result)) {
32
+ for (const [key, value] of Object.entries(args.result)) {
33
+ if (value === undefined)
34
+ continue;
35
+ process.stdout.write(value && typeof value === "object" ? ` ${key}: ${JSON.stringify(value)}\n` : ` ${key}: ${value}\n`);
36
+ }
37
+ }
38
+ else if (args.result !== undefined)
39
+ process.stdout.write(` ${args.result}\n`);
40
+ }
41
+ export function classify(error) {
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ if (error instanceof CliError)
44
+ return { code: error.code, message, hint: error.hint, details: error.details };
45
+ const lower = message.toLowerCase();
46
+ if (lower.includes("missing private key") || lower.includes("could not find a wallet"))
47
+ return { code: "WALLET_NOT_CONFIGURED", message, hint: "Set PRIVATE_KEY, TRON_PRIVATE_KEY, EVM_PRIVATE_KEY, or configure agent-wallet with a payer wallet." };
48
+ if (lower.includes("wallets_config") || lower.includes("wallet config"))
49
+ return { code: "WALLET_CONFIG_CORRUPT", message, hint: "Check ~/.agent-wallet/wallets_config.json or recreate the local agent-wallet configuration." };
50
+ if (lower.includes("does not exist") && lower.includes("account [t"))
51
+ return { code: "TRON_ACCOUNT_NOT_ACTIVATED", message, hint: "Activate the TRON address by sending it a small amount of TRX before signing contract calls." };
52
+ if (lower.includes("permit2_insufficient_balance") || lower.includes("insufficient") && lower.includes("balance"))
53
+ return { code: "INSUFFICIENT_TOKEN_BALANCE", message, hint: "Fund the payer address with the exact token and network advertised by the provider, then retry." };
54
+ if (lower.includes("transfer_from_failed") || lower.includes("transferfrom failed"))
55
+ return { code: "TOKEN_TRANSFER_FAILED", message, hint: "Check token balance, token contract, payer address, and that the selected x402 route matches the provider requirement." };
56
+ if (lower.includes("insufficient funds for gas") || lower.includes("insufficient gas") || lower.includes("energy"))
57
+ return { code: "INSUFFICIENT_GAS", message, hint: "Fund the payer address with the native gas token for this network." };
58
+ if (lower.includes("deadline") || lower.includes("expired"))
59
+ return { code: "DEADLINE_OR_CLOCK_SKEW", message, hint: "Check local clock sync and retry with a fresh payment requirement." };
60
+ if (lower.includes("permittransferfrom") || lower.includes("invalid signature") || lower.includes("permit reverted"))
61
+ return { code: "PERMIT_REVERTED", message, hint: "The token or Permit2 contract rejected the signature; retry with a fresh requirement and verify token/network support." };
62
+ if (lower.includes("tokenregistry") && lower.includes("import"))
63
+ return { code: "SDK_API_DRIFT", message, hint: "Installed x402 SDK packages do not match this CLI; reinstall @bankofai/x402-cli and SDK dependencies." };
64
+ if (lower.includes("429") || lower.includes("too many requests") || lower.includes("rate limit"))
65
+ return { code: "RATE_LIMITED", message, hint: "Wait briefly and retry; the upstream service or RPC is rate limiting requests." };
66
+ if (lower.includes("402 response missing"))
67
+ return { code: "INVALID_X402_RESPONSE", message, hint: "The endpoint returned HTTP 402 without a PAYMENT-REQUIRED header." };
68
+ if (lower.includes("no matching payment requirement"))
69
+ return { code: "NO_MATCHING_PAYMENT_REQUIREMENT", message, hint: "Relax --network, --token, or --scheme, or use values offered by the provider." };
70
+ if (lower.includes("exceeds --max"))
71
+ return { code: "PAYMENT_AMOUNT_TOO_HIGH", message, hint: "Increase the max amount flag only if this provider price is expected." };
72
+ if (lower.includes("failed to fetch") || lower.includes("fetch failed") || lower.includes("econnrefused") || lower.includes("timed out"))
73
+ return { code: "NETWORK_ERROR", message, hint: "Check the URL, local server, proxy, and network connectivity." };
74
+ if (lower.includes(" is required") || lower.includes("must be") || lower.includes("invalid --") || lower.includes("mutually exclusive"))
75
+ return { code: lower.includes("required") ? "MISSING_ARGUMENT" : "INVALID_ARGUMENT", message, hint: "Run the command with --help to see valid usage and options." };
76
+ return { code: "IO_ERROR", message, hint: "Run with --json for structured output, and check the provider/gateway logs for details." };
77
+ }
78
+ export async function withSdkStdoutRedirect(enabled, fn) {
79
+ if (!enabled)
80
+ return fn();
81
+ const originalLog = console.log;
82
+ console.log = (...args) => {
83
+ process.stderr.write(`${args.map(arg => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ")}\n`);
84
+ };
85
+ try {
86
+ return await fn();
87
+ }
88
+ finally {
89
+ console.log = originalLog;
90
+ }
91
+ }