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