Skip to content

Commit 46e252b

Browse files
committed
Keep recursive contexts on strict loader
Limit the `-p`/`--allow-private-address` opt-in to recursive object fetches and keep indirect JSON-LD `@context` loads on the strict loader. Add a regression test that proves recursive private contexts stay blocked even when recursive object fetches are explicitly allowed. Addresses #718 (review) Assisted-by: Codex:gpt-5.4
1 parent b5c0a70 commit 46e252b

2 files changed

Lines changed: 102 additions & 41 deletions

File tree

packages/cli/src/lookup.test.ts

Lines changed: 94 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,9 @@ function extractIdsFromRawOutput(content: string): string[] {
10641064
}
10651065

10661066
async function withRecursiveLookupServer<T>(
1067+
options: {
1068+
replyContextPath?: string;
1069+
},
10671070
callback: (server: {
10681071
rootUrl: URL;
10691072
replyUrl: URL;
@@ -1079,6 +1082,9 @@ async function withRecursiveLookupServer<T>(
10791082
const requestUrl = new URL(request.url);
10801083
const rootUrl = new URL("/notes/1", requestUrl.origin);
10811084
const replyUrl = new URL("/notes/0", requestUrl.origin);
1085+
const replyContextUrl = options.replyContextPath == null
1086+
? undefined
1087+
: new URL(options.replyContextPath, requestUrl.origin);
10821088
requestedPaths.push(requestUrl.pathname);
10831089

10841090
let body: unknown;
@@ -1092,10 +1098,25 @@ async function withRecursiveLookupServer<T>(
10921098
};
10931099
} else if (requestUrl.pathname === replyUrl.pathname) {
10941100
body = {
1095-
"@context": "https://www.w3.org/ns/activitystreams",
1101+
"@context": replyContextUrl == null
1102+
? "https://www.w3.org/ns/activitystreams"
1103+
: [
1104+
"https://www.w3.org/ns/activitystreams",
1105+
replyContextUrl.href,
1106+
],
10961107
id: replyUrl.href,
10971108
type: "Note",
10981109
content: "reply",
1110+
...(replyContextUrl == null ? {} : { fedifyTest: "value" }),
1111+
};
1112+
} else if (
1113+
replyContextUrl != null &&
1114+
requestUrl.pathname === replyContextUrl.pathname
1115+
) {
1116+
body = {
1117+
"@context": {
1118+
fedifyTest: "https://fedify.dev/ns/test#fedifyTest",
1119+
},
10991120
};
11001121
} else {
11011122
return new Response(null, { status: 404 });
@@ -1128,48 +1149,51 @@ test("runLookup - rejects recursive private targets by default", async () => {
11281149
const testFile = `${testDir}/out.jsonl`;
11291150
await mkdir(testDir, { recursive: true });
11301151
try {
1131-
await withRecursiveLookupServer(async ({ rootUrl, requestedPaths }) => {
1132-
const originalWrite = process.stderr.write;
1133-
let stderr = "";
1134-
process.stderr.write = ((
1135-
chunk: string | Uint8Array,
1136-
encodingOrCallback?: unknown,
1137-
callback?: () => void,
1138-
) => {
1139-
stderr += typeof chunk === "string"
1140-
? chunk
1141-
: Buffer.from(chunk).toString();
1142-
if (typeof encodingOrCallback === "function") {
1143-
encodingOrCallback();
1144-
} else {
1145-
callback?.();
1152+
await withRecursiveLookupServer(
1153+
{},
1154+
async ({ rootUrl, requestedPaths }) => {
1155+
const originalWrite = process.stderr.write;
1156+
let stderr = "";
1157+
process.stderr.write = ((
1158+
chunk: string | Uint8Array,
1159+
encodingOrCallback?: unknown,
1160+
callback?: () => void,
1161+
) => {
1162+
stderr += typeof chunk === "string"
1163+
? chunk
1164+
: Buffer.from(chunk).toString();
1165+
if (typeof encodingOrCallback === "function") {
1166+
encodingOrCallback();
1167+
} else {
1168+
callback?.();
1169+
}
1170+
return true;
1171+
}) as typeof process.stderr.write;
1172+
let exitCode: number | null;
1173+
try {
1174+
exitCode = await runLookupAndCaptureExitCode(
1175+
createLookupRunCommand({
1176+
urls: [rootUrl.href],
1177+
recurse: "replyTarget",
1178+
recurseDepth: 20,
1179+
allowPrivateAddress: false,
1180+
output: testFile,
1181+
}),
1182+
);
1183+
} finally {
1184+
process.stderr.write = originalWrite;
11461185
}
1147-
return true;
1148-
}) as typeof process.stderr.write;
1149-
let exitCode: number | null;
1150-
try {
1151-
exitCode = await runLookupAndCaptureExitCode(
1152-
createLookupRunCommand({
1153-
urls: [rootUrl.href],
1154-
recurse: "replyTarget",
1155-
recurseDepth: 20,
1156-
allowPrivateAddress: false,
1157-
output: testFile,
1158-
}),
1186+
assert.equal(exitCode, 1);
1187+
assert.deepEqual(requestedPaths, ["/notes/1"]);
1188+
assert.match(
1189+
stderr,
1190+
/Try with `-p`\/`--allow-private-address`/,
11591191
);
1160-
} finally {
1161-
process.stderr.write = originalWrite;
1162-
}
1163-
assert.equal(exitCode, 1);
1164-
assert.deepEqual(requestedPaths, ["/notes/1"]);
1165-
assert.match(
1166-
stderr,
1167-
/Try with `-p`\/`--allow-private-address`/,
1168-
);
11691192

1170-
const content = await readFile(testFile, "utf8");
1171-
assert.deepEqual(extractIdsFromRawOutput(content), [rootUrl.href]);
1172-
});
1193+
const content = await readFile(testFile, "utf8");
1194+
assert.deepEqual(extractIdsFromRawOutput(content), [rootUrl.href]);
1195+
},
1196+
);
11731197
} finally {
11741198
await rm(testDir, { recursive: true });
11751199
}
@@ -1181,6 +1205,7 @@ test("runLookup - allows recursive private targets with allowPrivateAddress", as
11811205
await mkdir(testDir, { recursive: true });
11821206
try {
11831207
await withRecursiveLookupServer(
1208+
{},
11841209
async ({ rootUrl, replyUrl, requestedPaths }) => {
11851210
const exitCode = await runLookupAndCaptureExitCode(
11861211
createLookupRunCommand({
@@ -1206,6 +1231,35 @@ test("runLookup - allows recursive private targets with allowPrivateAddress", as
12061231
}
12071232
});
12081233

1234+
test("runLookup - keeps recursive private contexts blocked", async () => {
1235+
const testDir = "./test_output_runlookup_recurse_private_context";
1236+
const testFile = `${testDir}/out.jsonl`;
1237+
await mkdir(testDir, { recursive: true });
1238+
try {
1239+
await withRecursiveLookupServer(
1240+
{ replyContextPath: "/contexts/reply" },
1241+
async ({ rootUrl, requestedPaths }) => {
1242+
const exitCode = await runLookupAndCaptureExitCode(
1243+
createLookupRunCommand({
1244+
urls: [rootUrl.href],
1245+
recurse: "replyTarget",
1246+
recurseDepth: 20,
1247+
allowPrivateAddress: true,
1248+
output: testFile,
1249+
}),
1250+
);
1251+
assert.equal(exitCode, 1);
1252+
assert.deepEqual(requestedPaths, ["/notes/1", "/notes/0"]);
1253+
1254+
const content = await readFile(testFile, "utf8");
1255+
assert.deepEqual(extractIdsFromRawOutput(content), [rootUrl.href]);
1256+
},
1257+
);
1258+
} finally {
1259+
await rm(testDir, { recursive: true });
1260+
}
1261+
});
1262+
12091263
test("runLookup - reverses output order in default multi-input mode", async () => {
12101264
const testDir = "./test_output_runlookup_default_reverse";
12111265
const testFile = `${testDir}/out.jsonl`;

packages/cli/src/lookup.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,14 @@ export async function runLookup(
916916
initialDocumentLoader;
917917
const recursiveLookupDocumentLoader: DocumentLoader = authLoader ??
918918
documentLoader;
919-
const recursiveContextLoader = contextLoader;
919+
const recursiveBaseContextLoader = await getContextLoader({
920+
userAgent: command.userAgent,
921+
allowPrivateAddress: false,
922+
});
923+
const recursiveContextLoader = wrapDocumentLoaderWithTimeout(
924+
recursiveBaseContextLoader,
925+
command.timeout,
926+
);
920927
let totalObjects = 0;
921928
const recurseDepth = command.recurseDepth!;
922929

0 commit comments

Comments
 (0)