@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 +1 -1
- package/dist/diagnostics.js +70 -0
- package/dist/lookup.js +84 -44
- package/package.json +11 -10
package/dist/deno.js
CHANGED
|
@@ -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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
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
|
-
})
|
|
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,
|
|
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
|
-
|
|
674
|
-
|
|
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.
|
|
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/
|
|
92
|
-
"@fedify/
|
|
93
|
-
"@fedify/sqlite": "2.4.0-dev.
|
|
94
|
-
"@fedify/
|
|
95
|
-
"@fedify/
|
|
96
|
-
"@fedify/vocab-runtime": "2.4.0-dev.
|
|
97
|
-
"@fedify/
|
|
98
|
-
"@fedify/
|
|
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",
|