@bankofai/x402-cli 1.0.0-beta.8 → 1.0.0-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/cli.js CHANGED
@@ -12,6 +12,8 @@ import { createPaymentPayload, decodeRequired, decodeResponse, decodeSignature,
12
12
  import { assertRawAmount, findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
13
13
  const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "force", "help", "human", "include-blocked", "json", "raw", "version"]);
14
14
  const require = createRequire(import.meta.url);
15
+ const DEFAULT_TIMEOUT_MS = 30_000;
16
+ const CATALOG_UPDATE_RETRIES = 3;
15
17
  class CliError extends Error {
16
18
  code;
17
19
  hint;
@@ -263,7 +265,7 @@ function classify(error) {
263
265
  hint: "Increase the max amount flag only if this provider price is expected.",
264
266
  };
265
267
  }
266
- if (lower.includes("failed to fetch") || lower.includes("fetch failed") || lower.includes("econnrefused")) {
268
+ if (lower.includes("failed to fetch") || lower.includes("fetch failed") || lower.includes("econnrefused") || lower.includes("timed out")) {
267
269
  return {
268
270
  code: "NETWORK_ERROR",
269
271
  message,
@@ -318,6 +320,7 @@ Options:
318
320
  --dry-run Read requirements but do not sign or pay
319
321
  --private-key <hex> Explicit payer private key (or PRIVATE_KEY/TRON_PRIVATE_KEY/EVM_PRIVATE_KEY)
320
322
  --rpc-url <url> Explicit network RPC URL
323
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
321
324
  --json Print JSON envelope
322
325
 
323
326
  Examples:
@@ -339,6 +342,7 @@ Options:
339
342
  --port <port> Bind port (default: 4020)
340
343
  --resource-url <url> URL advertised in payment requirements
341
344
  --facilitator-url <url> Facilitator base URL
345
+ --timeout-ms <ms> Facilitator timeout in milliseconds (default: 30000)
342
346
  --daemon Run in background and print the child pid
343
347
  --json Print JSON envelope
344
348
 
@@ -385,6 +389,7 @@ Options:
385
389
  --provider <fqn> Provider FQN for export-gateway
386
390
  --output-dir <dir> Output directory for generated files
387
391
  -n, --limit <count> Search result limit
392
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
388
393
  --include-blocked Include blocked providers in search
389
394
  --json Print JSON envelope
390
395
  `,
@@ -394,6 +399,7 @@ Options:
394
399
  Options:
395
400
  --catalog <source> catalog.json path or URL
396
401
  -n, --limit <count> Search result limit
402
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
397
403
  --include-blocked Include blocked providers in search
398
404
  --json Print JSON envelope
399
405
  `,
@@ -402,6 +408,7 @@ Options:
402
408
 
403
409
  Options:
404
410
  --catalog <source> catalog.json path or URL
411
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
405
412
  --json Print JSON envelope
406
413
  `,
407
414
  "catalog-pay-json": `Usage:
@@ -409,6 +416,7 @@ Options:
409
416
 
410
417
  Options:
411
418
  --catalog <source> catalog.json path or URL
419
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
412
420
  --raw Print raw pay payload instead of JSON envelope
413
421
  --json Print JSON envelope
414
422
  `,
@@ -417,6 +425,7 @@ Options:
417
425
 
418
426
  Options:
419
427
  --catalog <source> catalog.json path or URL
428
+ --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
420
429
  --json Print JSON envelope
421
430
  `,
422
431
  "catalog-export-gateway": `Usage:
@@ -607,8 +616,8 @@ function scoreFields(terms, fields) {
607
616
  }
608
617
  return { score, matchedFields };
609
618
  }
610
- async function readCatalog(source) {
611
- const text = await readText(source);
619
+ async function readCatalog(source, options) {
620
+ const text = await readText(source, options);
612
621
  const parsed = JSON.parse(text);
613
622
  if (Array.isArray(parsed))
614
623
  return parsed;
@@ -618,24 +627,24 @@ async function readCatalog(source) {
618
627
  return parsed.items;
619
628
  return [];
620
629
  }
621
- async function readCatalogObject(source) {
622
- const parsed = JSON.parse(await readText(source));
630
+ async function readCatalogObject(source, options) {
631
+ const parsed = JSON.parse(await readText(source, options));
623
632
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
624
633
  throw new Error(`expected JSON object from ${source}`);
625
634
  }
626
635
  return parsed;
627
636
  }
628
- async function readText(source) {
637
+ async function readText(source, options) {
629
638
  if (!source.startsWith("http://") && !source.startsWith("https://")) {
630
639
  return fs.readFileSync(source, "utf8");
631
640
  }
632
- const response = await fetch(source);
641
+ const response = await fetchWithTimeout(source, {}, timeoutMs(options), `fetch ${source}`);
633
642
  if (!response.ok)
634
643
  throw new Error(`failed to fetch ${source}: ${response.status}`);
635
644
  return response.text();
636
645
  }
637
- async function readJson(source) {
638
- return JSON.parse(await readText(source));
646
+ async function readJson(source, options) {
647
+ return JSON.parse(await readText(source, options));
639
648
  }
640
649
  function writeJson(file, value) {
641
650
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -713,11 +722,29 @@ function positiveIntegerOption(options, key, fallback) {
713
722
  throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
714
723
  return parsed;
715
724
  }
725
+ function timeoutMs(options) {
726
+ return options ? positiveIntegerOption(options, "timeout-ms", DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
727
+ }
728
+ async function fetchWithTimeout(input, init = {}, timeout = DEFAULT_TIMEOUT_MS, label = "request") {
729
+ const controller = new AbortController();
730
+ const timer = setTimeout(() => controller.abort(), timeout);
731
+ try {
732
+ return await fetch(input, { ...init, signal: controller.signal });
733
+ }
734
+ catch (error) {
735
+ if (error?.name === "AbortError")
736
+ throw new Error(`${label} timed out after ${timeout}ms`);
737
+ throw error;
738
+ }
739
+ finally {
740
+ clearTimeout(timer);
741
+ }
742
+ }
716
743
  function remoteBaseFromCatalogPayload(payload) {
717
744
  const base = payload.base_url ?? payload.baseUrl;
718
745
  return typeof base === "string" && /^https?:\/\//.test(base) ? `${base.replace(/\/+$/, "")}/` : undefined;
719
746
  }
720
- async function remoteBaseFromSource(source, payload) {
747
+ async function remoteBaseFromSource(source, payload, options) {
721
748
  const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
722
749
  if (fromPayload)
723
750
  return fromPayload;
@@ -725,7 +752,7 @@ async function remoteBaseFromSource(source, payload) {
725
752
  return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
726
753
  }
727
754
  try {
728
- return remoteBaseFromCatalogPayload(await readCatalogObject(source));
755
+ return remoteBaseFromCatalogPayload(await readCatalogObject(source, options));
729
756
  }
730
757
  catch {
731
758
  return undefined;
@@ -751,36 +778,37 @@ function catalogDetailSource(source, section, name) {
751
778
  return direct;
752
779
  return path.join(root, section, providerFilename(name));
753
780
  }
754
- async function readCatalogProvider(source, name) {
755
- const providers = await readCatalog(source);
781
+ async function readCatalogProvider(source, name, options) {
782
+ const providers = await readCatalog(source, options);
756
783
  const summary = providers.find((item) => item.name === name || item.fqn === name);
757
784
  if (!summary)
758
785
  throw new Error(`provider not found: ${name}`);
759
786
  const fqn = summary.fqn ?? summary.name ?? name;
760
787
  try {
761
- return await readJson(catalogDetailSource(source, "providers", fqn));
788
+ return await readJson(catalogDetailSource(source, "providers", fqn), options);
762
789
  }
763
790
  catch {
764
791
  return summary;
765
792
  }
766
793
  }
767
- async function readCatalogPayProvider(source, name) {
768
- const providers = await readCatalog(source);
794
+ async function readCatalogPayProvider(source, name, options) {
795
+ const providers = await readCatalog(source, options);
769
796
  const summary = providers.find((item) => item.name === name || item.fqn === name);
770
797
  const fqn = summary?.fqn ?? summary?.name ?? name;
771
798
  try {
772
- return await readJson(catalogDetailSource(source, "pay", fqn));
799
+ return await readJson(catalogDetailSource(source, "pay", fqn), options);
773
800
  }
774
801
  catch {
775
802
  if (summary)
776
- return readCatalogProvider(source, name);
803
+ return readCatalogProvider(source, name, options);
777
804
  throw new Error(`provider not found: ${name}`);
778
805
  }
779
806
  }
780
- async function cacheProviderAssets(source, catalogPayload) {
781
- const base = await remoteBaseFromSource(source, catalogPayload);
807
+ async function cacheProviderAssets(source, catalogPayload, options) {
808
+ const base = await remoteBaseFromSource(source, catalogPayload, options);
809
+ const warnings = [];
782
810
  if (!base)
783
- return { detailCount: 0, payCount: 0 };
811
+ return { detailCount: 0, payCount: 0, warnings };
784
812
  let detailCount = 0;
785
813
  let payCount = 0;
786
814
  for (const provider of catalogPayload.providers ?? []) {
@@ -789,34 +817,51 @@ async function cacheProviderAssets(source, catalogPayload) {
789
817
  continue;
790
818
  const filename = providerFilename(fqn);
791
819
  try {
792
- const detail = await readJson(new URL(`providers/${filename}`, base).toString());
820
+ const detail = await readJson(new URL(`providers/${filename}`, base).toString(), options);
793
821
  writeJson(path.join(cacheDir(), "providers", filename), detail);
794
822
  detailCount += 1;
795
823
  }
796
- catch {
797
- // A partial cache is still useful.
824
+ catch (error) {
825
+ warnings.push(`failed to cache provider detail ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
798
826
  }
799
827
  try {
800
- const pay = await readJson(new URL(`pay/${filename}`, base).toString());
828
+ const pay = await readJson(new URL(`pay/${filename}`, base).toString(), options);
801
829
  writeJson(path.join(cacheDir(), "pay", filename), pay);
802
830
  payCount += 1;
803
831
  }
804
- catch {
805
- // A partial cache is still useful.
832
+ catch (error) {
833
+ warnings.push(`failed to cache pay JSON ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
806
834
  }
807
835
  }
808
- return { detailCount, payCount };
836
+ return { detailCount, payCount, warnings };
809
837
  }
810
838
  async function catalogUpdate(source, options) {
811
- const payload = await readCatalogObject(source);
839
+ let payload;
840
+ const warnings = [];
841
+ for (let attempt = 1; attempt <= CATALOG_UPDATE_RETRIES; attempt += 1) {
842
+ try {
843
+ payload = await readCatalogObject(source, options);
844
+ break;
845
+ }
846
+ catch (error) {
847
+ const message = error instanceof Error ? error.message : String(error);
848
+ if (attempt === CATALOG_UPDATE_RETRIES)
849
+ throw error;
850
+ warnings.push(`catalog update attempt ${attempt} failed: ${message}`);
851
+ await delay(250 * attempt);
852
+ }
853
+ }
854
+ if (!payload)
855
+ throw new Error(`failed to read catalog from ${source}`);
812
856
  writeJson(cachedCatalogPath(), payload);
813
- const cached = await cacheProviderAssets(source, payload);
857
+ const cached = await cacheProviderAssets(source, payload, options);
814
858
  const result = {
815
859
  source,
816
860
  path: cachedCatalogPath(),
817
861
  providerCount: payload.provider_count ?? payload.providerCount ?? (payload.providers ?? []).length,
818
862
  detailCount: cached.detailCount,
819
863
  payCount: cached.payCount,
864
+ warnings: [...warnings, ...cached.warnings],
820
865
  };
821
866
  emit({ command: "catalog update", mode: outputMode(options), result });
822
867
  }
@@ -903,7 +948,7 @@ async function catalogExportGateway(gatewayUrl, options) {
903
948
  const providerFqn = requireArgument(opt(options, "provider"), "--provider", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
904
949
  sanitizeProviderName(providerFqn);
905
950
  const base = gatewayUrl.replace(/\/+$/, "");
906
- const detail = await readJson(`${base}/__402/catalog/providers/${providerFilename(providerFqn)}`);
951
+ const detail = await readJson(`${base}/__402/catalog/providers/${providerFilename(providerFqn)}`, options);
907
952
  const outputRoot = opt(options, "output-dir", "providers");
908
953
  const target = opt(options, "output-dir")
909
954
  ? path.resolve(outputRoot)
@@ -950,12 +995,12 @@ function buildRequirement(options) {
950
995
  extra: assetTransferMethod ? { assetTransferMethod } : {},
951
996
  };
952
997
  }
953
- async function facilitatorPost(baseUrl, path, body) {
954
- const response = await fetch(new URL(path, baseUrl), {
998
+ async function facilitatorPost(baseUrl, path, body, options) {
999
+ const response = await fetchWithTimeout(new URL(path, baseUrl), {
955
1000
  method: "POST",
956
1001
  headers: { "content-type": "application/json" },
957
1002
  body: JSON.stringify(body),
958
- });
1003
+ }, timeoutMs(options), `facilitator ${path}`);
959
1004
  const text = await response.text();
960
1005
  const data = text ? JSON.parse(text) : {};
961
1006
  if (!response.ok)
@@ -1075,7 +1120,7 @@ async function serve(options) {
1075
1120
  const verify = await facilitatorPost(facilitatorUrl, "/verify", {
1076
1121
  paymentPayload: payload,
1077
1122
  paymentRequirements: requirement,
1078
- });
1123
+ }, options);
1079
1124
  if (!(verify?.valid === true || verify?.isValid === true)) {
1080
1125
  response.writeHead(400, { "content-type": "application/json" });
1081
1126
  response.end(JSON.stringify({ error: "payment verification failed" }));
@@ -1084,7 +1129,7 @@ async function serve(options) {
1084
1129
  const settle = await facilitatorPost(facilitatorUrl, "/settle", {
1085
1130
  paymentPayload: payload,
1086
1131
  paymentRequirements: requirement,
1087
- });
1132
+ }, options);
1088
1133
  if (!(settle?.success === true || settle?.settled === true || typeof settle?.transaction === "string" || typeof settle?.txHash === "string")) {
1089
1134
  response.writeHead(502, { "content-type": "application/json" });
1090
1135
  response.end(JSON.stringify({ error: "settlement failed" }));
@@ -1140,11 +1185,11 @@ async function pay(url, options) {
1140
1185
  requireArgument(url, "URL", "x402-cli pay <url> [options]");
1141
1186
  const method = opt(options, "method", "GET");
1142
1187
  const baseHeaders = requestHeaders(options);
1143
- const probe = await fetch(url, {
1188
+ const probe = await fetchWithTimeout(url, {
1144
1189
  method,
1145
1190
  headers: baseHeaders,
1146
1191
  body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : opt(options, "body"),
1147
- });
1192
+ }, timeoutMs(options), `fetch ${url}`);
1148
1193
  if (probe.status !== 402) {
1149
1194
  emit({
1150
1195
  command: "client",
@@ -1188,11 +1233,11 @@ async function pay(url, options) {
1188
1233
  }));
1189
1234
  const retryHeaders = new Headers(baseHeaders);
1190
1235
  retryHeaders.set(headers.signature, encodeSignature(payload));
1191
- const paid = await fetch(url, {
1236
+ const paid = await fetchWithTimeout(url, {
1192
1237
  method,
1193
1238
  headers: retryHeaders,
1194
1239
  body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : opt(options, "body"),
1195
- });
1240
+ }, timeoutMs(options), `fetch ${url}`);
1196
1241
  const body = await responsePayload(paid);
1197
1242
  const paymentResponse = paid.headers.get(headers.response);
1198
1243
  const result = {
@@ -1372,16 +1417,16 @@ function catalogPayAssets(target, options) {
1372
1417
  result: { count: rows.length, assets: rows },
1373
1418
  });
1374
1419
  }
1375
- async function readProviderDetailForSearch(source, fqn) {
1420
+ async function readProviderDetailForSearch(source, fqn, options) {
1376
1421
  try {
1377
- return await readJson(catalogDetailSource(source, "providers", fqn));
1422
+ return await readJson(catalogDetailSource(source, "providers", fqn), options);
1378
1423
  }
1379
1424
  catch {
1380
1425
  return {};
1381
1426
  }
1382
1427
  }
1383
1428
  async function searchCatalog(source, query, options) {
1384
- const providers = await readCatalog(source);
1429
+ const providers = await readCatalog(source, options);
1385
1430
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1386
1431
  if (!terms.length)
1387
1432
  return [];
@@ -1393,7 +1438,7 @@ async function searchCatalog(source, query, options) {
1393
1438
  const fqn = String(provider.fqn ?? provider.name ?? "");
1394
1439
  if (!fqn)
1395
1440
  continue;
1396
- const detail = await readProviderDetailForSearch(source, fqn);
1441
+ const detail = await readProviderDetailForSearch(source, fqn, options);
1397
1442
  const tags = stringList(detail.featured_tags ?? provider.featured_tags ?? detail.tags ?? provider.tags);
1398
1443
  const endpoints = Array.isArray(detail.endpoints) ? detail.endpoints : Array.isArray(provider.endpoints) ? provider.endpoints : [];
1399
1444
  const categoryMeta = detail.category_meta ?? provider.category_meta;
@@ -1504,7 +1549,7 @@ async function catalogSearch(source, query, options) {
1504
1549
  }
1505
1550
  async function catalogShow(source, name, options) {
1506
1551
  requireArgument(name, "provider", "x402-cli catalog show <provider> [--catalog <source>]");
1507
- const provider = await readCatalogProvider(source, name);
1552
+ const provider = await readCatalogProvider(source, name, options);
1508
1553
  if (outputMode(options) === "json") {
1509
1554
  emit({ command: "catalog show", mode: "json", result: provider });
1510
1555
  return;
@@ -1519,7 +1564,7 @@ async function catalogShow(source, name, options) {
1519
1564
  }
1520
1565
  async function catalogEndpoints(source, name, options) {
1521
1566
  requireArgument(name, "provider", "x402-cli catalog endpoints <provider> [--catalog <source>]");
1522
- const provider = await readCatalogProvider(source, name);
1567
+ const provider = await readCatalogProvider(source, name, options);
1523
1568
  const endpoints = provider.endpoints ?? [];
1524
1569
  if (outputMode(options) === "json") {
1525
1570
  emit({ command: "catalog endpoints", mode: "json", result: { provider: provider.fqn ?? provider.name, endpoints } });
@@ -1533,7 +1578,7 @@ async function catalogEndpoints(source, name, options) {
1533
1578
  }
1534
1579
  async function catalogPayJson(source, name, options) {
1535
1580
  requireArgument(name, "provider", "x402-cli catalog pay-json <provider> [--catalog <source>]");
1536
- const provider = await readCatalogPayProvider(source, name);
1581
+ const provider = await readCatalogPayProvider(source, name, options);
1537
1582
  const endpoint = (provider.endpoints ?? []).find((item) => item.paid || item.x402_routes?.length || item.x402Routes?.length) ?? provider.endpoints?.[0];
1538
1583
  if (!endpoint)
1539
1584
  throw new Error(`provider has no endpoints: ${name}`);
package/dist/x402.js CHANGED
@@ -112,7 +112,7 @@ async function createTronWallet(privateKey) {
112
112
  return {
113
113
  getAddress: () => address,
114
114
  async signTypedData(args) {
115
- const signature = await tronWeb.trx._signTypedData(args.domain, args.types, args.message, rawKey);
115
+ const signature = await signTronTypedData(tronWeb, args, rawKey);
116
116
  return signature.startsWith("0x") ? signature : `0x${signature}`;
117
117
  },
118
118
  async signTransaction(tx) {
@@ -120,6 +120,13 @@ async function createTronWallet(privateKey) {
120
120
  },
121
121
  };
122
122
  }
123
+ export async function signTronTypedData(tronWeb, args, rawPrivateKey) {
124
+ const trx = tronWeb.trx;
125
+ const signer = typeof trx.signTypedData === "function" ? trx.signTypedData.bind(trx) : trx._signTypedData?.bind(trx);
126
+ if (!signer)
127
+ throw new Error("tronweb typed-data signing is not available");
128
+ return signer(args.domain, args.types, args.message, rawPrivateKey);
129
+ }
123
130
  export async function createPaymentPayload(args) {
124
131
  const selected = ensurePermit2(args.selected);
125
132
  const required = paymentRequired(selected, args.resource, args.extensions);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bankofai/x402-cli",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0-beta.9",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"