Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
23574ef
Add recurse mode and fix lookup separators
dahlia Mar 7, 2026
cd5c522
Validate lookup.recurseDepth config as positive integer
dahlia Mar 8, 2026
10b30f4
Correct lookup docs for traverse/recurse multi-argument behavior
dahlia Mar 8, 2026
ee07874
Print exactly one separator between printed lookup objects
dahlia Mar 8, 2026
acec2d3
Reuse output stream across lookup multi-object writes
dahlia Mar 8, 2026
d418286
Avoid poisoning visited set on suppressed lookup failures
dahlia Mar 8, 2026
280502b
Simplify recursive target resolver for validated recurse props
dahlia Mar 8, 2026
7bb5605
Fix recurse visited scope and output separator destination
dahlia Mar 8, 2026
d9e97d2
Block recursive private-address fetches in lookup
dahlia Mar 8, 2026
8ee7f87
Use strict loader for recursive lookup network fetches
dahlia Mar 8, 2026
5f3fd7a
Write separators to the configured lookup output
dahlia Mar 8, 2026
af550c2
Block private-address image fetches in lookup rendering
dahlia Mar 8, 2026
b27d76a
Isolate docloader cache keys by network policy
dahlia Mar 8, 2026
1db0a8b
Simplify image download error handling in lookup
dahlia Mar 8, 2026
5e3be4a
Support quoteUrl recursion targets in lookup
dahlia Mar 8, 2026
77c8e7e
Let Optique show recurse choices in help output
dahlia Mar 8, 2026
4f24401
Lazily initialize recurse-only document loaders
dahlia Mar 8, 2026
9fa76c7
Apply authorized fetch to recursive follow-up lookups
dahlia Mar 8, 2026
ff23875
Await stdout backpressure in writeSeparator
dahlia Mar 8, 2026
21b087e
Handle stream error events in lookup output
dahlia Mar 8, 2026
c6851fa
Lazily create runLookup output stream
dahlia Mar 8, 2026
e09a2a4
Handle recurse output write failures safely
dahlia Mar 8, 2026
1248f71
Guard multi-object output writes in lookup
dahlia Mar 8, 2026
09d2338
Align recurse rendering with strict context policy
dahlia Mar 8, 2026
204cdc6
Harden stream error handling in lookup output
dahlia Mar 8, 2026
dfce3fb
Apply strict URL policy in recurse lookups
dahlia Mar 8, 2026
a66d613
Deduplicate suppress-errors option definition
dahlia Mar 8, 2026
005c4c5
Always exit in finalizeAndExit cleanup path
dahlia Mar 8, 2026
933784d
Remove redundant recurse URL pre-validation
dahlia Mar 8, 2026
12ebc6f
Handle sync stream throw paths in lookup output helpers
dahlia Mar 8, 2026
442f1d5
Harden lookup output destination and finalization paths
dahlia Mar 8, 2026
9400c07
Tighten lookup loader safety and document public helpers
dahlia Mar 8, 2026
ee934f5
Harden lookup private-address handling and image redirects
dahlia Mar 8, 2026
5101c9c
Fix recurse private-address handling in authorized fetch mode
dahlia Mar 8, 2026
8f1d18b
Improve suppressed recurse diagnostics and output/body error handling
dahlia Mar 8, 2026
3c7eb9b
Scope private-address default, cleanup exit status, and deterministic…
dahlia Mar 8, 2026
95423d9
Harden image temp path extension handling
dahlia Mar 8, 2026
764e68d
Make recursive target mapping explicit for quote properties
dahlia Mar 8, 2026
aab372f
Clarify private-address policy for recursive lookup docs
dahlia Mar 8, 2026
f00210c
Improve lookup hints for private-address validation failures
dahlia Mar 8, 2026
b898148
Use switch-based recursion property mapping
dahlia Mar 8, 2026
da6a6fc
Fix lookup failure hint classification and suppression logic
dahlia Mar 8, 2026
9303408
Harden CLI document loader defaults against private-address access
dahlia Mar 8, 2026
7a05536
Handle extensionless image URLs in downloadImage safely
dahlia Mar 8, 2026
e859672
Keep private-address hints visible with authorized fetch enabled
dahlia Mar 8, 2026
d3737a8
Support extensionless nested image URLs in downloadImage
dahlia Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,31 @@ To be released.
[#473]: https://github.com/fedify-dev/fedify/issues/473
[#589]: https://github.com/fedify-dev/fedify/pull/589

### @fedify/cli

- Fixed `fedify lookup` printing separators with extra quotes between
adjacent objects/items in some output paths (e.g., recurse/traverse
flows). Separators are now printed as plain text consistently.
[[#608]]

- Added `--recurse` and `--recurse-depth` options to `fedify lookup` for
recursively following object relationships (e.g., reply chains via
`replyTarget` / `inReplyTo`, and quote chains via `quoteUrl` and quote
IRIs). `--traverse` and `--recurse` are now mutually exclusive,
`--recurse-depth` depends on `--recurse`, and `--suppress-errors` now
works in recurse mode as best-effort lookup.
[[#606], [#608]]

Comment thread
dahlia marked this conversation as resolved.
- Hardened `fedify lookup` by disallowing private/localhost document loads
by default. For local-development workflows, `-p`/`--allow-private-address`
(or `lookup.allowPrivateAddress = true` in config) can re-enable private
address access for explicit lookup/traverse requests. This option does
not apply to recursive fetches, which always disallow private addresses.
[[#608]]
Comment thread
dahlia marked this conversation as resolved.

[#606]: https://github.com/fedify-dev/fedify/issues/606
[#608]: https://github.com/fedify-dev/fedify/pull/608

### @fedify/vocab

- Fixed `Endpoints.toJsonLd()` to no longer emit invalid
Expand Down
93 changes: 80 additions & 13 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ maxRedirection = 5
[lookup]
authorizedFetch = false
firstKnock = "draft-cavage-http-signatures-12" # or "rfc9421"
allowPrivateAddress = false
traverse = false
suppressErrors = false
defaultFormat = "default" # "default", "raw", "compact", or "expand"
Expand Down Expand Up @@ -479,11 +480,6 @@ As you can see, the outputs are separated by `----` by default. You can change
the separator by using the [`-s`/`--separator`](#s-separator-output-separator)
option.

> [!NOTE]
> The `fedify lookup` command cannot take multiple argument if
> [`-t`/`--traverse`](#t-traverse-traverse-the-collection) option is turned
> on.

### `-t`/`--traverse`: Traverse the collection

*This option is available since Fedify 0.14.0.*
Expand All @@ -500,25 +496,81 @@ The difference between with and without the `-t`/`--traverse` option is that
the former will output the objects in the collection, while the latter will
output the collection object itself.

This option only works with a single argument, and it has to be a collection.
When this option is enabled, each argument has to resolve to a collection.

### `--recurse`: Recurse through object relationships

*This option is available since Fedify 2.1.0.*

The `--recurse` option is used to recursively follow an object relationship.
This is useful when you want to walk a reply chain through `replyTarget`, or
follow quote relationships through `quoteUrl`.

~~~~ sh
fedify lookup --recurse=replyTarget https://hollo.social/@fedify/019c8522-b247-79d3-b0e7-c6a2293bb1cf
~~~~

You can also provide the fully qualified property IRI:

~~~~ sh
fedify lookup --recurse=https://www.w3.org/ns/activitystreams#inReplyTo https://hollo.social/@fedify/019c8522-b247-79d3-b0e7-c6a2293bb1cf
~~~~

For quote relationships, both the short form and the full IRI are accepted:

~~~~ sh
fedify lookup --recurse=quoteUrl https://hollo.social/@fedify/019c8522-b247-79d3-b0e7-c6a2293bb1cf
fedify lookup --recurse=https://www.w3.org/ns/activitystreams#quoteUrl https://hollo.social/@fedify/019c8522-b247-79d3-b0e7-c6a2293bb1cf
~~~~

For short names, only Fedify property naming is accepted. For example,
`replyTarget` and `quoteUrl` are accepted, while `inReplyTo`, `_misskey_quote`,
and `quoteUri` are not accepted as short forms.

> [!NOTE]
> `--recurse` and [`-t`/`--traverse`](#t-traverse-traverse-the-collection)
> are mutually exclusive.
>
> Recursive fetches always disallow private/localhost addresses for safety.
> `-p`/`--allow-private-address` only applies to explicit lookup/traverse
> targets, not to recursive steps.

### `--recurse-depth`: Set recursion depth limit

*This option is available since Fedify 2.1.0.*

The `--recurse-depth` option sets the maximum recursion depth when using
[`--recurse`](#recurse-recurse-through-object-relationships). By default, it
is set to `20`.

~~~~ sh
fedify lookup --recurse=replyTarget --recurse-depth=10 https://hollo.social/@fedify/019c8522-b247-79d3-b0e7-c6a2293bb1cf
~~~~

### `-S`/`--suppress-errors`: Suppress partial errors during traversal
This option depends on the `--recurse` option.

### `-S`/`--suppress-errors`: Suppress partial errors during traversal or recursion

*This option is available since Fedify 0.14.0.*

The `-S`/`--suppress-errors` option is used to suppress partial errors during
traversal. For example, the below command looks up a collection object with
the `-t`/`--traverse` option:
traversal or recursion.

For traversal mode:

~~~~ sh
fedify lookup --traverse --suppress-errors https://fosstodon.org/users/hongminhee/outbox
~~~~

The difference between with and without the `-S`/`--suppress-errors` option is
that the former will suppress the partial errors during traversal, while the
latter will stop the traversal when an error occurs.
For recursion mode:

This option depends on the `-t`/`--traverse` option.
~~~~ sh
fedify lookup --recurse=replyTarget --suppress-errors https://hollo.social/@fedify/019c8522-b247-79d3-b0e7-c6a2293bb1cf
~~~~

The difference between with and without the `-S`/`--suppress-errors` option is
that the former will suppress the partial errors during traversal or recursion,
while the latter will stop on the first such error.

### `-c`/`--compact`: Compact JSON-LD

Expand Down Expand Up @@ -936,6 +988,21 @@ below command:
fedify lookup --user-agent MyApp/1.0 @fedify@hollo.social
~~~~

### `-p`/`--allow-private-address`: Allow private IP addresses

By default, `fedify lookup` does not fetch private or localhost addresses.
The `-p`/`--allow-private-address` option allows explicit lookup/traverse
requests to private addresses when needed for local development.

~~~~ sh
fedify lookup --allow-private-address http://localhost:8000/users/alice
~~~~

> [!NOTE]
> Recursive fetches enabled by
> [`--recurse`](#recurse-recurse-through-object-relationships) continue to
> disallow private addresses.

### `-s`/`--separator`: Output separator

*This option is available since Fedify 1.3.0.*
Expand Down
53 changes: 43 additions & 10 deletions packages/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import { parse as parseToml } from "smol-toml";
import {
array,
boolean,
check,
forward,
type InferOutput,
integer,
minValue,
number,
object,
optional,
picklist,
pipe,
string,
} from "valibot";

Expand All @@ -25,17 +30,45 @@ const webfingerSchema = object({
/**
* Schema for the lookup command configuration.
*/
const lookupSchema = object({
authorizedFetch: optional(boolean()),
firstKnock: optional(
picklist(["draft-cavage-http-signatures-12", "rfc9421"]),
const lookupSchema = pipe(
object({
authorizedFetch: optional(boolean()),
firstKnock: optional(
picklist(["draft-cavage-http-signatures-12", "rfc9421"]),
),
allowPrivateAddress: optional(boolean()),
traverse: optional(boolean()),
recurse: optional(
picklist([
"replyTarget",
"quoteUrl",
"https://www.w3.org/ns/activitystreams#inReplyTo",
"https://www.w3.org/ns/activitystreams#quoteUrl",
"https://misskey-hub.net/ns#_misskey_quote",
"http://fedibird.com/ns#quoteUri",
]),
),
recurseDepth: optional(pipe(number(), integer(), minValue(1))),
suppressErrors: optional(boolean()),
defaultFormat: optional(picklist(["default", "raw", "compact", "expand"])),
separator: optional(string()),
timeout: optional(number()),
}),
forward(
check(
(input) => !(input.traverse === true && input.recurse != null),
"lookup.traverse and lookup.recurse cannot be used together.",
),
["recurse"],
),
traverse: optional(boolean()),
suppressErrors: optional(boolean()),
defaultFormat: optional(picklist(["default", "raw", "compact", "expand"])),
separator: optional(string()),
timeout: optional(number()),
});
forward(
check(
(input) => input.recurse != null || input.recurseDepth == null,
"lookup.recurseDepth requires lookup.recurse.",
),
["recurseDepth"],
),
);

/**
* Schema for the inbox command configuration.
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/docloader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import test from "node:test";
import { getDocumentLoaderCachePrefix } from "./docloader.ts";

test("getDocumentLoaderCachePrefix - isolates strict and permissive policies", () => {
const strictPrefix = getDocumentLoaderCachePrefix("fedify-cli", false);
const permissivePrefix = getDocumentLoaderCachePrefix("fedify-cli", true);
assert.notDeepEqual(strictPrefix, permissivePrefix);
});

test("getDocumentLoaderCachePrefix - includes user agent namespace", () => {
const prefixA = getDocumentLoaderCachePrefix("agent-a", false);
const prefixB = getDocumentLoaderCachePrefix("agent-b", false);
assert.notDeepEqual(prefixA, prefixB);
});
28 changes: 24 additions & 4 deletions packages/cli/src/docloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,35 @@ const documentLoaders: Record<string, DocumentLoader> = {};

export interface DocumentLoaderOptions {
userAgent?: string;
allowPrivateAddress?: boolean;
}

/**
* Returns a cache prefix that separates document-loader entries by user agent
* and private-address policy.
*/
export function getDocumentLoaderCachePrefix(
Comment thread
dahlia marked this conversation as resolved.
userAgent: string | undefined,
allowPrivateAddress: boolean,
): readonly [string, ...string[]] {
return [
"_fedify",
"remoteDocument",
"cli",
userAgent ?? "",
allowPrivateAddress ? "allow-private" : "deny-private",
];
}

export async function getDocumentLoader(
{ userAgent }: DocumentLoaderOptions = {},
{ userAgent, allowPrivateAddress = false }: DocumentLoaderOptions = {},
): Promise<DocumentLoader> {
Comment thread
dahlia marked this conversation as resolved.
if (documentLoaders[userAgent ?? ""]) return documentLoaders[userAgent ?? ""];
const cacheKey = `${userAgent ?? ""}:${allowPrivateAddress}`;
if (documentLoaders[cacheKey]) return documentLoaders[cacheKey];
const kv = await getKvStore();
return documentLoaders[userAgent ?? ""] = kvCache({
return documentLoaders[cacheKey] = kvCache({
Comment thread
dahlia marked this conversation as resolved.
kv,
prefix: getDocumentLoaderCachePrefix(userAgent, allowPrivateAddress),
rules: [
[
new URLPattern({
Expand Down Expand Up @@ -54,7 +74,7 @@ export async function getDocumentLoader(
],
],
loader: getDefaultDocumentLoader({
allowPrivateAddress: true,
allowPrivateAddress,
userAgent,
}),
Comment thread
dahlia marked this conversation as resolved.
});
Expand Down
Loading
Loading