@fedify/cli 2.2.0-dev.938 → 2.2.0-dev.946

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 "@js-temporal/polyfill";
2
2
  //#region deno.json
3
- var version = "2.2.0-dev.938+94d98c0c";
3
+ var version = "2.2.0-dev.946+1764300f";
4
4
  //#endregion
5
5
  export { version };
package/dist/lookup.js CHANGED
@@ -6,16 +6,18 @@ import { colorEnabled, colors, formatObject } from "./utils.js";
6
6
  import { configContext } from "./config.js";
7
7
  import { createTunnelServiceOption, userAgentOption } from "./options.js";
8
8
  import { renderImages } from "./imagerenderer.js";
9
+ import { url } from "@optique/core/message";
9
10
  import { path, printError } from "@optique/run";
10
11
  import process from "node:process";
11
- import { argument, choice, command, constant, flag, float, integer, map, merge, message, multiple, object, option, optionNames, optional, or, string, withDefault } from "@optique/core";
12
+ import { argument, choice, command, constant, flag, float, integer, map, merge, message as message$1, multiple, object, option, optionNames, optional, or, string, withDefault } from "@optique/core";
12
13
  import { generateCryptoKeyPair, getAuthenticatedDocumentLoader, respondWithObject } from "@fedify/fedify";
13
14
  import { Application, Collection, CryptographicKey, Object as Object$1, lookupObject, traverseCollection } from "@fedify/vocab";
14
15
  import { getLogger } from "@logtape/logtape";
15
16
  import ora from "ora";
16
- import { UrlError } from "@fedify/vocab-runtime";
17
+ import { UrlError, expandIPv6Address, isValidPublicIPv4Address, isValidPublicIPv6Address } from "@fedify/vocab-runtime";
17
18
  import { bindConfig } from "@optique/config";
18
19
  import { createWriteStream } from "node:fs";
20
+ import { isIP } from "node:net";
19
21
  //#region src/lookup.ts
20
22
  const logger = getLogger([
21
23
  "fedify",
@@ -37,27 +39,25 @@ const recurseProperties = [
37
39
  MISSKEY_QUOTE_IRI,
38
40
  FEDIBIRD_QUOTE_IRI
39
41
  ];
40
- const suppressErrorsOption = bindConfig(flag("-S", "--suppress-errors", { description: message`Suppress partial errors during traversal or recursion.` }), {
42
+ const suppressErrorsOption = bindConfig(flag("-S", "--suppress-errors", { description: message$1`Suppress partial errors during traversal or recursion.` }), {
41
43
  context: configContext,
42
44
  key: (config) => config.lookup?.suppressErrors ?? false,
43
45
  default: false
44
46
  });
45
- const allowPrivateAddressOption = bindConfig(flag("-p", "--allow-private-address", { description: message`Allow private IP addresses for URLs discovered \
46
- during traversal. This option only has an effect when used together \
47
- with ${optionNames(["-t", "--traverse"])}, since URLs explicitly \
48
- provided on the command line always allow private addresses and \
49
- recursive fetches via ${optionNames(["--recurse"])} always disallow \
50
- them.` }), {
47
+ const allowPrivateAddressOption = bindConfig(flag("-p", "--allow-private-address", { description: message$1`Allow private IP addresses for URLs discovered \
48
+ during traversal or recursive object fetches. Recursive JSON-LD \
49
+ context URLs always remain blocked. URLs explicitly provided on the \
50
+ command line always allow private addresses.` }), {
51
51
  context: configContext,
52
52
  key: (config) => config.lookup?.allowPrivateAddress ?? false,
53
53
  default: false
54
54
  });
55
55
  const authorizedFetchOption = withDefault(object("Authorized fetch options", {
56
- authorizedFetch: bindConfig(map(flag("-a", "--authorized-fetch", { description: message`Sign the request with an one-time key.` }), () => true), {
56
+ authorizedFetch: bindConfig(map(flag("-a", "--authorized-fetch", { description: message$1`Sign the request with an one-time key.` }), () => true), {
57
57
  context: configContext,
58
58
  key: (config) => config.lookup?.authorizedFetch ? true : void 0
59
59
  }),
60
- firstKnock: bindConfig(option("--first-knock", choice(["draft-cavage-http-signatures-12", "rfc9421"]), { description: message`The first-knock spec for ${optionNames(["-a", "--authorized-fetch"])}. It is used for the double-knocking technique.` }), {
60
+ firstKnock: bindConfig(option("--first-knock", choice(["draft-cavage-http-signatures-12", "rfc9421"]), { description: message$1`The first-knock spec for ${optionNames(["-a", "--authorized-fetch"])}. It is used for the double-knocking technique.` }), {
61
61
  context: configContext,
62
62
  key: (config) => config.lookup?.firstKnock ?? "draft-cavage-http-signatures-12",
63
63
  default: "draft-cavage-http-signatures-12"
@@ -70,20 +70,20 @@ const authorizedFetchOption = withDefault(object("Authorized fetch options", {
70
70
  });
71
71
  const lookupModeOption = withDefault(or(object("Recurse options", {
72
72
  traverse: constant(false),
73
- recurse: bindConfig(option("--recurse", choice(recurseProperties, { metavar: "PROPERTY" }), { description: message`Recursively follow a relationship property.` }), {
73
+ recurse: bindConfig(option("--recurse", choice(recurseProperties, { metavar: "PROPERTY" }), { description: message$1`Recursively follow a relationship property.` }), {
74
74
  context: configContext,
75
75
  key: (config) => config.lookup?.recurse
76
76
  }),
77
77
  recurseDepth: withDefault(bindConfig(option("--recurse-depth", integer({
78
78
  min: 1,
79
79
  metavar: "DEPTH"
80
- }), { description: message`Maximum recursion depth for ${optionNames(["--recurse"])}.` }), {
80
+ }), { description: message$1`Maximum recursion depth for ${optionNames(["--recurse"])}.` }), {
81
81
  context: configContext,
82
82
  key: (config) => config.lookup?.recurseDepth
83
83
  }), 20),
84
84
  suppressErrors: suppressErrorsOption
85
85
  }), object("Traverse options", {
86
- traverse: bindConfig(flag("-t", "--traverse", { description: message`Traverse the given collection(s) to fetch all items.` }), {
86
+ traverse: bindConfig(flag("-t", "--traverse", { description: message$1`Traverse the given collection(s) to fetch all items.` }), {
87
87
  context: configContext,
88
88
  key: (config) => config.lookup?.traverse ?? false,
89
89
  default: false
@@ -102,22 +102,22 @@ const lookupCommand = command("lookup", merge(object({ command: constant("lookup
102
102
  timeout: optional(bindConfig(option("-T", "--timeout", float({
103
103
  min: 0,
104
104
  metavar: "SECONDS"
105
- }), { description: message`Set timeout for network requests in seconds.` }), {
105
+ }), { description: message$1`Set timeout for network requests in seconds.` }), {
106
106
  context: configContext,
107
107
  key: (config) => config.lookup?.timeout
108
108
  }))
109
- })), object("Arguments", { urls: multiple(argument(string({ metavar: "URL_OR_HANDLE" }), { description: message`One or more URLs or handles to look up.` }), { min: 1 }) }), object("Output options", {
110
- reverse: bindConfig(flag("--reverse", { description: message`Reverse the output order of fetched objects or items.` }), {
109
+ })), object("Arguments", { urls: multiple(argument(string({ metavar: "URL_OR_HANDLE" }), { description: message$1`One or more URLs or handles to look up.` }), { min: 1 }) }), object("Output options", {
110
+ reverse: bindConfig(flag("--reverse", { description: message$1`Reverse the output order of fetched objects or items.` }), {
111
111
  context: configContext,
112
112
  key: (config) => config.lookup?.reverse ?? false,
113
113
  default: false
114
114
  }),
115
- format: bindConfig(optional(or(map(flag("-r", "--raw", { description: message`Print the fetched JSON-LD document as is.` }), () => "raw"), map(flag("-C", "--compact", { description: message`Compact the fetched JSON-LD document.` }), () => "compact"), map(flag("-e", "--expand", { description: message`Expand the fetched JSON-LD document.` }), () => "expand"))), {
115
+ format: bindConfig(optional(or(map(flag("-r", "--raw", { description: message$1`Print the fetched JSON-LD document as is.` }), () => "raw"), map(flag("-C", "--compact", { description: message$1`Compact the fetched JSON-LD document.` }), () => "compact"), map(flag("-e", "--expand", { description: message$1`Expand the fetched JSON-LD document.` }), () => "expand"))), {
116
116
  context: configContext,
117
117
  key: (config) => config.lookup?.defaultFormat ?? "default",
118
118
  default: "default"
119
119
  }),
120
- separator: bindConfig(option("-s", "--separator", string({ metavar: "SEPARATOR" }), { description: message`Specify the separator between adjacent output objects or collection items.` }), {
120
+ separator: bindConfig(option("-s", "--separator", string({ metavar: "SEPARATOR" }), { description: message$1`Specify the separator between adjacent output objects or collection items.` }), {
121
121
  context: configContext,
122
122
  key: (config) => config.lookup?.separator ?? "----",
123
123
  default: "----"
@@ -126,10 +126,10 @@ const lookupCommand = command("lookup", merge(object({ command: constant("lookup
126
126
  metavar: "OUTPUT_PATH",
127
127
  type: "file",
128
128
  allowCreate: true
129
- }), { description: message`Specify the output file path.` }))
129
+ }), { description: message$1`Specify the output file path.` }))
130
130
  })), {
131
- brief: message`Look up Activity Streams objects.`,
132
- description: message`Look up Activity Streams objects by URL or actor handle.
131
+ brief: message$1`Look up Activity Streams objects.`,
132
+ description: message$1`Look up Activity Streams objects by URL or actor handle.
133
133
 
134
134
  The arguments can be either URLs or actor handles (e.g., ${"@username@domain"}), and they can be multiple.`
135
135
  });
@@ -281,13 +281,47 @@ function wrapDocumentLoaderWithTimeout(loader, timeoutSeconds) {
281
281
  function handleTimeoutError(spinner, timeoutSeconds, url) {
282
282
  const urlText = url ? ` for: ${colors.red(url)}` : "";
283
283
  spinner.fail(`Request timed out after ${timeoutSeconds} seconds${urlText}.`);
284
- printError(message`Try increasing the timeout with -T/--timeout option or check network connectivity.`);
284
+ printError(message$1`Try increasing the timeout with ${optionNames(["-T", "--timeout"])} option or check network connectivity.`);
285
285
  }
286
286
  function isPrivateAddressError(error) {
287
287
  const lowerMessage = (error instanceof Error ? error.message : String(error)).toLowerCase();
288
288
  if (error instanceof UrlError) return lowerMessage.includes("invalid or private address") || lowerMessage.includes("localhost is not allowed");
289
289
  return lowerMessage.includes("private address") || lowerMessage.includes("private ip") || lowerMessage.includes("localhost") || lowerMessage.includes("loopback");
290
290
  }
291
+ function getPrivateUrlCandidate(candidate) {
292
+ if (typeof candidate !== "string" && !(candidate instanceof URL)) return null;
293
+ try {
294
+ const url = new URL(candidate);
295
+ const hostname = url.hostname;
296
+ if (hostname === "localhost") return url;
297
+ const normalized = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
298
+ const ipVersion = isIP(normalized);
299
+ if (ipVersion === 4) return isValidPublicIPv4Address(normalized) ? null : url;
300
+ if (ipVersion === 6) return isValidPublicIPv6Address(expandIPv6Address(normalized)) ? null : url;
301
+ return null;
302
+ } catch {
303
+ return null;
304
+ }
305
+ }
306
+ function isPrivateAddressTarget(target) {
307
+ return getPrivateUrlCandidate(target) != null;
308
+ }
309
+ function getPrivateContextUrl(error) {
310
+ const errorMessage = error instanceof Error ? error.message : String(error);
311
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl" || !errorMessage.includes("valid JSON-LD object")) return null;
312
+ const structuredError = error;
313
+ const structuredUrl = getPrivateUrlCandidate(structuredError.details?.url) ?? getPrivateUrlCandidate(structuredError.url);
314
+ if (structuredUrl != null) return structuredUrl;
315
+ const match = errorMessage.match(/URL:\s*"([^"]+)"/);
316
+ if (match == null) return null;
317
+ return getPrivateUrlCandidate(match[1]);
318
+ }
319
+ function printRecursivePrivateAddressHint() {
320
+ printError(message$1`The recursive target appears to be private or localhost. Try with ${optionNames(["-p", "--allow-private-address"])}, or use ${optionNames(["-S", "--suppress-errors"])} to skip blocked steps.`);
321
+ }
322
+ function printRecursivePrivateContextHint(privateContextUrl) {
323
+ printError(message$1`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.`);
324
+ }
291
325
  function getLookupFailureHint(error, options = {}) {
292
326
  if (isPrivateAddressError(error)) return options.recursive ? "recursive-private-address" : "private-address";
293
327
  return "authorized-fetch";
@@ -303,13 +337,13 @@ function printLookupFailureHint(authLoader, error, options = {}) {
303
337
  if (!shouldPrintLookupFailureHint(authLoader, hint)) return;
304
338
  switch (hint) {
305
339
  case "private-address":
306
- printError(message`The URL appears to be private or localhost. Try with -p/--allow-private-address.`);
340
+ printError(message$1`The URL appears to be private or localhost. Try with ${optionNames(["-p", "--allow-private-address"])}.`);
307
341
  return;
308
342
  case "recursive-private-address":
309
- printError(message`Recursive fetches do not allow private/localhost URLs. Use -S/--suppress-errors to skip blocked steps, or fetch those targets explicitly without --recurse.`);
343
+ printRecursivePrivateAddressHint();
310
344
  return;
311
345
  case "authorized-fetch":
312
- printError(message`It may be a private object. Try with -a/--authorized-fetch.`);
346
+ printError(message$1`It may be a private object. Try with ${optionNames(["-a", "--authorized-fetch"])}.`);
313
347
  return;
314
348
  }
315
349
  }
@@ -383,7 +417,7 @@ async function runLookup(command, deps = {}) {
383
417
  ...deps
384
418
  };
385
419
  if (command.urls.length < 1) {
386
- printError(message`At least one URL or actor handle must be provided.`);
420
+ printError(message$1`At least one URL or actor handle must be provided.`);
387
421
  effectiveDeps.exit(1);
388
422
  }
389
423
  if (command.debug) await configureLogging();
@@ -494,26 +528,12 @@ async function runLookup(command, deps = {}) {
494
528
  }
495
529
  spinner.text = `Looking up the ${command.recurse != null ? "object chain" : command.traverse ? "collection" : command.urls.length > 1 ? "objects" : "object"}...`;
496
530
  if (command.recurse != null) {
497
- const recursiveDocumentLoader = wrapDocumentLoaderWithTimeout(await getDocumentLoader$1({
498
- userAgent: command.userAgent,
499
- allowPrivateAddress: false
500
- }), command.timeout);
531
+ const initialLookupDocumentLoader = initialAuthLoader ?? initialDocumentLoader;
532
+ const recursiveLookupDocumentLoader = authLoader ?? documentLoader;
501
533
  const recursiveContextLoader = wrapDocumentLoaderWithTimeout(await getContextLoader({
502
534
  userAgent: command.userAgent,
503
535
  allowPrivateAddress: false
504
536
  }), command.timeout);
505
- const recursiveAuthLoader = command.authorizedFetch && authIdentity != null ? wrapDocumentLoaderWithTimeout(getAuthenticatedDocumentLoader(authIdentity, {
506
- allowPrivateAddress: false,
507
- userAgent: command.userAgent,
508
- specDeterminer: {
509
- determineSpec() {
510
- return command.firstKnock;
511
- },
512
- rememberSpec() {}
513
- }
514
- }), command.timeout) : void 0;
515
- const initialLookupDocumentLoader = initialAuthLoader ?? initialDocumentLoader;
516
- const recursiveLookupDocumentLoader = recursiveAuthLoader ?? recursiveDocumentLoader;
517
537
  let totalObjects = 0;
518
538
  const recurseDepth = command.recurseDepth;
519
539
  for (let urlIndex = 0; urlIndex < command.urls.length; urlIndex++) {
@@ -538,7 +558,7 @@ async function runLookup(command, deps = {}) {
538
558
  }
539
559
  if (current == null) {
540
560
  spinner.fail(`Failed to fetch object: ${colors.red(url)}.`);
541
- if (authLoader == null) printError(message`It may be a private object. Try with -a/--authorized-fetch.`);
561
+ if (authLoader == null) printError(message$1`It may be a private object. Try with ${optionNames(["-a", "--authorized-fetch"])}.`);
542
562
  await finalizeAndExit(1);
543
563
  return;
544
564
  }
@@ -579,11 +599,18 @@ async function runLookup(command, deps = {}) {
579
599
  if (error instanceof TimeoutError) handleTimeoutError(spinner, command.timeout);
580
600
  else if (error instanceof RecursiveLookupError) {
581
601
  spinner.fail(`Failed to recursively fetch object: ${colors.red(error.target)}.`);
582
- if (authLoader == null) printError(message`It may be a private object. Try with -a/--authorized-fetch.`);
602
+ if (!command.allowPrivateAddress && isPrivateAddressTarget(error.target)) printRecursivePrivateAddressHint();
603
+ else if (authLoader == null) printError(message$1`It may be a private object. Try with ${optionNames(["-a", "--authorized-fetch"])}.`);
583
604
  } else {
584
605
  spinner.fail("Failed to recursively fetch object.");
606
+ const privateContextUrl = getPrivateContextUrl(error);
607
+ if (privateContextUrl != null) {
608
+ printRecursivePrivateContextHint(privateContextUrl);
609
+ await finalizeAndExit(1);
610
+ return;
611
+ }
585
612
  const hint = getLookupFailureHint(error, { recursive: true });
586
- if (shouldSuggestSuppressErrorsForLookupFailure(authLoader, hint)) printError(message`Use the -S/--suppress-errors option to suppress partial errors.`);
613
+ if (shouldSuggestSuppressErrorsForLookupFailure(authLoader, hint)) printError(message$1`Use the ${optionNames(["-S", "--suppress-errors"])} option to suppress partial errors.`);
587
614
  else printLookupFailureHint(authLoader, error, { recursive: true });
588
615
  }
589
616
  await finalizeAndExit(1);
@@ -657,7 +684,7 @@ async function runLookup(command, deps = {}) {
657
684
  }
658
685
  if (collection == null) {
659
686
  spinner.fail(`Failed to fetch object: ${colors.red(url)}.`);
660
- if (authLoader == null) printError(message`It may be a private object. Try with -a/--authorized-fetch.`);
687
+ if (authLoader == null) printError(message$1`It may be a private object. Try with ${optionNames(["-a", "--authorized-fetch"])}.`);
661
688
  await finalizeAndExit(1);
662
689
  return;
663
690
  }
@@ -719,7 +746,7 @@ async function runLookup(command, deps = {}) {
719
746
  else {
720
747
  spinner.fail(`Failed to complete the traversal for: ${colors.red(url)}.`);
721
748
  const hint = getLookupFailureHint(error);
722
- if (shouldSuggestSuppressErrorsForLookupFailure(authLoader, hint)) printError(message`Use the -S/--suppress-errors option to suppress partial errors.`);
749
+ if (shouldSuggestSuppressErrorsForLookupFailure(authLoader, hint)) printError(message$1`Use the ${optionNames(["-S", "--suppress-errors"])} option to suppress partial errors.`);
723
750
  else printLookupFailureHint(authLoader, error);
724
751
  }
725
752
  await finalizeAndExit(1);
@@ -754,7 +781,7 @@ async function runLookup(command, deps = {}) {
754
781
  const url = command.urls[i];
755
782
  if (obj == null) {
756
783
  spinner.fail(`Failed to fetch ${colors.red(url)}`);
757
- if (authLoader == null) printError(message`It may be a private object. Try with -a/--authorized-fetch.`);
784
+ if (authLoader == null) printError(message$1`It may be a private object. Try with ${optionNames(["-a", "--authorized-fetch"])}.`);
758
785
  success = false;
759
786
  } else {
760
787
  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.2.0-dev.938+94d98c0c",
3
+ "version": "2.2.0-dev.946+1764300f",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "README.md",
@@ -86,14 +86,14 @@
86
86
  "smol-toml": "^1.6.0",
87
87
  "srvx": "^0.8.7",
88
88
  "valibot": "^1.2.0",
89
- "@fedify/init": "2.2.0-dev.938+94d98c0c",
90
- "@fedify/fedify": "2.2.0-dev.938+94d98c0c",
91
- "@fedify/relay": "2.2.0-dev.938+94d98c0c",
92
- "@fedify/sqlite": "2.2.0-dev.938+94d98c0c",
93
- "@fedify/vocab-tools": "2.2.0-dev.938+94d98c0c",
94
- "@fedify/webfinger": "2.2.0-dev.938+94d98c0c",
95
- "@fedify/vocab-runtime": "2.2.0-dev.938+94d98c0c",
96
- "@fedify/vocab": "2.2.0-dev.938+94d98c0c"
89
+ "@fedify/fedify": "2.2.0-dev.946+1764300f",
90
+ "@fedify/init": "2.2.0-dev.946+1764300f",
91
+ "@fedify/vocab-runtime": "2.2.0-dev.946+1764300f",
92
+ "@fedify/relay": "2.2.0-dev.946+1764300f",
93
+ "@fedify/vocab-tools": "2.2.0-dev.946+1764300f",
94
+ "@fedify/webfinger": "2.2.0-dev.946+1764300f",
95
+ "@fedify/sqlite": "2.2.0-dev.946+1764300f",
96
+ "@fedify/vocab": "2.2.0-dev.946+1764300f"
97
97
  },
98
98
  "devDependencies": {
99
99
  "@types/bun": "^1.2.23",