@bankofai/x402-cli 1.0.1-beta.7 → 1.0.1-beta.9
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/args.js +96 -0
- package/dist/catalog-commands.js +558 -0
- package/dist/cli.js +49 -1372
- package/dist/daemon.js +47 -0
- package/dist/gateway/catalog.js +56 -0
- package/dist/gateway/cli.js +0 -0
- package/dist/gateway/config.js +48 -3
- package/dist/gateway/index.js +3 -0
- package/dist/gateway/server.js +95 -18
- 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/provider-config.js +94 -0
- package/dist/tokens.js +2 -2
- package/dist/x402.js +8 -2
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -1,979 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import http from "node:http";
|
|
3
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
3
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import YAML from "yaml";
|
|
11
4
|
import { createPaymentPayload, decodeRequired, decodeResponse, decodeSignature, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
|
|
12
5
|
import { assertRawAmount, findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
exitCode;
|
|
21
|
-
details;
|
|
22
|
-
constructor(code, message, hint, exitCode = 1, details) {
|
|
23
|
-
super(message);
|
|
24
|
-
this.code = code;
|
|
25
|
-
this.hint = hint;
|
|
26
|
-
this.exitCode = exitCode;
|
|
27
|
-
this.details = details;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function parseArgs(argv) {
|
|
31
|
-
const [command = "help", ...rest] = argv;
|
|
32
|
-
const positional = [];
|
|
33
|
-
const options = {};
|
|
34
|
-
for (let i = 0; i < rest.length; i += 1) {
|
|
35
|
-
const item = rest[i];
|
|
36
|
-
if (item === "-h") {
|
|
37
|
-
options.help = true;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (item === "-V") {
|
|
41
|
-
options.version = true;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (item === "-d") {
|
|
45
|
-
options.daemon = true;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (item === "-n") {
|
|
49
|
-
const next = rest[i + 1];
|
|
50
|
-
if (!next || next.startsWith("-")) {
|
|
51
|
-
options.limit = true;
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
options.limit = next;
|
|
55
|
-
i += 1;
|
|
56
|
-
}
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (!item.startsWith("--")) {
|
|
60
|
-
positional.push(item);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const eq = item.indexOf("=");
|
|
64
|
-
const key = eq > 2 ? item.slice(2, eq) : item.slice(2);
|
|
65
|
-
const inline = eq > 2 ? item.slice(eq + 1) : undefined;
|
|
66
|
-
const next = rest[i + 1];
|
|
67
|
-
if (inline !== undefined) {
|
|
68
|
-
options[key] = inline;
|
|
69
|
-
}
|
|
70
|
-
else if (BOOLEAN_FLAGS.has(key)) {
|
|
71
|
-
options[key] = true;
|
|
72
|
-
}
|
|
73
|
-
else if (!next || next.startsWith("--")) {
|
|
74
|
-
options[key] = true;
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
if (key === "header") {
|
|
78
|
-
const current = options[key];
|
|
79
|
-
options[key] = Array.isArray(current) ? [...current, next] : current ? [String(current), next] : [next];
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
options[key] = next;
|
|
83
|
-
}
|
|
84
|
-
i += 1;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return { command, positional, options };
|
|
88
|
-
}
|
|
89
|
-
function getVersion() {
|
|
90
|
-
try {
|
|
91
|
-
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
92
|
-
return String(pkg.version ?? "0.0.0");
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return "0.0.0";
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
function opt(options, key, fallback) {
|
|
99
|
-
const value = options[key];
|
|
100
|
-
return typeof value === "string" ? value : fallback;
|
|
101
|
-
}
|
|
102
|
-
function hasFlag(options, key) {
|
|
103
|
-
return options[key] === true;
|
|
104
|
-
}
|
|
105
|
-
function outputMode(options) {
|
|
106
|
-
if (hasFlag(options, "json") && hasFlag(options, "human")) {
|
|
107
|
-
throw new CliError("INVALID_ARGUMENT", "--json and --human are mutually exclusive", "Pass either --json or --human, not both.", 2);
|
|
108
|
-
}
|
|
109
|
-
return hasFlag(options, "json") ? "json" : "human";
|
|
110
|
-
}
|
|
111
|
-
function requireArgument(value, name, usage) {
|
|
112
|
-
if (value === undefined || value === "") {
|
|
113
|
-
throw new CliError("MISSING_ARGUMENT", `${name} is required`, `Usage: ${usage}`, 2);
|
|
114
|
-
}
|
|
115
|
-
return value;
|
|
116
|
-
}
|
|
117
|
-
function optAll(options, key) {
|
|
118
|
-
const value = options[key];
|
|
119
|
-
if (Array.isArray(value))
|
|
120
|
-
return value;
|
|
121
|
-
return typeof value === "string" ? [value] : [];
|
|
122
|
-
}
|
|
123
|
-
function printJson(value) {
|
|
124
|
-
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
125
|
-
}
|
|
126
|
-
function emit(args) {
|
|
127
|
-
const mode = args.mode ?? "human";
|
|
128
|
-
if (mode === "json") {
|
|
129
|
-
const envelope = {
|
|
130
|
-
ok: !args.error,
|
|
131
|
-
command: args.command,
|
|
132
|
-
};
|
|
133
|
-
if (args.network)
|
|
134
|
-
envelope.network = args.network;
|
|
135
|
-
if (args.scheme)
|
|
136
|
-
envelope.scheme = args.scheme;
|
|
137
|
-
if (args.error)
|
|
138
|
-
envelope.error = args.error;
|
|
139
|
-
else
|
|
140
|
-
envelope.result = args.result ?? null;
|
|
141
|
-
printJson(envelope);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (args.error) {
|
|
145
|
-
process.stderr.write(`ERROR ${args.command}: ${args.error.code}\n`);
|
|
146
|
-
process.stderr.write(` ${args.error.message}\n`);
|
|
147
|
-
if (args.error.hint)
|
|
148
|
-
process.stderr.write(` hint: ${args.error.hint}\n`);
|
|
149
|
-
if (args.error.details !== undefined)
|
|
150
|
-
process.stderr.write(` details: ${JSON.stringify(args.error.details)}\n`);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
|
|
154
|
-
process.stdout.write(`OK ${args.command}${suffix ? ` (${suffix})` : ""}\n`);
|
|
155
|
-
if (args.result && typeof args.result === "object" && !Array.isArray(args.result)) {
|
|
156
|
-
for (const [key, value] of Object.entries(args.result)) {
|
|
157
|
-
if (value === undefined)
|
|
158
|
-
continue;
|
|
159
|
-
if (value && typeof value === "object") {
|
|
160
|
-
process.stdout.write(` ${key}: ${JSON.stringify(value)}\n`);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
process.stdout.write(` ${key}: ${value}\n`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
else if (args.result !== undefined) {
|
|
168
|
-
process.stdout.write(` ${args.result}\n`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
function classify(error) {
|
|
172
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
-
if (error instanceof CliError) {
|
|
174
|
-
return {
|
|
175
|
-
code: error.code,
|
|
176
|
-
message,
|
|
177
|
-
hint: error.hint,
|
|
178
|
-
details: error.details,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const lower = message.toLowerCase();
|
|
182
|
-
if (lower.includes("missing private key") || lower.includes("could not find a wallet")) {
|
|
183
|
-
return {
|
|
184
|
-
code: "WALLET_NOT_CONFIGURED",
|
|
185
|
-
message,
|
|
186
|
-
hint: "Set PRIVATE_KEY, TRON_PRIVATE_KEY, EVM_PRIVATE_KEY, or configure agent-wallet with a payer wallet.",
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
if (lower.includes("wallets_config") || lower.includes("wallet config")) {
|
|
190
|
-
return {
|
|
191
|
-
code: "WALLET_CONFIG_CORRUPT",
|
|
192
|
-
message,
|
|
193
|
-
hint: "Check ~/.agent-wallet/wallets_config.json or recreate the local agent-wallet configuration.",
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
if (lower.includes("does not exist") && lower.includes("account [t")) {
|
|
197
|
-
return {
|
|
198
|
-
code: "TRON_ACCOUNT_NOT_ACTIVATED",
|
|
199
|
-
message,
|
|
200
|
-
hint: "Activate the TRON address by sending it a small amount of TRX before signing contract calls.",
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
if (lower.includes("permit2_insufficient_balance") || lower.includes("insufficient") && lower.includes("balance")) {
|
|
204
|
-
return {
|
|
205
|
-
code: "INSUFFICIENT_TOKEN_BALANCE",
|
|
206
|
-
message,
|
|
207
|
-
hint: "Fund the payer address with the exact token and network advertised by the provider, then retry.",
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
if (lower.includes("transfer_from_failed") || lower.includes("transferfrom failed")) {
|
|
211
|
-
return {
|
|
212
|
-
code: "TOKEN_TRANSFER_FAILED",
|
|
213
|
-
message,
|
|
214
|
-
hint: "Check token balance, token contract, payer address, and that the selected x402 route matches the provider requirement.",
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
if (lower.includes("insufficient funds for gas") || lower.includes("insufficient gas") || lower.includes("energy")) {
|
|
218
|
-
return {
|
|
219
|
-
code: "INSUFFICIENT_GAS",
|
|
220
|
-
message,
|
|
221
|
-
hint: "Fund the payer address with the native gas token for this network.",
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
if (lower.includes("deadline") || lower.includes("expired")) {
|
|
225
|
-
return {
|
|
226
|
-
code: "DEADLINE_OR_CLOCK_SKEW",
|
|
227
|
-
message,
|
|
228
|
-
hint: "Check local clock sync and retry with a fresh payment requirement.",
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
if (lower.includes("permittransferfrom") || lower.includes("invalid signature") || lower.includes("permit reverted")) {
|
|
232
|
-
return {
|
|
233
|
-
code: "PERMIT_REVERTED",
|
|
234
|
-
message,
|
|
235
|
-
hint: "The token or Permit2 contract rejected the signature; retry with a fresh requirement and verify token/network support.",
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
if (lower.includes("tokenregistry") && lower.includes("import")) {
|
|
239
|
-
return {
|
|
240
|
-
code: "SDK_API_DRIFT",
|
|
241
|
-
message,
|
|
242
|
-
hint: "Installed x402 SDK packages do not match this CLI; reinstall @bankofai/x402-cli and SDK dependencies.",
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
if (lower.includes("429") || lower.includes("too many requests") || lower.includes("rate limit")) {
|
|
246
|
-
return {
|
|
247
|
-
code: "RATE_LIMITED",
|
|
248
|
-
message,
|
|
249
|
-
hint: "Wait briefly and retry; the upstream service or RPC is rate limiting requests.",
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
if (lower.includes("402 response missing")) {
|
|
253
|
-
return {
|
|
254
|
-
code: "INVALID_X402_RESPONSE",
|
|
255
|
-
message,
|
|
256
|
-
hint: "The endpoint returned HTTP 402 without a PAYMENT-REQUIRED header.",
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
if (lower.includes("no matching payment requirement")) {
|
|
260
|
-
return {
|
|
261
|
-
code: "NO_MATCHING_PAYMENT_REQUIREMENT",
|
|
262
|
-
message,
|
|
263
|
-
hint: "Relax --network, --token, or --scheme, or use values offered by the provider.",
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
if (lower.includes("exceeds --max")) {
|
|
267
|
-
return {
|
|
268
|
-
code: "PAYMENT_AMOUNT_TOO_HIGH",
|
|
269
|
-
message,
|
|
270
|
-
hint: "Increase the max amount flag only if this provider price is expected.",
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
if (lower.includes("failed to fetch") || lower.includes("fetch failed") || lower.includes("econnrefused") || lower.includes("timed out")) {
|
|
274
|
-
return {
|
|
275
|
-
code: "NETWORK_ERROR",
|
|
276
|
-
message,
|
|
277
|
-
hint: "Check the URL, local server, proxy, and network connectivity.",
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
if (lower.includes(" is required") || lower.includes("must be") || lower.includes("invalid --") || lower.includes("mutually exclusive")) {
|
|
281
|
-
return {
|
|
282
|
-
code: lower.includes("required") ? "MISSING_ARGUMENT" : "INVALID_ARGUMENT",
|
|
283
|
-
message,
|
|
284
|
-
hint: "Run the command with --help to see valid usage and options.",
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
return {
|
|
288
|
-
code: "IO_ERROR",
|
|
289
|
-
message,
|
|
290
|
-
hint: "Run with --json for structured output, and check the provider/gateway logs for details.",
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
function helpText(topic = "root") {
|
|
294
|
-
const sections = {
|
|
295
|
-
root: `x402-cli ${getVersion()}
|
|
296
|
-
|
|
297
|
-
Usage:
|
|
298
|
-
x402-cli <command> [options]
|
|
299
|
-
|
|
300
|
-
Commands:
|
|
301
|
-
pay <url> Pay an x402-protected URL
|
|
302
|
-
serve Run a local x402 paywall endpoint
|
|
303
|
-
roundtrip Start serve, pay it, then exit
|
|
304
|
-
gateway <command> Manage local gateway provider files
|
|
305
|
-
catalog <command> Search, cache, and export provider catalog assets
|
|
306
|
-
|
|
307
|
-
Global options:
|
|
308
|
-
-h, --help Show help
|
|
309
|
-
-V, --version Show version
|
|
310
|
-
--json Print machine-readable JSON envelope
|
|
311
|
-
--human Print human-readable output (default)
|
|
312
|
-
`,
|
|
313
|
-
pay: `Usage:
|
|
314
|
-
x402-cli pay <url> [options]
|
|
315
|
-
|
|
316
|
-
Options:
|
|
317
|
-
--method <method> HTTP method (default: GET)
|
|
318
|
-
--header "Name: Value" Request header, repeatable
|
|
319
|
-
--body <body> Request body for non-GET/HEAD methods
|
|
320
|
-
--network <caip2> Require a specific network
|
|
321
|
-
--token <symbol> Require a specific token
|
|
322
|
-
--scheme <scheme> Require a specific x402 scheme
|
|
323
|
-
--gasfree-api-url <url> Override the TRON GasFree relayer API URL
|
|
324
|
-
--max-gasfree-fee <amt> Maximum GasFree relayer fee in token units
|
|
325
|
-
--max-gasfree-fee-raw <n> Maximum GasFree relayer fee in smallest units
|
|
326
|
-
--max-amount <amount> Maximum human-readable payment amount
|
|
327
|
-
--max-raw-amount <amount> Maximum smallest-unit payment amount
|
|
328
|
-
--dry-run Read requirements but do not sign or pay
|
|
329
|
-
--private-key <hex> Explicit payer private key (or PRIVATE_KEY/TRON_PRIVATE_KEY/EVM_PRIVATE_KEY)
|
|
330
|
-
--rpc-url <url> Explicit network RPC URL
|
|
331
|
-
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
332
|
-
--json Print JSON envelope
|
|
333
|
-
|
|
334
|
-
Examples:
|
|
335
|
-
x402-cli pay https://api.example.com/paid --dry-run --json
|
|
336
|
-
x402-cli pay https://api.example.com/paid --max-amount 0.01
|
|
337
|
-
`,
|
|
338
|
-
serve: `Usage:
|
|
339
|
-
x402-cli serve --pay-to <address> [options]
|
|
340
|
-
|
|
341
|
-
Options:
|
|
342
|
-
--pay-to <address> Recipient wallet address
|
|
343
|
-
--amount <amount> Human-readable token amount (default: 0.0001)
|
|
344
|
-
--raw-amount <amount> Smallest-unit amount
|
|
345
|
-
--network <caip2> Payment network (default: tron:0xcd8690dc)
|
|
346
|
-
--scheme <scheme> Payment scheme: exact or exact_gasfree (default: exact)
|
|
347
|
-
--token <symbol> Token symbol (default: USDT)
|
|
348
|
-
--asset <address> Explicit token address
|
|
349
|
-
--decimals <count> Token decimals for unregistered --asset
|
|
350
|
-
--host <host> Bind host (default: 127.0.0.1)
|
|
351
|
-
--port <port> Bind port (default: 4020)
|
|
352
|
-
--resource-url <url> URL advertised in payment requirements
|
|
353
|
-
--facilitator-url <url> Facilitator base URL
|
|
354
|
-
--timeout-ms <ms> Facilitator timeout in milliseconds (default: 30000)
|
|
355
|
-
--daemon Run in background and print the child pid
|
|
356
|
-
--json Print JSON envelope
|
|
357
|
-
|
|
358
|
-
Examples:
|
|
359
|
-
x402-cli serve --pay-to T... --network tron:0xcd8690dc --token USDT
|
|
360
|
-
x402-cli serve --pay-to 0x... --network eip155:97 --token USDT --amount 0.0001
|
|
361
|
-
`,
|
|
362
|
-
roundtrip: `Usage:
|
|
363
|
-
x402-cli roundtrip --pay-to <address> [serve/pay options]
|
|
364
|
-
`,
|
|
365
|
-
gateway: `Usage:
|
|
366
|
-
x402-cli gateway <search|start|check|scaffold|catalog> [options]
|
|
367
|
-
|
|
368
|
-
Commands:
|
|
369
|
-
search <query> Search a gateway/catalog artifact
|
|
370
|
-
start Start a local x402 gateway process
|
|
371
|
-
check <providers> Validate provider.yml files
|
|
372
|
-
scaffold <name> Write a starter provider.yml
|
|
373
|
-
catalog <command> Build/check/search gateway catalog assets
|
|
374
|
-
`,
|
|
375
|
-
"gateway-catalog": `Usage:
|
|
376
|
-
x402-cli gateway catalog <build|check|pay-assets|search> [options]
|
|
377
|
-
|
|
378
|
-
Commands:
|
|
379
|
-
build <providers> Build a local catalog from provider.yml files
|
|
380
|
-
check <providers> Validate local provider.yml files
|
|
381
|
-
pay-assets <providers> List payable endpoint assets
|
|
382
|
-
search <query> Search a catalog artifact
|
|
383
|
-
`,
|
|
384
|
-
catalog: `Usage:
|
|
385
|
-
x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build> [options]
|
|
386
|
-
|
|
387
|
-
Commands:
|
|
388
|
-
update Cache hosted/local catalog assets under ~/.cache
|
|
389
|
-
search <query> Search providers
|
|
390
|
-
show <provider> Show provider detail JSON
|
|
391
|
-
endpoints <provider> List provider endpoints
|
|
392
|
-
pay-json <provider> Print provider pay JSON
|
|
393
|
-
export-gateway <url> Export catalog.json and pay.md from a gateway
|
|
394
|
-
build <providers> Build catalog from provider.yml files
|
|
395
|
-
|
|
396
|
-
Options:
|
|
397
|
-
--catalog <source> catalog.json path or URL
|
|
398
|
-
--provider <fqn> Provider FQN for export-gateway
|
|
399
|
-
--output-dir <dir> Output directory for generated files
|
|
400
|
-
-n, --limit <count> Search result limit
|
|
401
|
-
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
402
|
-
--include-blocked Include blocked providers in search
|
|
403
|
-
--json Print JSON envelope
|
|
404
|
-
`,
|
|
405
|
-
"catalog-search": `Usage:
|
|
406
|
-
x402-cli catalog search <query> [--catalog <source>] [options]
|
|
407
|
-
|
|
408
|
-
Options:
|
|
409
|
-
--catalog <source> catalog.json path or URL
|
|
410
|
-
-n, --limit <count> Search result limit
|
|
411
|
-
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
412
|
-
--include-blocked Include blocked providers in search
|
|
413
|
-
--json Print JSON envelope
|
|
414
|
-
`,
|
|
415
|
-
"catalog-show": `Usage:
|
|
416
|
-
x402-cli catalog show <provider> [--catalog <source>] [options]
|
|
417
|
-
|
|
418
|
-
Options:
|
|
419
|
-
--catalog <source> catalog.json path or URL
|
|
420
|
-
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
421
|
-
--json Print JSON envelope
|
|
422
|
-
`,
|
|
423
|
-
"catalog-pay-json": `Usage:
|
|
424
|
-
x402-cli catalog pay-json <provider> [--catalog <source>] [options]
|
|
425
|
-
|
|
426
|
-
Options:
|
|
427
|
-
--catalog <source> catalog.json path or URL
|
|
428
|
-
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
429
|
-
--raw Print raw pay payload instead of JSON envelope
|
|
430
|
-
--json Print JSON envelope
|
|
431
|
-
`,
|
|
432
|
-
"catalog-endpoints": `Usage:
|
|
433
|
-
x402-cli catalog endpoints <provider> [--catalog <source>] [options]
|
|
434
|
-
|
|
435
|
-
Options:
|
|
436
|
-
--catalog <source> catalog.json path or URL
|
|
437
|
-
--timeout-ms <ms> Network timeout in milliseconds (default: 30000)
|
|
438
|
-
--json Print JSON envelope
|
|
439
|
-
`,
|
|
440
|
-
"catalog-export-gateway": `Usage:
|
|
441
|
-
x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]
|
|
442
|
-
|
|
443
|
-
Options:
|
|
444
|
-
--provider <fqn> Provider FQN to export
|
|
445
|
-
--output-dir <dir> Output directory for generated files
|
|
446
|
-
--force Overwrite existing files
|
|
447
|
-
--json Print JSON envelope
|
|
448
|
-
`,
|
|
449
|
-
};
|
|
450
|
-
return sections[topic] ?? sections.root;
|
|
451
|
-
}
|
|
452
|
-
function readYaml(file) {
|
|
453
|
-
return YAML.parse(fs.readFileSync(file, "utf8"));
|
|
454
|
-
}
|
|
455
|
-
function expandEnv(value) {
|
|
456
|
-
return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
457
|
-
if (!(name in process.env))
|
|
458
|
-
throw new Error(`environment variable \${${name}} is not set`);
|
|
459
|
-
return process.env[name] ?? "";
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
function expandDeep(value) {
|
|
463
|
-
if (typeof value === "string")
|
|
464
|
-
return expandEnv(value);
|
|
465
|
-
if (Array.isArray(value))
|
|
466
|
-
return value.map(expandDeep);
|
|
467
|
-
if (value && typeof value === "object") {
|
|
468
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandDeep(item)]));
|
|
469
|
-
}
|
|
470
|
-
return value;
|
|
471
|
-
}
|
|
472
|
-
function providerFiles(root) {
|
|
473
|
-
const stat = fs.statSync(root);
|
|
474
|
-
if (stat.isFile())
|
|
475
|
-
return [root];
|
|
476
|
-
const out = [];
|
|
477
|
-
for (const entry of fs.readdirSync(root, { recursive: true })) {
|
|
478
|
-
const file = path.join(root, String(entry));
|
|
479
|
-
if (file.endsWith("provider.yml") || file.endsWith("provider.yaml"))
|
|
480
|
-
out.push(file);
|
|
481
|
-
}
|
|
482
|
-
return out.sort();
|
|
483
|
-
}
|
|
484
|
-
function loadProviderFile(file) {
|
|
485
|
-
const provider = expandDeep(readYaml(file));
|
|
486
|
-
validateProvider(provider, file);
|
|
487
|
-
provider.operator.network = normalizeNetwork(provider.operator.network);
|
|
488
|
-
provider.operator.scheme = provider.operator.scheme ?? "exact";
|
|
489
|
-
return provider;
|
|
490
|
-
}
|
|
491
|
-
function validateProvider(provider, file = "provider.yml") {
|
|
492
|
-
const required = [
|
|
493
|
-
["name", provider?.name],
|
|
494
|
-
["forward_url", provider?.forward_url],
|
|
495
|
-
["operator.network", provider?.operator?.network],
|
|
496
|
-
["operator.recipient", provider?.operator?.recipient],
|
|
497
|
-
];
|
|
498
|
-
for (const [name, value] of required) {
|
|
499
|
-
if (typeof value !== "string" || !value.trim())
|
|
500
|
-
throw new Error(`${file}: ${name} is required`);
|
|
501
|
-
}
|
|
502
|
-
try {
|
|
503
|
-
const url = new URL(provider.forward_url);
|
|
504
|
-
if (!["http:", "https:"].includes(url.protocol))
|
|
505
|
-
throw new Error("unsupported protocol");
|
|
506
|
-
}
|
|
507
|
-
catch {
|
|
508
|
-
throw new Error(`${file}: forward_url must be a valid http(s) URL`);
|
|
509
|
-
}
|
|
510
|
-
if (!Array.isArray(provider.endpoints) || !provider.endpoints.length) {
|
|
511
|
-
throw new Error(`${file}: endpoints must contain at least one endpoint`);
|
|
512
|
-
}
|
|
513
|
-
const seen = new Set();
|
|
514
|
-
for (const endpoint of provider.endpoints) {
|
|
515
|
-
if (typeof endpoint.method !== "string" || typeof endpoint.path !== "string") {
|
|
516
|
-
throw new Error(`${file}: each endpoint needs method and path`);
|
|
517
|
-
}
|
|
518
|
-
if (!endpoint.method.trim() || !endpoint.path.trim() || !endpoint.path.startsWith("/")) {
|
|
519
|
-
throw new Error(`${file}: endpoint method/path must be non-empty and path must start with /`);
|
|
520
|
-
}
|
|
521
|
-
const price = providerPrice(endpoint);
|
|
522
|
-
if (!Number.isFinite(price) || price < 0)
|
|
523
|
-
throw new Error(`${file}: endpoint price_usd must be a finite number >= 0`);
|
|
524
|
-
const key = `${endpoint.method.toUpperCase()} ${endpoint.path}`;
|
|
525
|
-
if (seen.has(key))
|
|
526
|
-
throw new Error(`${file}: duplicate endpoint ${key}`);
|
|
527
|
-
seen.add(key);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
function providerPrice(endpoint) {
|
|
531
|
-
return endpoint?.metering?.dimensions?.[0]?.tiers?.[0]?.price_usd ?? 0;
|
|
532
|
-
}
|
|
533
|
-
function providerAssetTransferMethod(provider) {
|
|
534
|
-
return provider.operator?.asset_transfer_method ?? provider.operator?.assetTransferMethod ?? "permit2";
|
|
535
|
-
}
|
|
536
|
-
function providerScheme(provider) {
|
|
537
|
-
return provider.operator?.scheme ?? "exact";
|
|
538
|
-
}
|
|
539
|
-
function providerCatalog(provider) {
|
|
540
|
-
return {
|
|
541
|
-
name: provider.name,
|
|
542
|
-
title: provider.title ?? provider.name,
|
|
543
|
-
description: provider.description ?? "",
|
|
544
|
-
category: provider.category ?? "other",
|
|
545
|
-
service_url: provider.display?.service_url,
|
|
546
|
-
tags: provider.display?.tags ?? [],
|
|
547
|
-
network: normalizeNetwork(provider.operator.network),
|
|
548
|
-
currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
|
|
549
|
-
endpoints: (provider.endpoints ?? []).map((endpoint) => ({
|
|
550
|
-
method: endpoint.method.toUpperCase(),
|
|
551
|
-
path: `/providers/${provider.name}${endpoint.path}`,
|
|
552
|
-
upstream_path: endpoint.path,
|
|
553
|
-
description: endpoint.description ?? "",
|
|
554
|
-
paid: providerPrice(endpoint) > 0 ? {
|
|
555
|
-
scheme: providerScheme(provider),
|
|
556
|
-
network: normalizeNetwork(provider.operator.network),
|
|
557
|
-
currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
|
|
558
|
-
price_usd: providerPrice(endpoint),
|
|
559
|
-
} : null,
|
|
560
|
-
x402_routes: providerPrice(endpoint) > 0 ? [{
|
|
561
|
-
provider: provider.name,
|
|
562
|
-
network: normalizeNetwork(provider.operator.network),
|
|
563
|
-
scheme: providerScheme(provider),
|
|
564
|
-
assetTransferMethod: providerAssetTransferMethod(provider),
|
|
565
|
-
url: `/providers/${provider.name}${endpoint.path}`,
|
|
566
|
-
}] : [],
|
|
567
|
-
})),
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
const FIELD_WEIGHTS = {
|
|
571
|
-
fqn: 12,
|
|
572
|
-
title: 10,
|
|
573
|
-
tags: 8,
|
|
574
|
-
chain_kinds: 8,
|
|
575
|
-
chains: 8,
|
|
576
|
-
category: 6,
|
|
577
|
-
category_meta: 6,
|
|
578
|
-
endpoints: 6,
|
|
579
|
-
i18n: 5,
|
|
580
|
-
description: 4,
|
|
581
|
-
use_case: 4,
|
|
582
|
-
service_url: 2,
|
|
583
|
-
};
|
|
584
|
-
function stringList(value) {
|
|
585
|
-
return Array.isArray(value) ? value.filter(item => item != null).map(String) : [];
|
|
586
|
-
}
|
|
587
|
-
function dictValues(value) {
|
|
588
|
-
if (!value || typeof value !== "object")
|
|
589
|
-
return [];
|
|
590
|
-
const out = [];
|
|
591
|
-
for (const child of Object.values(value)) {
|
|
592
|
-
if (child && typeof child === "object" && !Array.isArray(child))
|
|
593
|
-
out.push(...dictValues(child));
|
|
594
|
-
else if (Array.isArray(child))
|
|
595
|
-
out.push(...child.filter(item => item != null).map(String));
|
|
596
|
-
else if (child != null)
|
|
597
|
-
out.push(String(child));
|
|
598
|
-
}
|
|
599
|
-
return out;
|
|
600
|
-
}
|
|
601
|
-
function chainMetaValues(chainsMeta) {
|
|
602
|
-
return chainsMeta.flatMap(dictValues);
|
|
603
|
-
}
|
|
604
|
-
function endpointFields(endpoints) {
|
|
605
|
-
const values = [];
|
|
606
|
-
for (const endpoint of endpoints) {
|
|
607
|
-
values.push(String(endpoint.method ?? ""), String(endpoint.path ?? ""), String(endpoint.probe_status ?? ""));
|
|
608
|
-
const paid = endpoint.paid;
|
|
609
|
-
if (paid && typeof paid === "object") {
|
|
610
|
-
values.push(String(paid.network ?? ""), String(paid.currency ?? ""), String(paid.amount_raw ?? ""));
|
|
611
|
-
}
|
|
612
|
-
values.push(String(endpoint.title ?? ""), String(endpoint.description ?? ""), String(endpoint.use_case ?? endpoint.useCase ?? ""));
|
|
613
|
-
}
|
|
614
|
-
return values;
|
|
615
|
-
}
|
|
616
|
-
function scoreFields(terms, fields) {
|
|
617
|
-
let score = 0;
|
|
618
|
-
const matchedFields = [];
|
|
619
|
-
for (const [field, values] of Object.entries(fields)) {
|
|
620
|
-
const haystack = values.filter(Boolean).join(" ").toLowerCase();
|
|
621
|
-
if (!haystack)
|
|
622
|
-
continue;
|
|
623
|
-
const count = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0);
|
|
624
|
-
if (count) {
|
|
625
|
-
score += (FIELD_WEIGHTS[field] ?? 1) * count;
|
|
626
|
-
matchedFields.push(field);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
return { score, matchedFields };
|
|
630
|
-
}
|
|
631
|
-
async function readCatalog(source, options) {
|
|
632
|
-
const text = await readText(source, options);
|
|
633
|
-
const parsed = JSON.parse(text);
|
|
634
|
-
if (Array.isArray(parsed))
|
|
635
|
-
return parsed;
|
|
636
|
-
if (Array.isArray(parsed.providers))
|
|
637
|
-
return parsed.providers;
|
|
638
|
-
if (Array.isArray(parsed.items))
|
|
639
|
-
return parsed.items;
|
|
640
|
-
return [];
|
|
641
|
-
}
|
|
642
|
-
async function readCatalogObject(source, options) {
|
|
643
|
-
const parsed = JSON.parse(await readText(source, options));
|
|
644
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
645
|
-
throw new Error(`expected JSON object from ${source}`);
|
|
646
|
-
}
|
|
647
|
-
return parsed;
|
|
648
|
-
}
|
|
649
|
-
async function readText(source, options) {
|
|
650
|
-
if (!source.startsWith("http://") && !source.startsWith("https://")) {
|
|
651
|
-
return fs.readFileSync(source, "utf8");
|
|
652
|
-
}
|
|
653
|
-
const response = await fetchWithTimeout(source, {}, timeoutMs(options), `fetch ${source}`);
|
|
654
|
-
if (!response.ok)
|
|
655
|
-
throw new Error(`failed to fetch ${source}: ${response.status}`);
|
|
656
|
-
return response.text();
|
|
657
|
-
}
|
|
658
|
-
async function readJson(source, options) {
|
|
659
|
-
return JSON.parse(await readText(source, options));
|
|
660
|
-
}
|
|
661
|
-
function writeJson(file, value) {
|
|
662
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
663
|
-
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
664
|
-
}
|
|
665
|
-
async function responsePayload(response) {
|
|
666
|
-
const text = await response.text();
|
|
667
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
668
|
-
if (contentType.toLowerCase().includes("json")) {
|
|
669
|
-
try {
|
|
670
|
-
return JSON.parse(text);
|
|
671
|
-
}
|
|
672
|
-
catch {
|
|
673
|
-
return text;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
return text;
|
|
677
|
-
}
|
|
678
|
-
async function withSdkStdoutRedirect(enabled, fn) {
|
|
679
|
-
if (!enabled)
|
|
680
|
-
return fn();
|
|
681
|
-
const originalLog = console.log;
|
|
682
|
-
console.log = (...args) => {
|
|
683
|
-
process.stderr.write(`${args.map(arg => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ")}\n`);
|
|
684
|
-
};
|
|
685
|
-
try {
|
|
686
|
-
return await fn();
|
|
687
|
-
}
|
|
688
|
-
finally {
|
|
689
|
-
console.log = originalLog;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
function cacheDir() {
|
|
693
|
-
return path.join(os.homedir(), ".cache", "x402-cli", "catalog");
|
|
694
|
-
}
|
|
695
|
-
function cachedCatalogPath() {
|
|
696
|
-
return path.join(cacheDir(), "catalog.json");
|
|
697
|
-
}
|
|
698
|
-
function providerFilename(fqn) {
|
|
699
|
-
return `${fqn.replace(/\//g, "__")}.json`;
|
|
700
|
-
}
|
|
701
|
-
function sanitizeProviderName(name) {
|
|
702
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(name) || name.includes("..")) {
|
|
703
|
-
throw new Error("provider name must be a safe FQN using letters, numbers, dots, underscores, dashes, or slashes");
|
|
704
|
-
}
|
|
705
|
-
return name;
|
|
706
|
-
}
|
|
707
|
-
function safeOutputPath(baseDir, ...parts) {
|
|
708
|
-
const root = path.resolve(baseDir);
|
|
709
|
-
const target = path.resolve(root, ...parts);
|
|
710
|
-
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
|
|
711
|
-
throw new Error(`refusing to write outside output directory: ${target}`);
|
|
712
|
-
}
|
|
713
|
-
return target;
|
|
714
|
-
}
|
|
715
|
-
function ensureWritable(file, options) {
|
|
716
|
-
if (fs.existsSync(file) && !hasFlag(options, "force")) {
|
|
717
|
-
throw new Error(`${file} already exists; pass --force to overwrite`);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
function defaultCatalogSource() {
|
|
721
|
-
const envSource = process.env.X402_CATALOG || process.env.X402_GATEWAY_CATALOG;
|
|
722
|
-
if (envSource)
|
|
723
|
-
return envSource;
|
|
724
|
-
return fs.existsSync(cachedCatalogPath())
|
|
725
|
-
? cachedCatalogPath()
|
|
726
|
-
: "https://x402-catalog.bankofai.io/api/catalog.json";
|
|
727
|
-
}
|
|
728
|
-
function positiveIntegerOption(options, key, fallback) {
|
|
729
|
-
const value = opt(options, key, String(fallback));
|
|
730
|
-
if (!/^\d+$/.test(value))
|
|
731
|
-
throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
|
|
732
|
-
const parsed = Number(value);
|
|
733
|
-
if (!Number.isSafeInteger(parsed) || parsed <= 0)
|
|
734
|
-
throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
|
|
735
|
-
return parsed;
|
|
736
|
-
}
|
|
737
|
-
function timeoutMs(options) {
|
|
738
|
-
return options ? positiveIntegerOption(options, "timeout-ms", DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
739
|
-
}
|
|
740
|
-
async function fetchWithTimeout(input, init = {}, timeout = DEFAULT_TIMEOUT_MS, label = "request") {
|
|
741
|
-
const controller = new AbortController();
|
|
742
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
743
|
-
try {
|
|
744
|
-
return await fetch(input, { ...init, signal: controller.signal });
|
|
745
|
-
}
|
|
746
|
-
catch (error) {
|
|
747
|
-
if (error instanceof Error && error.name === "AbortError")
|
|
748
|
-
throw new Error(`${label} timed out after ${timeout}ms`);
|
|
749
|
-
throw error;
|
|
750
|
-
}
|
|
751
|
-
finally {
|
|
752
|
-
clearTimeout(timer);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
function remoteBaseFromCatalogPayload(payload) {
|
|
756
|
-
const base = payload.base_url ?? payload.baseUrl;
|
|
757
|
-
return typeof base === "string" && /^https?:\/\//.test(base) ? `${base.replace(/\/+$/, "")}/` : undefined;
|
|
758
|
-
}
|
|
759
|
-
async function remoteBaseFromSource(source, payload, options) {
|
|
760
|
-
const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
|
|
761
|
-
if (fromPayload)
|
|
762
|
-
return fromPayload;
|
|
763
|
-
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
764
|
-
return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
|
|
765
|
-
}
|
|
766
|
-
try {
|
|
767
|
-
return remoteBaseFromCatalogPayload(await readCatalogObject(source, options));
|
|
768
|
-
}
|
|
769
|
-
catch {
|
|
770
|
-
return undefined;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
function catalogDetailSource(source, section, name) {
|
|
774
|
-
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
775
|
-
const base = new URL(source);
|
|
776
|
-
const pathname = base.pathname.endsWith("/catalog.json")
|
|
777
|
-
? base.pathname.slice(0, -"catalog.json".length)
|
|
778
|
-
: base.pathname.endsWith("/")
|
|
779
|
-
? base.pathname
|
|
780
|
-
: `${base.pathname}/`;
|
|
781
|
-
base.pathname = `${pathname}${section}/${providerFilename(name)}`;
|
|
782
|
-
base.search = "";
|
|
783
|
-
base.hash = "";
|
|
784
|
-
return base.toString();
|
|
785
|
-
}
|
|
786
|
-
const stat = fs.existsSync(source) ? fs.statSync(source) : undefined;
|
|
787
|
-
const root = stat?.isDirectory() ? source : path.dirname(source);
|
|
788
|
-
const direct = path.join(root, section, `${name}.json`);
|
|
789
|
-
if (fs.existsSync(direct))
|
|
790
|
-
return direct;
|
|
791
|
-
return path.join(root, section, providerFilename(name));
|
|
792
|
-
}
|
|
793
|
-
async function readCatalogProvider(source, name, options) {
|
|
794
|
-
const providers = await readCatalog(source, options);
|
|
795
|
-
const summary = providers.find((item) => item.name === name || item.fqn === name);
|
|
796
|
-
if (!summary)
|
|
797
|
-
throw new Error(`provider not found: ${name}`);
|
|
798
|
-
const fqn = summary.fqn ?? summary.name ?? name;
|
|
799
|
-
try {
|
|
800
|
-
return await readJson(catalogDetailSource(source, "providers", fqn), options);
|
|
801
|
-
}
|
|
802
|
-
catch {
|
|
803
|
-
return summary;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
async function readCatalogPayProvider(source, name, options) {
|
|
807
|
-
const providers = await readCatalog(source, options);
|
|
808
|
-
const summary = providers.find((item) => item.name === name || item.fqn === name);
|
|
809
|
-
const fqn = summary?.fqn ?? summary?.name ?? name;
|
|
810
|
-
try {
|
|
811
|
-
return await readJson(catalogDetailSource(source, "pay", fqn), options);
|
|
812
|
-
}
|
|
813
|
-
catch {
|
|
814
|
-
if (summary)
|
|
815
|
-
return readCatalogProvider(source, name, options);
|
|
816
|
-
throw new Error(`provider not found: ${name}`);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
async function cacheProviderAssets(source, catalogPayload, options) {
|
|
820
|
-
const base = await remoteBaseFromSource(source, catalogPayload, options);
|
|
821
|
-
const warnings = [];
|
|
822
|
-
if (!base)
|
|
823
|
-
return { detailCount: 0, payCount: 0, warnings };
|
|
824
|
-
let detailCount = 0;
|
|
825
|
-
let payCount = 0;
|
|
826
|
-
for (const provider of catalogPayload.providers ?? []) {
|
|
827
|
-
const fqn = provider?.fqn ?? provider?.name;
|
|
828
|
-
if (typeof fqn !== "string" || !fqn)
|
|
829
|
-
continue;
|
|
830
|
-
const filename = providerFilename(fqn);
|
|
831
|
-
try {
|
|
832
|
-
const detail = await readJson(new URL(`providers/${filename}`, base).toString(), options);
|
|
833
|
-
writeJson(path.join(cacheDir(), "providers", filename), detail);
|
|
834
|
-
detailCount += 1;
|
|
835
|
-
}
|
|
836
|
-
catch (error) {
|
|
837
|
-
warnings.push(`failed to cache provider detail ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
838
|
-
}
|
|
839
|
-
try {
|
|
840
|
-
const pay = await readJson(new URL(`pay/${filename}`, base).toString(), options);
|
|
841
|
-
writeJson(path.join(cacheDir(), "pay", filename), pay);
|
|
842
|
-
payCount += 1;
|
|
843
|
-
}
|
|
844
|
-
catch (error) {
|
|
845
|
-
warnings.push(`failed to cache pay JSON ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
return { detailCount, payCount, warnings };
|
|
849
|
-
}
|
|
850
|
-
async function catalogUpdate(source, options) {
|
|
851
|
-
let payload;
|
|
852
|
-
const warnings = [];
|
|
853
|
-
for (let attempt = 1; attempt <= CATALOG_UPDATE_RETRIES; attempt += 1) {
|
|
854
|
-
try {
|
|
855
|
-
payload = await readCatalogObject(source, options);
|
|
856
|
-
break;
|
|
857
|
-
}
|
|
858
|
-
catch (error) {
|
|
859
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
860
|
-
if (attempt === CATALOG_UPDATE_RETRIES)
|
|
861
|
-
throw error;
|
|
862
|
-
warnings.push(`catalog update attempt ${attempt} failed: ${message}`);
|
|
863
|
-
await delay(250 * attempt);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
if (!payload)
|
|
867
|
-
throw new Error(`failed to read catalog from ${source}`);
|
|
868
|
-
writeJson(cachedCatalogPath(), payload);
|
|
869
|
-
const cached = await cacheProviderAssets(source, payload, options);
|
|
870
|
-
const result = {
|
|
871
|
-
source,
|
|
872
|
-
path: cachedCatalogPath(),
|
|
873
|
-
providerCount: payload.provider_count ?? payload.providerCount ?? (payload.providers ?? []).length,
|
|
874
|
-
detailCount: cached.detailCount,
|
|
875
|
-
payCount: cached.payCount,
|
|
876
|
-
warnings: [...warnings, ...cached.warnings],
|
|
877
|
-
};
|
|
878
|
-
emit({ command: "catalog update", mode: outputMode(options), result });
|
|
879
|
-
}
|
|
880
|
-
function zhCopy(title, subtitle, description, useCase) {
|
|
881
|
-
return { title, subtitle, description, useCase };
|
|
882
|
-
}
|
|
883
|
-
function submissionCatalog(detail) {
|
|
884
|
-
const title = String(detail.title ?? detail.fqn);
|
|
885
|
-
const subtitle = String(detail.subtitle ?? detail.use_case ?? title);
|
|
886
|
-
const description = String(detail.description ?? subtitle);
|
|
887
|
-
const useCase = String(detail.use_case ?? detail.useCase ?? description);
|
|
888
|
-
return {
|
|
889
|
-
version: 1,
|
|
890
|
-
fqn: detail.fqn,
|
|
891
|
-
title,
|
|
892
|
-
subtitle,
|
|
893
|
-
description,
|
|
894
|
-
useCase,
|
|
895
|
-
i18n: detail.i18n ?? { "zh-CN": zhCopy(title, subtitle, description, useCase) },
|
|
896
|
-
logo: detail.logo ?? "https://x402-catalog.bankofai.io/assets/providers/default.png",
|
|
897
|
-
category: detail.category ?? "other",
|
|
898
|
-
chains: detail.chains ?? [],
|
|
899
|
-
isFirstParty: Boolean(detail.is_first_party ?? detail.isFirstParty),
|
|
900
|
-
isFeatured: Boolean(detail.is_featured ?? detail.isFeatured),
|
|
901
|
-
featuredTags: detail.featured_tags ?? detail.featuredTags ?? [],
|
|
902
|
-
serviceUrl: detail.service_url ?? detail.serviceUrl,
|
|
903
|
-
endpoints: (detail.endpoints ?? []).map((endpoint) => {
|
|
904
|
-
const endpointTitle = endpoint.title ?? endpoint.path;
|
|
905
|
-
const endpointSubtitle = endpoint.subtitle ?? endpoint.path;
|
|
906
|
-
const endpointDescription = endpoint.description ?? description;
|
|
907
|
-
const endpointUseCase = endpoint.use_case ?? endpoint.useCase ?? useCase;
|
|
908
|
-
return {
|
|
909
|
-
method: endpoint.method,
|
|
910
|
-
path: endpoint.path,
|
|
911
|
-
url: endpoint.url,
|
|
912
|
-
title: endpointTitle,
|
|
913
|
-
subtitle: endpointSubtitle,
|
|
914
|
-
description: endpointDescription,
|
|
915
|
-
useCase: endpointUseCase,
|
|
916
|
-
i18n: endpoint.i18n ?? { "zh-CN": zhCopy(endpointTitle, endpointSubtitle, endpointDescription, endpointUseCase) },
|
|
917
|
-
metered: Boolean(endpoint.metered),
|
|
918
|
-
minPriceUsd: endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0,
|
|
919
|
-
maxPriceUsd: endpoint.max_price_usd ?? endpoint.maxPriceUsd ?? 0,
|
|
920
|
-
};
|
|
921
|
-
}),
|
|
922
|
-
status: detail.status ?? {
|
|
923
|
-
catalog: "draft",
|
|
924
|
-
gateway: "unknown",
|
|
925
|
-
payment: "unknown",
|
|
926
|
-
upstream: "unknown",
|
|
927
|
-
},
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
function payMarkdownFromDetail(detail) {
|
|
931
|
-
const lines = [
|
|
932
|
-
`# ${detail.title ?? detail.fqn}`,
|
|
933
|
-
"",
|
|
934
|
-
"## Service",
|
|
935
|
-
"",
|
|
936
|
-
`- FQN: \`${detail.fqn}\``,
|
|
937
|
-
`- Service URL: \`${detail.service_url ?? detail.serviceUrl ?? ""}\``,
|
|
938
|
-
`- Category: \`${detail.category ?? ""}\``,
|
|
939
|
-
`- Chains: \`${(detail.chains ?? []).join(", ")}\``,
|
|
940
|
-
"",
|
|
941
|
-
"## Endpoints",
|
|
942
|
-
"",
|
|
943
|
-
];
|
|
944
|
-
for (const endpoint of detail.endpoints ?? []) {
|
|
945
|
-
const metered = Boolean(endpoint.metered);
|
|
946
|
-
const price = endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0;
|
|
947
|
-
lines.push(`### ${endpoint.method} ${endpoint.path}`, "", endpoint.description ?? "", "", `- URL: \`${endpoint.url ?? ""}\``, `- Metered: \`${String(metered)}\``, `- Price: \`$${price}\``, "");
|
|
948
|
-
if (metered) {
|
|
949
|
-
lines.push("```bash", `x402-cli pay '${endpoint.url ?? ""}'`, "```", "");
|
|
950
|
-
}
|
|
951
|
-
else {
|
|
952
|
-
lines.push("No payment required.", "");
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
lines.push("## Notes", "", "This file is public. Do not include upstream API keys, bearer tokens, provider.yml, `.env`, passwords, or private infrastructure URLs.", "");
|
|
956
|
-
return lines.join("\n");
|
|
957
|
-
}
|
|
958
|
-
async function catalogExportGateway(gatewayUrl, options) {
|
|
959
|
-
requireArgument(gatewayUrl, "gateway-url", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
|
|
960
|
-
const providerFqn = requireArgument(opt(options, "provider"), "--provider", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
|
|
961
|
-
sanitizeProviderName(providerFqn);
|
|
962
|
-
const base = gatewayUrl.replace(/\/+$/, "");
|
|
963
|
-
const detail = await readJson(`${base}/__402/catalog/providers/${providerFilename(providerFqn)}`, options);
|
|
964
|
-
const outputRoot = opt(options, "output-dir", "providers");
|
|
965
|
-
const target = opt(options, "output-dir")
|
|
966
|
-
? path.resolve(outputRoot)
|
|
967
|
-
: safeOutputPath("providers", providerFilename(providerFqn).replace(/\.json$/, ""));
|
|
968
|
-
fs.mkdirSync(target, { recursive: true });
|
|
969
|
-
const catalogPath = path.join(target, "catalog.json");
|
|
970
|
-
const payMdPath = path.join(target, "pay.md");
|
|
971
|
-
ensureWritable(catalogPath, options);
|
|
972
|
-
ensureWritable(payMdPath, options);
|
|
973
|
-
writeJson(catalogPath, submissionCatalog(detail));
|
|
974
|
-
fs.writeFileSync(payMdPath, payMarkdownFromDetail(detail));
|
|
975
|
-
emit({ command: "catalog export-gateway", mode: outputMode(options), result: { provider: providerFqn, catalog: catalogPath, payMd: payMdPath } });
|
|
976
|
-
}
|
|
6
|
+
import { CliError, hasFlag, opt, optAll, outputMode, parseArgs, requireArgument } from "./args.js";
|
|
7
|
+
import { classify, emit, withSdkStdoutRedirect } from "./output.js";
|
|
8
|
+
import { fetchWithTimeout, readBoundedText, responsePayload, timeoutMs } from "./http-client.js";
|
|
9
|
+
import { startServeDaemon } from "./daemon.js";
|
|
10
|
+
import { getVersion, helpText } from "./help.js";
|
|
11
|
+
import { catalogBuild, catalogPayAssets, gatewayCheck, gatewayScaffold, gatewayStart } from "./gateway-commands.js";
|
|
12
|
+
import { catalogSearch, defaultCatalogSource, handleCatalog } from "./catalog-commands.js";
|
|
977
13
|
function buildRequirement(options) {
|
|
978
14
|
const network = normalizeNetwork(opt(options, "network", "tron:0xcd8690dc"));
|
|
979
15
|
const scheme = opt(options, "scheme", "exact");
|
|
@@ -994,8 +30,8 @@ function buildRequirement(options) {
|
|
|
994
30
|
if (!explicitAsset && !registryToken)
|
|
995
31
|
throw new Error(`unknown token ${tokenSymbol} on ${network}`);
|
|
996
32
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : registryToken.decimals;
|
|
997
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
998
|
-
throw new Error("--decimals must be
|
|
33
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
34
|
+
throw new Error("--decimals must be an integer between 0 and 255");
|
|
999
35
|
const rawAmount = opt(options, "rawAmount") ?? opt(options, "raw-amount");
|
|
1000
36
|
const humanAmount = opt(options, "amount");
|
|
1001
37
|
if (rawAmount && humanAmount)
|
|
@@ -1003,13 +39,17 @@ function buildRequirement(options) {
|
|
|
1003
39
|
const amount = rawAmount ? assertRawAmount(rawAmount, "--raw-amount") : toSmallestUnit(humanAmount ?? "0.0001", decimals);
|
|
1004
40
|
const assetAddress = explicitAsset ?? registryToken.address;
|
|
1005
41
|
const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
|
|
42
|
+
const maxTimeoutSeconds = Number(opt(options, "valid-for-seconds", "300"));
|
|
43
|
+
if (!Number.isInteger(maxTimeoutSeconds) || maxTimeoutSeconds <= 0 || maxTimeoutSeconds > 86400) {
|
|
44
|
+
throw new Error("--valid-for-seconds must be an integer between 1 and 86400");
|
|
45
|
+
}
|
|
1006
46
|
return {
|
|
1007
47
|
scheme,
|
|
1008
48
|
network,
|
|
1009
49
|
amount,
|
|
1010
50
|
asset: assetAddress,
|
|
1011
51
|
payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
|
|
1012
|
-
maxTimeoutSeconds
|
|
52
|
+
maxTimeoutSeconds,
|
|
1013
53
|
extra: scheme === "exact" && assetTransferMethod ? { assetTransferMethod } : {},
|
|
1014
54
|
};
|
|
1015
55
|
}
|
|
@@ -1019,10 +59,16 @@ async function facilitatorPost(baseUrl, path, body, options) {
|
|
|
1019
59
|
headers: { "content-type": "application/json" },
|
|
1020
60
|
body: JSON.stringify(body),
|
|
1021
61
|
}, timeoutMs(options), `facilitator ${path}`);
|
|
1022
|
-
const text = await response
|
|
1023
|
-
|
|
62
|
+
const text = await readBoundedText(response, `facilitator ${path} response`);
|
|
63
|
+
let data = {};
|
|
64
|
+
try {
|
|
65
|
+
data = text ? JSON.parse(text) : {};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new Error(`facilitator ${path} returned invalid JSON`);
|
|
69
|
+
}
|
|
1024
70
|
if (!response.ok)
|
|
1025
|
-
throw new Error(`facilitator ${path} failed
|
|
71
|
+
throw new Error(`facilitator ${path} failed with HTTP ${response.status}`);
|
|
1026
72
|
return data;
|
|
1027
73
|
}
|
|
1028
74
|
function requestHeaders(options) {
|
|
@@ -1035,37 +81,6 @@ function requestHeaders(options) {
|
|
|
1035
81
|
}
|
|
1036
82
|
return headersOut;
|
|
1037
83
|
}
|
|
1038
|
-
function stripFlag(argv, flag) {
|
|
1039
|
-
return argv.filter(item => item !== flag);
|
|
1040
|
-
}
|
|
1041
|
-
function serveDaemon(argv, options) {
|
|
1042
|
-
const requirement = buildRequirement(options);
|
|
1043
|
-
if (!requirement.payTo)
|
|
1044
|
-
throw new Error("--pay-to is required");
|
|
1045
|
-
const daemonArgs = stripFlag(stripFlag(argv, "--daemon"), "-d");
|
|
1046
|
-
const script = fileURLToPath(import.meta.url);
|
|
1047
|
-
const child = spawn(process.execPath, [script, ...daemonArgs], {
|
|
1048
|
-
detached: true,
|
|
1049
|
-
stdio: "ignore",
|
|
1050
|
-
env: process.env,
|
|
1051
|
-
});
|
|
1052
|
-
child.unref();
|
|
1053
|
-
const host = opt(options, "host", "127.0.0.1");
|
|
1054
|
-
const port = Number(opt(options, "port", "4020"));
|
|
1055
|
-
const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
|
|
1056
|
-
emit({
|
|
1057
|
-
command: "server",
|
|
1058
|
-
mode: outputMode(options),
|
|
1059
|
-
network: requirement.network,
|
|
1060
|
-
scheme: requirement.scheme,
|
|
1061
|
-
result: {
|
|
1062
|
-
pid: child.pid,
|
|
1063
|
-
pay_url: resourceUrl,
|
|
1064
|
-
resource_url: resourceUrl,
|
|
1065
|
-
daemon: true,
|
|
1066
|
-
},
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
84
|
function validateAmountLimits(selected, options) {
|
|
1070
85
|
const maxRaw = opt(options, "max-rawAmount") ?? opt(options, "max-raw-amount");
|
|
1071
86
|
const maxAmount = opt(options, "max-amount");
|
|
@@ -1079,8 +94,8 @@ function validateAmountLimits(selected, options) {
|
|
|
1079
94
|
throw new Error("cannot evaluate --max-amount for an unknown asset; pass --max-raw-amount or --decimals");
|
|
1080
95
|
}
|
|
1081
96
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
|
|
1082
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
1083
|
-
throw new Error("--decimals must be
|
|
97
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
98
|
+
throw new Error("--decimals must be an integer between 0 and 255");
|
|
1084
99
|
if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, decimals))) {
|
|
1085
100
|
throw new Error(`payment amount exceeds --max-amount ${maxAmount}`);
|
|
1086
101
|
}
|
|
@@ -1107,8 +122,8 @@ function gasfreeFeeLimitRaw(selected, options) {
|
|
|
1107
122
|
throw new Error("cannot evaluate --max-gasfree-fee for an unknown asset; pass --max-gasfree-fee-raw or --decimals");
|
|
1108
123
|
}
|
|
1109
124
|
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
|
|
1110
|
-
if (!Number.isInteger(decimals) || decimals < 0)
|
|
1111
|
-
throw new Error("--decimals must be
|
|
125
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255)
|
|
126
|
+
throw new Error("--decimals must be an integer between 0 and 255");
|
|
1112
127
|
return toSmallestUnit(maxHuman, decimals);
|
|
1113
128
|
}
|
|
1114
129
|
async function serve(options) {
|
|
@@ -1173,7 +188,7 @@ async function serve(options) {
|
|
|
1173
188
|
paymentPayload: payload,
|
|
1174
189
|
paymentRequirements: requirement,
|
|
1175
190
|
}, options);
|
|
1176
|
-
if (!(settle?.success === true
|
|
191
|
+
if (!(settle?.success === true && typeof settle?.transaction === "string" && settle.transaction.length > 0 && typeof settle?.network === "string" && settle.network.length > 0)) {
|
|
1177
192
|
response.writeHead(502, { "content-type": "application/json" });
|
|
1178
193
|
response.end(JSON.stringify({ error: "settlement failed" }));
|
|
1179
194
|
return;
|
|
@@ -1182,7 +197,7 @@ async function serve(options) {
|
|
|
1182
197
|
"content-type": "application/json",
|
|
1183
198
|
[headers.response]: encodeResponse(settle),
|
|
1184
199
|
});
|
|
1185
|
-
response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction
|
|
200
|
+
response.end(JSON.stringify({ success: true, network: requirement.network, scheme: requirement.scheme, transaction: settle.transaction }));
|
|
1186
201
|
}
|
|
1187
202
|
catch (error) {
|
|
1188
203
|
response.writeHead(500, { "content-type": "application/json" });
|
|
@@ -1209,6 +224,17 @@ function selectRequirement(accepts, options) {
|
|
|
1209
224
|
const scheme = opt(options, "scheme");
|
|
1210
225
|
const token = opt(options, "token");
|
|
1211
226
|
const selected = accepts.find(req => {
|
|
227
|
+
if (!req || !["exact", "exact_gasfree"].includes(req.scheme) || typeof req.network !== "string" || typeof req.asset !== "string")
|
|
228
|
+
return false;
|
|
229
|
+
if (req.scheme === "exact_gasfree" && !req.network.startsWith("tron:"))
|
|
230
|
+
return false;
|
|
231
|
+
try {
|
|
232
|
+
if (!findTokenByAddress(req.network, req.asset))
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
1212
238
|
if (network && normalizeNetwork(network) !== req.network)
|
|
1213
239
|
return false;
|
|
1214
240
|
if (scheme && scheme !== req.scheme)
|
|
@@ -1233,6 +259,9 @@ function selectRequirement(accepts, options) {
|
|
|
1233
259
|
async function pay(url, options) {
|
|
1234
260
|
requireArgument(url, "URL", "x402-cli pay <url> [options]");
|
|
1235
261
|
const method = opt(options, "method", "GET");
|
|
262
|
+
if (!/^[A-Z]+$/.test(method) || !["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"].includes(method)) {
|
|
263
|
+
throw new CliError("INVALID_ARGUMENT", `unsupported HTTP method ${method}`, "Use an uppercase standard HTTP method.", 2);
|
|
264
|
+
}
|
|
1236
265
|
const baseHeaders = requestHeaders(options);
|
|
1237
266
|
const probe = await fetchWithTimeout(url, {
|
|
1238
267
|
method,
|
|
@@ -1298,7 +327,7 @@ async function pay(url, options) {
|
|
|
1298
327
|
const body = await responsePayload(paid);
|
|
1299
328
|
const paymentResponse = paid.headers.get(headers.response);
|
|
1300
329
|
const settlement = paymentResponse ? decodeResponse(paymentResponse) : undefined;
|
|
1301
|
-
const settled = settlement
|
|
330
|
+
const settled = settlement?.success === true && typeof settlement?.transaction === "string" && settlement.transaction.length > 0 && typeof settlement?.network === "string" && settlement.network.length > 0;
|
|
1302
331
|
const result = {
|
|
1303
332
|
url,
|
|
1304
333
|
status: paid.status,
|
|
@@ -1312,6 +341,9 @@ async function pay(url, options) {
|
|
|
1312
341
|
} : {}),
|
|
1313
342
|
...(creation.gasfreeEstimate ? { gasfreeEstimate: creation.gasfreeEstimate } : {}),
|
|
1314
343
|
};
|
|
344
|
+
if (paymentResponse && !settled) {
|
|
345
|
+
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);
|
|
346
|
+
}
|
|
1315
347
|
if (!paid.ok) {
|
|
1316
348
|
const retryAfter = paid.headers.get("retry-after");
|
|
1317
349
|
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
|
|
@@ -1332,339 +364,9 @@ async function roundtrip(options) {
|
|
|
1332
364
|
await pay(`http://127.0.0.1:${port}/pay`, options);
|
|
1333
365
|
process.exit(0);
|
|
1334
366
|
}
|
|
1335
|
-
function executableInPath(name) {
|
|
1336
|
-
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
1337
|
-
if (!dir)
|
|
1338
|
-
continue;
|
|
1339
|
-
const candidate = path.join(dir, name);
|
|
1340
|
-
try {
|
|
1341
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
1342
|
-
return candidate;
|
|
1343
|
-
}
|
|
1344
|
-
catch {
|
|
1345
|
-
// Try the next PATH entry.
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
return undefined;
|
|
1349
|
-
}
|
|
1350
|
-
function resolveGatewayPackageRuntime() {
|
|
1351
|
-
try {
|
|
1352
|
-
return require.resolve("@bankofai/x402-gateway/dist/cli.js");
|
|
1353
|
-
}
|
|
1354
|
-
catch {
|
|
1355
|
-
return undefined;
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
function gatewayCommand(options) {
|
|
1359
|
-
const explicit = opt(options, "gateway-bin");
|
|
1360
|
-
const gatewayPackageRuntime = resolveGatewayPackageRuntime();
|
|
1361
|
-
const candidates = [
|
|
1362
|
-
explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
|
|
1363
|
-
gatewayPackageRuntime ? { file: gatewayPackageRuntime, source: "@bankofai/x402-gateway dependency" } : undefined,
|
|
1364
|
-
{ file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
|
|
1365
|
-
executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
|
|
1366
|
-
{ file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
|
|
1367
|
-
].filter(Boolean);
|
|
1368
|
-
for (const candidate of candidates) {
|
|
1369
|
-
try {
|
|
1370
|
-
fs.accessSync(candidate.file, fs.constants.R_OK);
|
|
1371
|
-
if (candidate.file.endsWith(".js")) {
|
|
1372
|
-
return { command: process.execPath, argsPrefix: [candidate.file], source: candidate.source };
|
|
1373
|
-
}
|
|
1374
|
-
return { command: candidate.file, argsPrefix: [], source: candidate.source };
|
|
1375
|
-
}
|
|
1376
|
-
catch {
|
|
1377
|
-
// Try the next candidate.
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
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>.");
|
|
1381
|
-
}
|
|
1382
|
-
async function gatewayStart(args, options) {
|
|
1383
|
-
const gateway = gatewayCommand(options);
|
|
1384
|
-
const child = spawn(gateway.command, [...gateway.argsPrefix, ...args], {
|
|
1385
|
-
stdio: "inherit",
|
|
1386
|
-
env: process.env,
|
|
1387
|
-
});
|
|
1388
|
-
await new Promise((resolve, reject) => {
|
|
1389
|
-
child.on("exit", code => code === 0 ? resolve() : reject(new Error(`gateway exited with ${code}`)));
|
|
1390
|
-
child.on("error", reject);
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
function gatewayCheck(target, options) {
|
|
1394
|
-
const files = providerFiles(target);
|
|
1395
|
-
const providers = files.map(loadProviderFile);
|
|
1396
|
-
const names = new Set();
|
|
1397
|
-
for (const provider of providers) {
|
|
1398
|
-
if (names.has(provider.name))
|
|
1399
|
-
throw new Error(`duplicate provider name: ${provider.name}`);
|
|
1400
|
-
names.add(provider.name);
|
|
1401
|
-
}
|
|
1402
|
-
emit({
|
|
1403
|
-
command: "gateway check",
|
|
1404
|
-
mode: outputMode(options),
|
|
1405
|
-
result: { providers: providers.map(p => p.name), count: providers.length },
|
|
1406
|
-
});
|
|
1407
|
-
}
|
|
1408
|
-
function gatewayScaffold(name, options) {
|
|
1409
|
-
const outputDir = opt(options, "output-dir", path.join("providers", name));
|
|
1410
|
-
const forwardUrl = opt(options, "forward-url", "https://api.example.com");
|
|
1411
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1412
|
-
const body = `name: ${name}
|
|
1413
|
-
title: "${name}"
|
|
1414
|
-
description: "x402 provider"
|
|
1415
|
-
category: data
|
|
1416
|
-
version: v1
|
|
1417
|
-
|
|
1418
|
-
forward_url: ${forwardUrl}
|
|
1419
|
-
|
|
1420
|
-
routing:
|
|
1421
|
-
type: proxy
|
|
1422
|
-
|
|
1423
|
-
operator:
|
|
1424
|
-
network: tron:0xcd8690dc
|
|
1425
|
-
currencies:
|
|
1426
|
-
usd: ["USDT"]
|
|
1427
|
-
recipient: <provider-recipient-address>
|
|
1428
|
-
scheme: exact
|
|
1429
|
-
protocol: exact
|
|
1430
|
-
asset_transfer_method: permit2
|
|
1431
|
-
facilitator_url: https://facilitator.bankofai.io
|
|
1432
|
-
facilitator_api_key: <facilitator-api-key>
|
|
1433
|
-
valid_for_seconds: 300
|
|
1434
|
-
|
|
1435
|
-
endpoints:
|
|
1436
|
-
- method: GET
|
|
1437
|
-
path: /v1/ping
|
|
1438
|
-
metering:
|
|
1439
|
-
dimensions:
|
|
1440
|
-
- tiers:
|
|
1441
|
-
- price_usd: 0.0001
|
|
1442
|
-
`;
|
|
1443
|
-
fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
|
|
1444
|
-
emit({
|
|
1445
|
-
command: "gateway scaffold",
|
|
1446
|
-
mode: outputMode(options),
|
|
1447
|
-
result: { file: path.join(outputDir, "provider.yml") },
|
|
1448
|
-
});
|
|
1449
|
-
}
|
|
1450
|
-
function catalogBuild(target, options) {
|
|
1451
|
-
const providers = providerFiles(target).map(loadProviderFile).map(providerCatalog);
|
|
1452
|
-
const catalog = { version: 1, generatedAt: new Date().toISOString(), providers };
|
|
1453
|
-
const output = opt(options, "output", opt(options, "dist-dir") ? path.join(opt(options, "dist-dir"), "catalog.json") : undefined);
|
|
1454
|
-
if (output) {
|
|
1455
|
-
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
1456
|
-
fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
|
|
1457
|
-
emit({
|
|
1458
|
-
command: "catalog build",
|
|
1459
|
-
mode: outputMode(options),
|
|
1460
|
-
result: { output, count: providers.length },
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
else if (outputMode(options) === "json") {
|
|
1464
|
-
emit({ command: "catalog build", mode: "json", result: catalog });
|
|
1465
|
-
}
|
|
1466
|
-
else {
|
|
1467
|
-
printJson(catalog);
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
function catalogPayAssets(target, options) {
|
|
1471
|
-
const rows = providerFiles(target).map(loadProviderFile).flatMap(provider => (provider.endpoints ?? []).map((endpoint) => ({
|
|
1472
|
-
provider: provider.name,
|
|
1473
|
-
method: endpoint.method,
|
|
1474
|
-
path: `/providers/${provider.name}${endpoint.path}`,
|
|
1475
|
-
network: normalizeNetwork(provider.operator.network),
|
|
1476
|
-
currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
|
|
1477
|
-
price_usd: providerPrice(endpoint),
|
|
1478
|
-
scheme: providerScheme(provider),
|
|
1479
|
-
assetTransferMethod: providerAssetTransferMethod(provider),
|
|
1480
|
-
})));
|
|
1481
|
-
emit({
|
|
1482
|
-
command: "gateway catalog pay-assets",
|
|
1483
|
-
mode: outputMode(options),
|
|
1484
|
-
result: { count: rows.length, assets: rows },
|
|
1485
|
-
});
|
|
1486
|
-
}
|
|
1487
|
-
async function readProviderDetailForSearch(source, fqn, options) {
|
|
1488
|
-
try {
|
|
1489
|
-
return await readJson(catalogDetailSource(source, "providers", fqn), options);
|
|
1490
|
-
}
|
|
1491
|
-
catch {
|
|
1492
|
-
return {};
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
async function searchCatalog(source, query, options) {
|
|
1496
|
-
const providers = await readCatalog(source, options);
|
|
1497
|
-
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1498
|
-
if (!terms.length)
|
|
1499
|
-
return [];
|
|
1500
|
-
const includeBlocked = hasFlag(options, "include-blocked");
|
|
1501
|
-
const hits = [];
|
|
1502
|
-
for (const provider of providers) {
|
|
1503
|
-
if (provider.block && !includeBlocked)
|
|
1504
|
-
continue;
|
|
1505
|
-
const fqn = String(provider.fqn ?? provider.name ?? "");
|
|
1506
|
-
if (!fqn)
|
|
1507
|
-
continue;
|
|
1508
|
-
const detail = await readProviderDetailForSearch(source, fqn, options);
|
|
1509
|
-
const tags = stringList(detail.featured_tags ?? provider.featured_tags ?? detail.tags ?? provider.tags);
|
|
1510
|
-
const endpoints = Array.isArray(detail.endpoints) ? detail.endpoints : Array.isArray(provider.endpoints) ? provider.endpoints : [];
|
|
1511
|
-
const categoryMeta = detail.category_meta ?? provider.category_meta;
|
|
1512
|
-
const chains = stringList(detail.chains ?? provider.chains);
|
|
1513
|
-
const chainKinds = stringList(detail.chain_kinds ?? provider.chain_kinds);
|
|
1514
|
-
const chainsMetaRaw = detail.chains_meta ?? provider.chains_meta ?? [];
|
|
1515
|
-
const chainsMeta = Array.isArray(chainsMetaRaw) ? chainsMetaRaw.filter(item => item && typeof item === "object") : [];
|
|
1516
|
-
const titleZh = String(detail.title_zh ?? provider.title_zh ?? "");
|
|
1517
|
-
const mainTitle = String(detail.main_title ?? provider.main_title ?? detail.mainTitle ?? provider.mainTitle ?? "");
|
|
1518
|
-
const subTitle = String(detail.sub_title ?? provider.sub_title ?? detail.subTitle ?? provider.subTitle ?? "");
|
|
1519
|
-
const fields = {
|
|
1520
|
-
fqn: [fqn],
|
|
1521
|
-
title: [String(detail.title ?? provider.title ?? ""), mainTitle],
|
|
1522
|
-
i18n: [titleZh, subTitle, ...dictValues(detail.i18n ?? provider.i18n)],
|
|
1523
|
-
category: [String(detail.category ?? provider.category ?? "")],
|
|
1524
|
-
category_meta: dictValues(categoryMeta),
|
|
1525
|
-
chains: [...chains, ...chainMetaValues(chainsMeta)],
|
|
1526
|
-
chain_kinds: chainKinds,
|
|
1527
|
-
service_url: [String(detail.service_url ?? provider.service_url ?? detail.serviceUrl ?? provider.serviceUrl ?? "")],
|
|
1528
|
-
description: [String(detail.description ?? provider.description ?? "")],
|
|
1529
|
-
use_case: [String(detail.use_case ?? provider.use_case ?? detail.useCase ?? provider.useCase ?? "")],
|
|
1530
|
-
tags,
|
|
1531
|
-
endpoints: endpointFields(endpoints),
|
|
1532
|
-
};
|
|
1533
|
-
const scored = scoreFields(terms, fields);
|
|
1534
|
-
if (scored.score === 0)
|
|
1535
|
-
continue;
|
|
1536
|
-
hits.push({
|
|
1537
|
-
provider,
|
|
1538
|
-
detail,
|
|
1539
|
-
fqn,
|
|
1540
|
-
title: fields.title[0],
|
|
1541
|
-
category: fields.category[0],
|
|
1542
|
-
serviceUrl: fields.service_url[0],
|
|
1543
|
-
description: fields.description[0] || undefined,
|
|
1544
|
-
useCase: fields.use_case[0] || undefined,
|
|
1545
|
-
titleZh: titleZh || undefined,
|
|
1546
|
-
mainTitle: mainTitle || undefined,
|
|
1547
|
-
subTitle: subTitle || undefined,
|
|
1548
|
-
categoryMeta: categoryMeta && typeof categoryMeta === "object" ? categoryMeta : undefined,
|
|
1549
|
-
chains,
|
|
1550
|
-
chainKinds,
|
|
1551
|
-
chainsMeta,
|
|
1552
|
-
tags,
|
|
1553
|
-
endpoints,
|
|
1554
|
-
score: scored.score,
|
|
1555
|
-
matchedFields: scored.matchedFields,
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
return hits
|
|
1559
|
-
.sort((a, b) => b.score - a.score || a.fqn.localeCompare(b.fqn))
|
|
1560
|
-
.slice(0, positiveIntegerOption(options, "limit", 10));
|
|
1561
|
-
}
|
|
1562
|
-
function searchHitToJson(hit) {
|
|
1563
|
-
return {
|
|
1564
|
-
fqn: hit.fqn,
|
|
1565
|
-
title: hit.title,
|
|
1566
|
-
category: hit.category,
|
|
1567
|
-
serviceUrl: hit.serviceUrl,
|
|
1568
|
-
description: hit.description,
|
|
1569
|
-
useCase: hit.useCase,
|
|
1570
|
-
title_zh: hit.titleZh,
|
|
1571
|
-
main_title: hit.mainTitle,
|
|
1572
|
-
sub_title: hit.subTitle,
|
|
1573
|
-
category_meta: hit.categoryMeta,
|
|
1574
|
-
chains: hit.chains,
|
|
1575
|
-
chain_kinds: hit.chainKinds,
|
|
1576
|
-
chains_meta: hit.chainsMeta,
|
|
1577
|
-
tags: hit.tags,
|
|
1578
|
-
score: hit.score,
|
|
1579
|
-
matchedFields: hit.matchedFields,
|
|
1580
|
-
endpoints: hit.endpoints,
|
|
1581
|
-
};
|
|
1582
|
-
}
|
|
1583
|
-
async function catalogSearch(source, query, options) {
|
|
1584
|
-
positiveIntegerOption(options, "limit", 10);
|
|
1585
|
-
const hits = await searchCatalog(source, query, options);
|
|
1586
|
-
const results = hits.map(searchHitToJson);
|
|
1587
|
-
if (outputMode(options) === "json") {
|
|
1588
|
-
emit({ command: "catalog search", mode: "json", result: { query, catalog: source, count: hits.length, results } });
|
|
1589
|
-
return;
|
|
1590
|
-
}
|
|
1591
|
-
if (!hits.length) {
|
|
1592
|
-
process.stdout.write("no matches\n");
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
for (const hit of hits) {
|
|
1596
|
-
const tags = hit.tags.length ? hit.tags.join(",") : "-";
|
|
1597
|
-
process.stdout.write(`${hit.fqn.padEnd(32)} score=${String(hit.score).padEnd(3)} category=${hit.category.padEnd(12)} tags=${tags}\n`);
|
|
1598
|
-
if (hit.title)
|
|
1599
|
-
process.stdout.write(` ${hit.title}\n`);
|
|
1600
|
-
if (hit.description)
|
|
1601
|
-
process.stdout.write(` ${hit.description.split("\n")[0]}\n`);
|
|
1602
|
-
if (hit.serviceUrl)
|
|
1603
|
-
process.stdout.write(` service: ${hit.serviceUrl}\n`);
|
|
1604
|
-
for (const endpoint of hit.endpoints.slice(0, 3)) {
|
|
1605
|
-
const method = String(endpoint.method ?? "");
|
|
1606
|
-
const pathText = String(endpoint.path ?? endpoint.url ?? "");
|
|
1607
|
-
const paid = endpoint.paid;
|
|
1608
|
-
let suffix = "";
|
|
1609
|
-
if (paid && typeof paid === "object") {
|
|
1610
|
-
suffix = ` ${paid.network ?? ""} ${paid.currency ?? ""} ${paid.amount_raw ?? ""}`.trimEnd();
|
|
1611
|
-
}
|
|
1612
|
-
process.stdout.write(` ${method.padEnd(6)} ${pathText}${suffix ? ` ${suffix}` : ""}\n`);
|
|
1613
|
-
}
|
|
1614
|
-
process.stdout.write("\n");
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
async function catalogShow(source, name, options) {
|
|
1618
|
-
requireArgument(name, "provider", "x402-cli catalog show <provider> [--catalog <source>]");
|
|
1619
|
-
const provider = await readCatalogProvider(source, name, options);
|
|
1620
|
-
if (outputMode(options) === "json") {
|
|
1621
|
-
emit({ command: "catalog show", mode: "json", result: provider });
|
|
1622
|
-
return;
|
|
1623
|
-
}
|
|
1624
|
-
process.stdout.write(`${provider.fqn ?? provider.name} - ${provider.title ?? provider.main_title ?? provider.name}\n`);
|
|
1625
|
-
if (provider.description)
|
|
1626
|
-
process.stdout.write(`${String(provider.description).split("\n")[0]}\n`);
|
|
1627
|
-
if (provider.category)
|
|
1628
|
-
process.stdout.write(`category: ${provider.category}\n`);
|
|
1629
|
-
if (provider.chains)
|
|
1630
|
-
process.stdout.write(`chains: ${provider.chains.join(", ")}\n`);
|
|
1631
|
-
}
|
|
1632
|
-
async function catalogEndpoints(source, name, options) {
|
|
1633
|
-
requireArgument(name, "provider", "x402-cli catalog endpoints <provider> [--catalog <source>]");
|
|
1634
|
-
const provider = await readCatalogProvider(source, name, options);
|
|
1635
|
-
const endpoints = provider.endpoints ?? [];
|
|
1636
|
-
if (outputMode(options) === "json") {
|
|
1637
|
-
emit({ command: "catalog endpoints", mode: "json", result: { provider: provider.fqn ?? provider.name, endpoints } });
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
for (const endpoint of endpoints) {
|
|
1641
|
-
process.stdout.write(`${String(endpoint.method ?? "").padEnd(6)} ${endpoint.path ?? endpoint.url ?? ""}\n`);
|
|
1642
|
-
if (endpoint.description)
|
|
1643
|
-
process.stdout.write(` ${String(endpoint.description).split("\n")[0]}\n`);
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
async function catalogPayJson(source, name, options) {
|
|
1647
|
-
requireArgument(name, "provider", "x402-cli catalog pay-json <provider> [--catalog <source>]");
|
|
1648
|
-
const provider = await readCatalogPayProvider(source, name, options);
|
|
1649
|
-
const endpoint = (provider.endpoints ?? []).find((item) => item.paid || item.x402_routes?.length || item.x402Routes?.length) ?? provider.endpoints?.[0];
|
|
1650
|
-
if (!endpoint)
|
|
1651
|
-
throw new Error(`provider has no endpoints: ${name}`);
|
|
1652
|
-
const result = {
|
|
1653
|
-
provider: provider.fqn ?? provider.name,
|
|
1654
|
-
url: endpoint.url ?? endpoint.path,
|
|
1655
|
-
method: endpoint.method,
|
|
1656
|
-
paid: endpoint.paid,
|
|
1657
|
-
x402_routes: endpoint.x402_routes ?? endpoint.x402Routes ?? [],
|
|
1658
|
-
endpoint,
|
|
1659
|
-
};
|
|
1660
|
-
if (hasFlag(options, "raw"))
|
|
1661
|
-
printJson(result);
|
|
1662
|
-
else
|
|
1663
|
-
emit({ command: "catalog pay-json", mode: outputMode(options), result });
|
|
1664
|
-
}
|
|
1665
367
|
async function handleGateway(args) {
|
|
1666
368
|
const { command, positional, options } = parseArgs(args);
|
|
1667
|
-
if (hasFlag(options, "help") ||
|
|
369
|
+
if (hasFlag(options, "help") || ["help", "--help", "-h"].includes(command)) {
|
|
1668
370
|
process.stdout.write(helpText(command === "catalog" || positional[0] === "catalog" ? "gateway-catalog" : "gateway"));
|
|
1669
371
|
return;
|
|
1670
372
|
}
|
|
@@ -1695,31 +397,6 @@ async function handleGatewayCatalog(positional, options) {
|
|
|
1695
397
|
else
|
|
1696
398
|
throw new CliError("UNKNOWN_COMMAND", `Unknown gateway catalog command: ${sub}`, "Run x402-cli gateway catalog --help to list commands.", 2);
|
|
1697
399
|
}
|
|
1698
|
-
async function handleCatalog(args) {
|
|
1699
|
-
const { command, positional, options } = parseArgs(args);
|
|
1700
|
-
if (hasFlag(options, "help") || command === "help") {
|
|
1701
|
-
const topic = command === "help" ? positional[0] : command;
|
|
1702
|
-
process.stdout.write(helpText(topic ? `catalog-${topic}` : "catalog"));
|
|
1703
|
-
return;
|
|
1704
|
-
}
|
|
1705
|
-
const source = opt(options, "catalog", defaultCatalogSource());
|
|
1706
|
-
if (command === "update")
|
|
1707
|
-
await catalogUpdate(source, options);
|
|
1708
|
-
else if (command === "search")
|
|
1709
|
-
await catalogSearch(source, requireArgument(positional.join(" "), "query", "x402-cli catalog search <query> [options]"), options);
|
|
1710
|
-
else if (command === "show")
|
|
1711
|
-
await catalogShow(source, positional[0], options);
|
|
1712
|
-
else if (command === "endpoints")
|
|
1713
|
-
await catalogEndpoints(source, positional[0], options);
|
|
1714
|
-
else if (command === "pay-json")
|
|
1715
|
-
await catalogPayJson(source, positional[0], options);
|
|
1716
|
-
else if (command === "export-gateway")
|
|
1717
|
-
await catalogExportGateway(positional[0], options);
|
|
1718
|
-
else if (command === "build")
|
|
1719
|
-
catalogBuild(positional[0] ?? "providers", options);
|
|
1720
|
-
else
|
|
1721
|
-
throw new CliError("UNKNOWN_COMMAND", `Unknown catalog command: ${command}`, "Run x402-cli catalog --help to list commands.", 2);
|
|
1722
|
-
}
|
|
1723
400
|
async function main() {
|
|
1724
401
|
const argv = process.argv.slice(2);
|
|
1725
402
|
const { command, positional, options } = parseArgs(argv);
|
|
@@ -1742,7 +419,7 @@ async function main() {
|
|
|
1742
419
|
}
|
|
1743
420
|
if (command === "serve") {
|
|
1744
421
|
if (hasFlag(options, "daemon"))
|
|
1745
|
-
|
|
422
|
+
await startServeDaemon(argv, options, buildRequirement(options), fileURLToPath(import.meta.url));
|
|
1746
423
|
else
|
|
1747
424
|
await serve(options);
|
|
1748
425
|
}
|