@fedify/cli 2.4.0-dev.2155 → 2.4.0-dev.2157

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/deno.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "temporal-polyfill";
2
2
  //#region deno.json
3
- var version = "2.4.0-dev.2155+2ccc5085";
3
+ var version = "2.4.0-dev.2157+b76e5888";
4
4
  //#endregion
5
5
  export { version };
@@ -0,0 +1,70 @@
1
+ import "temporal-polyfill";
2
+ import { FetchError, UrlError } from "@fedify/vocab-runtime";
3
+ import "@fedify/vocab";
4
+ //#region src/diagnostics.ts
5
+ /** Records loader failures that lookupObject would otherwise swallow or wrap. */
6
+ function createLookupDiagnostics(documentLoader, contextLoader) {
7
+ let objectFailure;
8
+ let contextFailure;
9
+ const wrap = (loader, source, record) => async (url, options) => {
10
+ try {
11
+ const document = await loader(url, options);
12
+ record(void 0);
13
+ return document;
14
+ } catch (error) {
15
+ record({
16
+ error,
17
+ source
18
+ });
19
+ throw error;
20
+ }
21
+ };
22
+ return {
23
+ documentLoader: wrap(documentLoader, "object", (e) => objectFailure = e),
24
+ contextLoader: wrap(contextLoader, "context", (e) => contextFailure = e),
25
+ clearFailures() {
26
+ objectFailure = void 0;
27
+ contextFailure = void 0;
28
+ },
29
+ getObjectFailure: () => objectFailure,
30
+ getContextFailure: () => contextFailure
31
+ };
32
+ }
33
+ function escapeControlCharacters(message) {
34
+ return message.replace(/[\x00-\x1f\x7f-\x9f]/g, (character) => `\\x${character.charCodeAt(0).toString(16).padStart(2, "0")}`);
35
+ }
36
+ /** Describes a failure without recommending signing unless HTTP supports it. */
37
+ function describeLookupFailure(failure, authorizedFetch) {
38
+ const error = failure?.error;
39
+ const http = error instanceof FetchError ? /^(.+): HTTP (\d{3}): \1$/s.exec(error.message) : null;
40
+ if (http != null) {
41
+ const status = Number(http[2]);
42
+ const suggestsAuthorizedFetch = failure?.source === "object" && !authorizedFetch && [
43
+ 401,
44
+ 403,
45
+ 404
46
+ ].includes(status);
47
+ return {
48
+ message: escapeControlCharacters(`HTTP ${status} from ${http[1]}.`) + (suggestsAuthorizedFetch ? " It may be a private object. Try with -a/--authorized-fetch." : ""),
49
+ suggestsAuthorizedFetch
50
+ };
51
+ }
52
+ const causes = [];
53
+ const seen = /* @__PURE__ */ new Set();
54
+ let cause = error;
55
+ while (cause instanceof Error && !seen.has(cause) && causes.length < 8) {
56
+ seen.add(cause);
57
+ causes.push(cause);
58
+ if (cause instanceof UrlError && cause.reason === "dns" || cause instanceof TypeError && /dns error|failed to lookup address information/i.test(cause.message) || "code" in cause && (cause.code === "ENOTFOUND" || cause.code === "EAI_AGAIN")) return {
59
+ message: "Could not resolve the host in the URL. Check the URL and your network connection.",
60
+ suggestsAuthorizedFetch: false
61
+ };
62
+ cause = cause.cause;
63
+ }
64
+ return {
65
+ message: escapeControlCharacters(causes.length > 0 ? causes.map((e) => e.message).join(": ") : error == null ? "Could not fetch or parse the object. Check the URL or actor handle." : String(error)),
66
+ suggestsAuthorizedFetch: false
67
+ };
68
+ }
69
+ //#endregion
70
+ export { createLookupDiagnostics, describeLookupFailure };
package/dist/lookup.js CHANGED
@@ -4,9 +4,10 @@ import { colorEnabled, colors, describeError, formatObject } from "./utils.js";
4
4
  import { configureLogging } from "./log.js";
5
5
  import { getContextLoader, getDocumentLoader as getDocumentLoader$1 } from "./docloader.js";
6
6
  import { spawnTemporaryServer } from "./tempserver.js";
7
+ import { createLookupDiagnostics, describeLookupFailure } from "./diagnostics.js";
7
8
  import { renderImages } from "./imagerenderer.js";
8
9
  import process from "node:process";
9
- import { message, optionNames } from "@optique/core";
10
+ import { message, optionNames, text } from "@optique/core";
10
11
  import { printError } from "@optique/run";
11
12
  import { createWriteStream } from "node:fs";
12
13
  import { UrlError, expandIPv6Address, isValidPublicIPv4Address, isValidPublicIPv6Address } from "@fedify/vocab-runtime";
@@ -164,6 +165,9 @@ function wrapDocumentLoaderWithTimeout(loader, timeoutSeconds) {
164
165
  return loader(url, {
165
166
  ...options,
166
167
  signal
168
+ }).catch((error) => {
169
+ if (signal?.aborted) throw signal.reason;
170
+ throw error;
167
171
  }).finally(() => clearTimeoutSignal(signal));
168
172
  };
169
173
  }
@@ -174,7 +178,7 @@ function handleTimeoutError(spinner, timeoutSeconds, url) {
174
178
  }
175
179
  function isPrivateAddressError(error) {
176
180
  const lowerMessage = describeError(error).toLowerCase();
177
- if (error instanceof UrlError) return lowerMessage.includes("invalid or private address") || lowerMessage.includes("localhost is not allowed");
181
+ if (error instanceof UrlError) return error.reason === "disallowed" && (lowerMessage.includes("invalid or private address") || lowerMessage.includes("localhost is not allowed"));
178
182
  return lowerMessage.includes("private address") || lowerMessage.includes("private ip") || lowerMessage.includes("localhost") || lowerMessage.includes("loopback");
179
183
  }
180
184
  function getPrivateUrlCandidate(candidate) {
@@ -212,6 +216,7 @@ function printRecursivePrivateContextHint(privateContextUrl) {
212
216
  printError(message`Recursive JSON-LD context URL ${url(privateContextUrl)} is always blocked, even with ${optionNames(["-p", "--allow-private-address"])}. Use ${optionNames(["-S", "--suppress-errors"])} to skip blocked steps.`);
213
217
  }
214
218
  function getLookupFailureHint(error, options = {}) {
219
+ if (error instanceof UrlError && error.reason === "dns") return "dns";
215
220
  if (isPrivateAddressError(error)) return options.recursive ? "recursive-private-address" : "private-address";
216
221
  return "authorized-fetch";
217
222
  }
@@ -225,6 +230,9 @@ function printLookupFailureHint(authLoader, error, options = {}) {
225
230
  const hint = getLookupFailureHint(error, options);
226
231
  if (!shouldPrintLookupFailureHint(authLoader, hint)) return;
227
232
  switch (hint) {
233
+ case "dns":
234
+ printError(message`DNS lookup failed. Check the hostname and network connectivity.`);
235
+ return;
228
236
  case "private-address":
229
237
  printError(message`The URL appears to be private or localhost. Try with ${optionNames(["-p", "--allow-private-address"])}.`);
230
238
  return;
@@ -305,6 +313,29 @@ async function runLookup(command, deps = {}) {
305
313
  exit: (code) => process.exit(code),
306
314
  ...deps
307
315
  };
316
+ const lookupDiagnosed = async (identifier, options) => {
317
+ const diagnostics = createLookupDiagnostics(options.documentLoader, options.contextLoader);
318
+ try {
319
+ const object = await effectiveDeps.lookupObject(identifier, {
320
+ ...options,
321
+ ...diagnostics
322
+ });
323
+ return {
324
+ object,
325
+ failure: object == null ? diagnostics.getObjectFailure() ?? diagnostics.getContextFailure() : void 0,
326
+ thrownError: void 0
327
+ };
328
+ } catch (error) {
329
+ return {
330
+ object: null,
331
+ thrownError: error,
332
+ failure: diagnostics.getContextFailure() ?? diagnostics.getObjectFailure() ?? {
333
+ error,
334
+ source: "other"
335
+ }
336
+ };
337
+ }
338
+ };
308
339
  if (command.urls.length < 1) {
309
340
  printError(message`At least one URL or actor handle must be provided.`);
310
341
  effectiveDeps.exit(1);
@@ -556,39 +587,33 @@ async function runLookup(command, deps = {}) {
556
587
  for (let urlIndex = 0; urlIndex < command.urls.length; urlIndex++) {
557
588
  const url = command.urls[urlIndex];
558
589
  if (urlIndex > 0) spinner.text = `Looking up collection ${urlIndex + 1}/${command.urls.length}...`;
559
- let collection = null;
560
- try {
561
- collection = await effectiveDeps.lookupObject(url, {
562
- documentLoader: initialAuthLoader ?? initialDocumentLoader,
563
- contextLoader,
564
- userAgent: command.userAgent
565
- });
566
- } catch (error) {
567
- if (error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, url);
590
+ const result = await lookupDiagnosed(url, {
591
+ documentLoader: initialAuthLoader ?? initialDocumentLoader,
592
+ contextLoader,
593
+ userAgent: command.userAgent
594
+ });
595
+ const collection = result.object;
596
+ if (collection == null) {
597
+ if (result.thrownError instanceof TimeoutError || result.failure?.error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, url);
568
598
  else {
569
599
  spinner.fail(`Failed to fetch object: ${colors.red(url)}.`);
570
- printLookupFailureHint(authLoader, error);
600
+ printError(message`${text(describeLookupFailure(result.failure, authLoader != null).message)}`);
571
601
  }
572
602
  await finalizeAndExit(1);
573
603
  return;
574
604
  }
575
- if (collection == null) {
576
- spinner.fail(`Failed to fetch object: ${colors.red(url)}.`);
577
- if (authLoader == null) printError(message`It may be a private object. Try with ${optionNames(["-a", "--authorized-fetch"])}.`);
578
- await finalizeAndExit(1);
579
- return;
580
- }
581
605
  if (!(collection instanceof Collection)) {
582
606
  spinner.fail(`Not a collection: ${colors.red(url)}. The -t/--traverse option requires a collection.`);
583
607
  await finalizeAndExit(1);
584
608
  return;
585
609
  }
586
610
  spinner.succeed(`Fetched collection: ${colors.green(url)}.`);
611
+ const diagnostics = createLookupDiagnostics(authLoader ?? documentLoader, contextLoader);
587
612
  try {
588
613
  if (command.reverse) {
589
614
  const { items: traversedItems, error: traversalError } = await collectAsyncItems(effectiveDeps.traverseCollection(collection, {
590
- documentLoader: authLoader ?? documentLoader,
591
- contextLoader,
615
+ documentLoader: diagnostics.documentLoader,
616
+ contextLoader: diagnostics.contextLoader,
592
617
  suppressError: command.suppressErrors
593
618
  }));
594
619
  for (let index = traversedItems.length - 1; index >= 0; index--) {
@@ -601,7 +626,14 @@ async function runLookup(command, deps = {}) {
601
626
  url,
602
627
  error
603
628
  });
604
- spinner.fail(`Failed to write output for: ${colors.red(url)}.`);
629
+ if (error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, url);
630
+ else {
631
+ spinner.fail(`Failed to write output for: ${colors.red(url)}.`);
632
+ printError(message`${text(describeLookupFailure({
633
+ error,
634
+ source: "other"
635
+ }, authLoader != null).message)}`);
636
+ }
605
637
  await finalizeAndExit(1);
606
638
  return;
607
639
  }
@@ -609,8 +641,8 @@ async function runLookup(command, deps = {}) {
609
641
  }
610
642
  if (traversalError != null) throw traversalError;
611
643
  } else for await (const item of effectiveDeps.traverseCollection(collection, {
612
- documentLoader: authLoader ?? documentLoader,
613
- contextLoader,
644
+ documentLoader: diagnostics.documentLoader,
645
+ contextLoader: diagnostics.contextLoader,
614
646
  suppressError: command.suppressErrors
615
647
  })) {
616
648
  try {
@@ -621,23 +653,38 @@ async function runLookup(command, deps = {}) {
621
653
  url,
622
654
  error
623
655
  });
624
- spinner.fail(`Failed to write output for: ${colors.red(url)}.`);
656
+ if (error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, url);
657
+ else {
658
+ spinner.fail(`Failed to write output for: ${colors.red(url)}.`);
659
+ printError(message`${text(describeLookupFailure({
660
+ error,
661
+ source: "other"
662
+ }, authLoader != null).message)}`);
663
+ }
625
664
  await finalizeAndExit(1);
626
665
  return;
627
666
  }
628
667
  totalItems++;
668
+ diagnostics.clearFailures();
629
669
  }
630
670
  } catch (error) {
631
671
  logger.error("Failed to complete the traversal for {url}: {error}", {
632
672
  url,
633
673
  error
634
674
  });
635
- if (error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, url);
675
+ const failure = diagnostics.getContextFailure() ?? diagnostics.getObjectFailure() ?? {
676
+ error,
677
+ source: "other"
678
+ };
679
+ if (error instanceof TimeoutError || failure.error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, url);
636
680
  else {
637
681
  spinner.fail(`Failed to complete the traversal for: ${colors.red(url)}.`);
638
- const hint = getLookupFailureHint(error);
639
- if (shouldSuggestSuppressErrorsForLookupFailure(authLoader, hint)) printError(message`Use the ${optionNames(["-S", "--suppress-errors"])} option to suppress partial errors.`);
640
- else printLookupFailureHint(authLoader, error);
682
+ if (isPrivateAddressError(failure.error)) printLookupFailureHint(authLoader, failure.error);
683
+ else {
684
+ const diagnostic = describeLookupFailure(failure, authLoader != null);
685
+ printError(message`${text(diagnostic.message)}`);
686
+ if (!diagnostic.suggestsAuthorizedFetch && !command.suppressErrors) printError(message`Use the -S/--suppress-errors option to suppress partial errors.`);
687
+ }
641
688
  }
642
689
  await finalizeAndExit(1);
643
690
  return;
@@ -647,31 +694,24 @@ async function runLookup(command, deps = {}) {
647
694
  await finalizeAndExit(0);
648
695
  return;
649
696
  }
650
- const promises = [];
651
- for (const url of command.urls) promises.push(effectiveDeps.lookupObject(url, {
697
+ const objects = await Promise.all(command.urls.map((url) => lookupDiagnosed(url, {
652
698
  documentLoader: initialAuthLoader ?? initialDocumentLoader,
653
699
  contextLoader,
654
700
  userAgent: command.userAgent
655
- }).catch((error) => {
656
- if (error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, url);
657
- throw error;
658
- }));
659
- let objects = [];
660
- try {
661
- objects = await Promise.all(promises);
662
- } catch (_error) {
663
- await finalizeAndExit(1);
664
- return;
665
- }
701
+ })));
666
702
  spinner.stop();
667
703
  let success = true;
668
704
  let printedCount = 0;
669
705
  const successfulObjects = [];
670
- for (const [i, obj] of objects.entries()) {
706
+ for (const [i, result] of objects.entries()) {
707
+ const obj = result.object;
671
708
  const url = command.urls[i];
672
709
  if (obj == null) {
673
- spinner.fail(`Failed to fetch ${colors.red(url)}`);
674
- if (authLoader == null) printError(message`It may be a private object. Try with ${optionNames(["-a", "--authorized-fetch"])}.`);
710
+ if (result.thrownError instanceof TimeoutError || result.failure?.error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout, url);
711
+ else {
712
+ spinner.fail(`Failed to fetch ${colors.red(url)}`);
713
+ printError(message`${text(describeLookupFailure(result.failure, authLoader != null).message)}`);
714
+ }
675
715
  success = false;
676
716
  } else {
677
717
  spinner.succeed(`Fetched object: ${colors.green(url)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/cli",
3
- "version": "2.4.0-dev.2155+2ccc5085",
3
+ "version": "2.4.0-dev.2157+b76e5888",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "README.md",
@@ -88,21 +88,22 @@
88
88
  "temporal-polyfill": "^1.0.1",
89
89
  "valibot": "^1.4.0",
90
90
  "yaml": "^2.9.0",
91
- "@fedify/init": "2.4.0-dev.2155+2ccc5085",
92
- "@fedify/fedify": "2.4.0-dev.2155+2ccc5085",
93
- "@fedify/sqlite": "2.4.0-dev.2155+2ccc5085",
94
- "@fedify/relay": "2.4.0-dev.2155+2ccc5085",
95
- "@fedify/vocab": "2.4.0-dev.2155+2ccc5085",
96
- "@fedify/vocab-runtime": "2.4.0-dev.2155+2ccc5085",
97
- "@fedify/vocab-tools": "2.4.0-dev.2155+2ccc5085",
98
- "@fedify/webfinger": "2.4.0-dev.2155+2ccc5085"
91
+ "@fedify/fedify": "2.4.0-dev.2157+b76e5888",
92
+ "@fedify/relay": "2.4.0-dev.2157+b76e5888",
93
+ "@fedify/sqlite": "2.4.0-dev.2157+b76e5888",
94
+ "@fedify/vocab": "2.4.0-dev.2157+b76e5888",
95
+ "@fedify/init": "2.4.0-dev.2157+b76e5888",
96
+ "@fedify/vocab-runtime": "2.4.0-dev.2157+b76e5888",
97
+ "@fedify/webfinger": "2.4.0-dev.2157+b76e5888",
98
+ "@fedify/vocab-tools": "2.4.0-dev.2157+b76e5888"
99
99
  },
100
100
  "devDependencies": {
101
101
  "@types/bun": "^1.2.23",
102
102
  "@types/node": "^24.12.2",
103
103
  "@types/pngjs": "^6.0.5",
104
104
  "tsdown": "^0.22.0",
105
- "typescript": "^6.0.0"
105
+ "typescript": "^6.0.0",
106
+ "@fedify/fixture": "2.0.0"
106
107
  },
107
108
  "scripts": {
108
109
  "build:self": "tsdown",