fix(engine): downloads refuse hosts that resolve to private addresses (#6571)

* fix(engine): downloads refuse hosts that resolve to private addresses

* fix(core): shorten the env proxy doc comment

* test(producer): remote-media tests stub the download fetch seam
This commit is contained in:
Miguel Ángel
2026-10-28 20:08:50 +00:00
committed by GitHub
parent 3ca1005a81
commit 9830059bdb
22 changed files with 349 additions and 183 deletions
+2
View File
@@ -437,6 +437,8 @@
// as a direct dep to guarantee installation even when transitive
// resolution fails (corrupted cache, dedup edge cases).
"debug",
// The engine loads it at runtime; tsup keeps it external in the CLI bundle.
"undici",
// @puppeteer/browsers' optional peer: its zip fallback when no system `unzip` exists.
"yauzl",
// Loaded through the optional package loader for Gemini vision captioning.
+2
View File
@@ -841,6 +841,8 @@ jobs:
- name: Install dependencies
if: runner.os == 'Windows'
run: bun install --frozen-lockfile --ignore-scripts --linker=hoisted
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
- run: bun run --cwd packages/core build
- run: bun run --cwd packages/cli test src/utils/npxCommand.test.ts src/commands/skills.test.ts
sdk-tests:
+2
View File
@@ -69,6 +69,7 @@
"hyperframes-localize-fonts": "./bin/hyperframes-localize-fonts.mjs",
},
"dependencies": {
"undici": "7.25.0",
"@hono/node-server": "^2.0.5",
"@puppeteer/browsers": "^3.2.2",
"acorn": "^8.17.0",
@@ -165,6 +166,7 @@
"name": "@hyperframes/engine",
"version": "0.8.210",
"dependencies": {
"undici": "7.25.0",
"@hono/node-server": "^2.0.5",
"@hyperframes/core": "workspace:^",
"@hyperframes/parsers": "workspace:^",
+1
View File
@@ -70,6 +70,7 @@
"open": "^10.0.0",
"postcss": "^8.5.8",
"puppeteer-core": "^25.10.0",
"undici": "7.25.0",
"yauzl": "^3.4.0"
},
"devDependencies": {
+1 -6
View File
@@ -1,13 +1,8 @@
import { promises as dns } from "node:dns";
import { fetchGoesThroughEnvProxy } from "@hyperframes/core/env-proxy";
const PROBE_TIMEOUT_MS = 1000;
function fetchGoesThroughEnvProxy(): boolean {
if (!(process.env["HTTPS_PROXY"] ?? process.env["https_proxy"])?.trim()) return false;
const flags = [...process.execArgv, ...(process.env["NODE_OPTIONS"]?.split(/\s+/u) ?? [])];
return process.env["NODE_USE_ENV_PROXY"] === "1" || flags.includes("--use-env-proxy");
}
/**
* Whether DNS answers for `host` via c-ares, one short try per nameserver. The system lookup
* `fetch` makes cannot be aborted and holds even `process.exit`, so background requests ask this first.
+2 -5
View File
@@ -1,3 +1,4 @@
import { envProxyEnabled } from "@hyperframes/core/env-proxy";
import { basename, dirname, join, posix, relative, resolve } from "node:path";
import {
closeSync,
@@ -235,11 +236,7 @@ function proxySupportHint(): string {
const proxyConfigured = ["HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy"].some((key) =>
Boolean(process.env[key]?.trim()),
);
const proxyEnabled =
process.env["NODE_USE_ENV_PROXY"] === "1" ||
process.execArgv.includes("--use-env-proxy") ||
process.env["NODE_OPTIONS"]?.split(/\s+/u).includes("--use-env-proxy") === true;
if (!proxyConfigured || proxyEnabled) return "";
if (!proxyConfigured || envProxyEnabled()) return "";
return (
". Proxy variables are set but ignored by Node fetch; if this network requires them, retry with " +
"NODE_USE_ENV_PROXY=1 (Node 22.21+)"
+1
View File
@@ -44,6 +44,7 @@ var __dirname = __hf_dirname(__filename);`,
},
external: [
"puppeteer-core",
"undici",
"puppeteer",
"@puppeteer/browsers",
// Native module — its platform binary (@img/sharp-<os>-<arch>) must be
+6
View File
@@ -74,6 +74,12 @@
"types": "./dist/removeTree.d.ts",
"environments": ["bun", "node"]
},
"./env-proxy": {
"source": "./src/envProxy.ts",
"runtime": "./dist/envProxy.js",
"types": "./dist/envProxy.d.ts",
"environments": ["bun", "node"]
},
"./rename-retry": {
"source": "./src/renameWithRetry.ts",
"runtime": "./dist/renameWithRetry.js",
+10
View File
@@ -86,6 +86,12 @@
"import": "./src/removeTree.ts",
"types": "./src/removeTree.ts"
},
"./env-proxy": {
"bun": "./src/envProxy.ts",
"node": "./dist/envProxy.js",
"import": "./src/envProxy.ts",
"types": "./src/envProxy.ts"
},
"./rename-retry": {
"bun": "./src/renameWithRetry.ts",
"node": "./dist/renameWithRetry.js",
@@ -660,6 +666,10 @@
"import": "./dist/removeTree.js",
"types": "./dist/removeTree.d.ts"
},
"./env-proxy": {
"import": "./dist/envProxy.js",
"types": "./dist/envProxy.d.ts"
},
"./rename-retry": {
"import": "./dist/renameWithRetry.js",
"types": "./dist/renameWithRetry.d.ts"
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { envProxyEnabled, fetchGoesThroughEnvProxy } from "./envProxy.js";
describe("env proxy", () => {
it("needs a proxy variable and Node started with env-proxy support", () => {
const proxy = { HTTPS_PROXY: "http://proxy.test:3128" };
expect(fetchGoesThroughEnvProxy({ ...proxy, NODE_USE_ENV_PROXY: "1" }, [])).toBe(true);
expect(fetchGoesThroughEnvProxy(proxy, ["--use-env-proxy"])).toBe(true);
expect(
fetchGoesThroughEnvProxy({ ...proxy, NODE_OPTIONS: "--no-warnings --use-env-proxy" }, []),
).toBe(true);
expect(
fetchGoesThroughEnvProxy({ https_proxy: " http://proxy.test ", NODE_USE_ENV_PROXY: "1" }, []),
).toBe(true);
expect(fetchGoesThroughEnvProxy(proxy, [])).toBe(false);
expect(fetchGoesThroughEnvProxy({ NODE_USE_ENV_PROXY: "1" }, [])).toBe(false);
expect(fetchGoesThroughEnvProxy({ HTTPS_PROXY: " ", NODE_USE_ENV_PROXY: "1" }, [])).toBe(false);
});
it("reports env-proxy support separately from the variables", () => {
expect(envProxyEnabled({ NODE_USE_ENV_PROXY: "1" }, [])).toBe(true);
expect(envProxyEnabled({}, [])).toBe(false);
});
});
+18
View File
@@ -0,0 +1,18 @@
/** Whether `fetch` follows the proxy variables (`NODE_USE_ENV_PROXY=1` or `--use-env-proxy`). */
export function envProxyEnabled(
env: NodeJS.ProcessEnv = process.env,
execArgv: readonly string[] = process.execArgv,
): boolean {
const flags = [...execArgv, ...(env["NODE_OPTIONS"]?.split(/\s+/u) ?? [])];
return env["NODE_USE_ENV_PROXY"] === "1" || flags.includes("--use-env-proxy");
}
/** Whether `fetch` reaches the network through `HTTPS_PROXY`, which then does the name resolution. */
export function fetchGoesThroughEnvProxy(
env: NodeJS.ProcessEnv = process.env,
execArgv: readonly string[] = process.execArgv,
): boolean {
return (
Boolean((env["HTTPS_PROXY"] ?? env["https_proxy"])?.trim()) && envProxyEnabled(env, execArgv)
);
}
+2 -1
View File
@@ -100,7 +100,8 @@
"hono": "^4.6.0",
"linkedom": "^0.18.12",
"puppeteer": "~25.11.0",
"puppeteer-core": "^25.11.0"
"puppeteer-core": "^25.11.0",
"undici": "7.25.0"
},
"devDependencies": {
"@types/node": "^25.0.10",
+1
View File
@@ -379,6 +379,7 @@ export {
} from "./utils/notMediaPayload.js";
export {
_setDownloadFetchForTests,
assertPublicHttpsUrl,
isBlockedNetworkHost,
downloadToTemp,
@@ -83,7 +83,11 @@ vi.mock("../utils/ffprobe.js", async (importOriginal) => {
});
import { RATE_RANGE } from "@hyperframes/core/audio-automation";
import { _setDownloadFetchForTests } from "../utils/urlDownloader.js";
import { AudioFxRenderError } from "./audioFxRender.js";
const stubFetch = (replacement: unknown): void =>
_setDownloadFetchForTests(replacement as Parameters<typeof _setDownloadFetchForTests>[0]);
import { MAX_MIX_INPUTS, parseAudioElements, processCompositionAudio } from "./audioMixer.js";
import { encodeAac } from "./aacEncode.js";
@@ -156,6 +160,7 @@ describe("processCompositionAudio", () => {
afterEach(() => {
vi.unstubAllGlobals();
_setDownloadFetchForTests();
runFfmpegMock.mockReset();
extractAudioMetadataMock.mockReset();
extractAudioMetadataMock.mockResolvedValue({
@@ -230,7 +235,7 @@ describe("processCompositionAudio", () => {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response("<!doctype html><html><body>denied</body></html>"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const result = await processCompositionAudio(
[
@@ -1914,7 +1919,7 @@ describe("processCompositionAudio", () => {
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
tempDirs.push(baseDir, workDir);
const fetchMock = vi.fn().mockRejectedValue(new Error("downloaded"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const url = "https://bucket.s3.amazonaws.com/cas/take.mov?X-Amz-Signature=s";
const result = await processCompositionAudio(
@@ -2,7 +2,14 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { safeDownloadUrlIdentity, UrlDownloadError } from "../utils/urlDownloader.js";
import {
_setDownloadFetchForTests,
safeDownloadUrlIdentity,
UrlDownloadError,
} from "../utils/urlDownloader.js";
const stubFetch = (replacement: unknown): void =>
_setDownloadFetchForTests(replacement as Parameters<typeof _setDownloadFetchForTests>[0]);
import {
classifyFfmpegSpawnError,
classifyVideoExtractionError,
@@ -13,6 +20,7 @@ const tempDirs: string[] = [];
afterEach(() => {
vi.unstubAllGlobals();
_setDownloadFetchForTests();
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
@@ -62,8 +70,7 @@ describe("extractAllVideoFrames download failure metadata", () => {
it("threads a query-free fingerprint, allowlisted host, and zero used retries", async () => {
const source =
"https://media.customer-cdn.example/private/clip.mp4?X-Amz-Signature=super-secret#fragment";
vi.stubGlobal(
"fetch",
stubFetch(
vi.fn().mockResolvedValue(new Response("<!doctype html><html><body>denied</body></html>")),
);
@@ -97,7 +104,7 @@ describe("extractAllVideoFrames download failure metadata", () => {
it("collapses an exhausted HTTP retry to status class and normalized host", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 503 }));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const result = await extractAllVideoFrames(
[
@@ -2,7 +2,10 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { downloadToTemp } from "./urlDownloader.js";
import { _setDownloadFetchForTests, downloadToTemp } from "./urlDownloader.js";
const stubFetch = (replacement: unknown): void =>
_setDownloadFetchForTests(replacement as Parameters<typeof _setDownloadFetchForTests>[0]);
const dirs: string[] = [];
@@ -12,7 +15,7 @@ beforeEach(() => {
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
_setDownloadFetchForTests();
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
@@ -77,7 +80,7 @@ const fetchCalls = (fetchMock: { mock: { calls: unknown[] } }, count: number) =>
/** Starts a download whose every attempt gets a fresh `response()`, once the first fetch is made. */
async function startDownload(response: () => Response) {
const fetchMock = vi.fn(async () => response());
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const outcome: { path?: string; error?: unknown } = {};
void downloadToTemp("https://cdn.example/media.mp4", tempDir()).then(
(path) => (outcome.path = path),
@@ -0,0 +1,68 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
const answers = vi.hoisted(() => ({ list: [] as Array<{ address: string; family: number }> }));
vi.mock("node:dns", () => {
const lookup = (
_host: string,
_options: unknown,
callback: (error: null, addresses: typeof answers.list) => void,
) => callback(null, answers.list);
return { default: { lookup }, lookup };
});
import { downloadToTemp, fetchPublicHttpsText, UrlDownloadError } from "./urlDownloader.js";
const dirs: string[] = [];
afterEach(() => {
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
describe("downloads refuse hosts that resolve to private addresses", () => {
it.each([
["a private IPv4 answer", [{ address: "10.1.2.3", family: 4 }]],
[
"a link-local answer among public ones",
[
{ address: "93.184.216.34", family: 4 },
{ address: "169.254.169.254", family: 4 },
],
],
["a loopback IPv6 answer", [{ address: "::1", family: 6 }]],
])("downloadToTemp rejects %s without retrying", async (_, list) => {
answers.list = list;
const dir = mkdtempSync(join(tmpdir(), "hf-public-address-"));
dirs.push(dir);
const error = await downloadToTemp("https://assets.example.test/a.png", dir).catch((e) => e);
expect(error).toBeInstanceOf(UrlDownloadError);
expect(error).toMatchObject({ kind: "http_rejected", retryable: false });
});
it("fetchPublicHttpsText rejects the same way", async () => {
answers.list = [{ address: "192.168.0.7", family: 4 }];
await expect(
fetchPublicHttpsText("https://assets.example.test/x.html", { maxBytes: 1024 }),
).rejects.toMatchObject({
kind: "http_rejected",
});
});
it("keeps Node's own fetch when it goes through an env proxy, which resolves the name", async () => {
answers.list = [{ address: "10.1.2.3", family: 4 }];
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:3128");
vi.stubEnv("NODE_USE_ENV_PROXY", "1");
const viaProxy = vi.fn(async () => new Response("<html></html>"));
vi.stubGlobal("fetch", viaProxy);
try {
await expect(
fetchPublicHttpsText("https://assets.example.test/x.html", { maxBytes: 1024 }),
).resolves.toBe("<html></html>");
expect(viaProxy).toHaveBeenCalledOnce();
} finally {
vi.unstubAllGlobals();
vi.unstubAllEnvs();
}
});
});
+66 -62
View File
@@ -15,12 +15,16 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { createHash } from "node:crypto";
import {
_setDownloadFetchForTests,
assertPublicHttpsUrl,
downloadToTemp,
fetchPublicHttpsText,
UrlDownloadError,
} from "./urlDownloader.js";
const stubFetch = (replacement: unknown): void =>
_setDownloadFetchForTests(replacement as Parameters<typeof _setDownloadFetchForTests>[0]);
const fsRaceControls = vi.hoisted(() => ({
deleteBeforeLstatPath: undefined as string | undefined,
deleteInjectedWinnerBeforeLstatPath: undefined as string | undefined,
@@ -148,6 +152,7 @@ afterEach(() => {
heldDeadlines.clear();
vi.useRealTimers();
vi.unstubAllGlobals();
_setDownloadFetchForTests();
fsRaceControls.deleteBeforeLstatPath = undefined;
fsRaceControls.deleteInjectedWinnerBeforeLstatPath = undefined;
fsRaceControls.injectRaceAtLinkPath = undefined;
@@ -280,7 +285,7 @@ describe("fetchPublicHttpsText", () => {
headers: { location: "https://169.254.169.254/latest/meta-data/" },
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
await expect(
fetchPublicHttpsText("https://styles.example/fonts.css", { maxBytes: 1024 }),
@@ -299,7 +304,7 @@ describe("fetchPublicHttpsText", () => {
cancelled = true;
},
});
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(body)));
stubFetch(vi.fn().mockResolvedValue(new Response(body)));
await expect(
fetchPublicHttpsText("https://styles.example/large.css", { maxBytes: 10 }),
@@ -321,7 +326,7 @@ describe("fetchPublicHttpsText", () => {
});
return new Response(body);
});
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
await expect(
fetchPublicHttpsText("https://styles.example/stalled.css", {
@@ -334,8 +339,7 @@ describe("fetchPublicHttpsText", () => {
describe("downloadToTemp atomic publication and bounded retry", () => {
it("does not race a real wall-clock deadline on a slow attempt", async () => {
vi.stubGlobal(
"fetch",
stubFetch(
vi.fn().mockImplementation(
(_url: string, init: RequestInit) =>
new Promise<Response>((resolve, reject) => {
@@ -360,7 +364,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
}),
)
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp("https://cdn.example/redirect.mp4", dir, 1_000);
@@ -384,7 +388,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
headers: { location: "http://169.254.169.254/latest/meta-data/" },
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -404,7 +408,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
headers: { location: "https://[::ffff:169.254.169.254]/latest/meta-data/" },
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -421,7 +425,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.fn()
.mockResolvedValueOnce(new Response(null, { status: 503, statusText: "Service Unavailable" }))
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const onTransientRetry = vi.fn();
@@ -448,7 +452,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(null, { status: 503, statusText: signedUrl }));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
let message = "";
@@ -476,7 +480,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.mockResolvedValueOnce(
new Response(complete, { headers: { "content-length": String(complete.length) } }),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const onTransientRetry = vi.fn();
@@ -503,7 +507,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.mockImplementation(() =>
Promise.resolve(new Response("short", { headers: { "content-length": "12" } })),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -528,7 +532,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
}),
)
.mockResolvedValueOnce(new Response("complete", { headers: { "content-length": "8" } }));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp("https://cdn.example/unsolicited-range.mp4", dir, 1_000);
@@ -545,7 +549,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
headers: { "content-range": "bytes nonsense" },
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -567,7 +571,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
headers: { "content-range": "bytes 0-7/8", "content-length": "8" },
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp(
@@ -593,7 +597,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
headers: { "content-range": "bytes 0-7/8" },
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -614,7 +618,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
}),
),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -646,7 +650,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
},
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -667,7 +671,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.fn()
.mockResolvedValueOnce(new Response(corrupt))
.mockResolvedValueOnce(new Response(complete));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp(
@@ -685,7 +689,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
it("rejects a malformed caller checksum before fetching", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -710,7 +714,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
),
);
const onTransientRetry = vi.fn();
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -742,7 +746,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
},
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp(
@@ -769,7 +773,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
},
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp("https://cdn.example/server-sha.mp4", dir, 1_000);
@@ -784,7 +788,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(bytes, { headers: { "content-md5": contentMd5 } }));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp("https://cdn.example/legacy-md5.mp4", dir, 1_000);
@@ -802,7 +806,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
}),
),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -820,7 +824,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
}),
),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -839,7 +843,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
},
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp(
@@ -859,7 +863,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(" <!doctype html><html><body>denied</body></html>"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -876,7 +880,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
it("rejects a BOM-prefixed remote error document without retrying", async () => {
const payload = Buffer.from("\uFEFF <!doctype html><html><body>denied</body></html>");
const fetchMock = vi.fn().mockResolvedValue(new Response(payload));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -894,7 +898,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
writeFileSync(cachePath, "<!doctype html><html>expired</html>");
const fontBytes = Buffer.from("wOF2valid-font-payload");
const fetchMock = vi.fn().mockResolvedValue(new Response(fontBytes));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const path = await downloadToTemp(url, dir, 1_000);
@@ -910,7 +914,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
headers: { "content-type": "video/mp4" },
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -940,7 +944,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.mockResolvedValueOnce(
new Response(mediaBytes, { headers: { "content-type": "text/html" } }),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp(
@@ -964,7 +968,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
headers: { "content-length": String(mediaBytes.length), etag },
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const events: unknown[] = [];
const signedUrl =
@@ -1004,7 +1008,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
"does not reject valid %s inputs via a duplicate format allowlist",
async (kind, mediaBytes) => {
const fetchMock = vi.fn().mockResolvedValue(new Response(mediaBytes));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp(`https://cdn.example/extensionless-${kind}`, dir, 1_000);
@@ -1020,7 +1024,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
const fetchMock = vi
.fn()
.mockRejectedValue(new TypeError(`fetch failed while requesting ${signedUrl}`));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
let message = "";
@@ -1043,7 +1047,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.mockRejectedValue(
Object.assign(new Error(`no space left writing ${signedUrl}`), { code: "ENOSPC" }),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
await expect(downloadToTemp(signedUrl, makeTempDir(), 1_000)).rejects.toMatchObject({
kind: "filesystem",
@@ -1055,7 +1059,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
const signedUrl = "https://cdn.example/private/clip.mp4?X-Amz-Signature=super-secret-signature";
const hostile = Object.assign(new Error("boom"), { code: signedUrl });
hostile.name = signedUrl;
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(hostile));
stubFetch(vi.fn().mockRejectedValue(hostile));
await expect(downloadToTemp(signedUrl, makeTempDir(), 1_000)).rejects.toMatchObject({
message: "Download failed while writing the local artifact",
@@ -1078,7 +1082,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
new Response(errorBody, { status: 503, statusText: "Service Unavailable" }),
)
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp("https://cdn.example/streaming-503.mp4", dir, 1_000);
@@ -1093,7 +1097,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(null, { status: 404, statusText: "Not Found" }));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -1111,7 +1115,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(null, { status: 503, statusText: "Service Unavailable" }));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
await expect(
@@ -1137,7 +1141,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.fn()
.mockResolvedValueOnce(new Response(resetBody))
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp("https://cdn.example/reset-once.mp4", dir, 1_000);
@@ -1161,7 +1165,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.fn()
.mockResolvedValueOnce(new Response(disconnectedBody))
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp("https://cdn.example/undici-reset-once.mp4", dir, 1_000);
@@ -1188,7 +1192,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
return new Response(stalledBody);
})
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const pending = downloadToTemp("https://cdn.example/stalled-body.mp4", dir, 1_000);
@@ -1215,7 +1219,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
return new Response(stalledBody);
})
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const pending = downloadToTemp("https://cdn.example/unresponsive-body.mp4", dir, 1_000);
@@ -1243,7 +1247,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
return new Response(truncatedBody);
})
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const pending = downloadToTemp("https://cdn.example/aborted-chunked-body.mp4", dir, 1_000);
@@ -1261,7 +1265,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.fn()
.mockResolvedValueOnce(new Response(""))
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp("https://cdn.example/empty-once.mp4", dir, 1_000);
@@ -1276,7 +1280,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
.fn()
.mockResolvedValueOnce(new Response(null))
.mockResolvedValueOnce(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const path = await downloadToTemp("https://cdn.example/null-body-once.mp4", dir, 1_000);
@@ -1288,7 +1292,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
it("removes a stale zero-byte final file before downloading", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const stalePath = join(dir, "download_eda0de5dc5a3.mp4");
writeFileSync(stalePath, "");
@@ -1309,7 +1313,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
writeFileSync(cachePath, "vanishing-cache-entry");
fsRaceControls.deleteBeforeLstatPath = cachePath;
const fetchMock = vi.fn().mockResolvedValue(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const path = await downloadToTemp(url, dir, 1_000);
@@ -1321,7 +1325,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
it("does not trust a nonempty symlink at the final cache path", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response("downloaded"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const target = join(dir, "attacker-controlled.mp4");
const cachePath = join(dir, "download_eda0de5dc5a3.mp4");
@@ -1338,7 +1342,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
it("does not retry caller cancellation", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const controller = new AbortController();
controller.abort();
@@ -1360,7 +1364,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
setTimeout(() => resolve(new Response("complete")), 10);
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const [first, second] = await Promise.all([
@@ -1380,7 +1384,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
const lockPath = join(dir, `${cacheName}.hf-lock`);
mkdirSync(lockPath);
const fetchMock = vi.fn().mockResolvedValue(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const pending = downloadToTemp(url, dir, 1_000);
await new Promise((resolve) => setTimeout(resolve, 25));
@@ -1403,7 +1407,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
const staleTime = new Date(Date.now() - 6 * 60_000);
utimesSync(lockPath, staleTime, staleTime);
const fetchMock = vi.fn().mockResolvedValue(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const path = await downloadToTemp(url, dir, 1_000);
@@ -1424,7 +1428,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
utimesSync(lockPath, staleTime, staleTime);
fsRaceControls.replaceStaleLockAfterObservationPath = lockPath;
const fetchMock = vi.fn().mockResolvedValue(new Response("complete"));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const pending = downloadToTemp(url, dir, 1_000);
await new Promise((resolve) => setTimeout(resolve, 25));
@@ -1448,7 +1452,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
writeFileSync(cachePath, "cached");
fsRaceControls.replaceLockOnReleasePath = lockPath;
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const path = await downloadToTemp(url, dir, 1_000);
@@ -1472,7 +1476,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
setTimeout(() => resolve(new Response(body)), 10);
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const [first, second] = await Promise.all([
downloadToTemp(url, dir, 1_000, firstController.signal),
@@ -1504,7 +1508,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
setTimeout(() => resolve(new Response(secondBytes)), 5),
),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const url = "https://cdn.example/versioned.mp4";
@@ -1532,7 +1536,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
fsRaceControls.deleteBeforeReadPath = cachePath;
const onTelemetry = vi.fn();
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(new Response("complete")));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const path = await downloadToTemp(url, dir, 1_000, undefined, undefined, { onTelemetry });
@@ -1557,7 +1561,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
fsRaceControls.deleteInjectedWinnerBeforeLstatPath = cachePath;
const onTelemetry = vi.fn();
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(new Response("complete")));
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const path = await downloadToTemp(url, dir, 1_000, undefined, undefined, { onTelemetry });
@@ -1597,7 +1601,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
setTimeout(() => resolve(new Response("complete")), 10);
}),
);
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const first = downloadToTemp(
@@ -1651,7 +1655,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
});
return new Response(body);
});
vi.stubGlobal("fetch", fetchMock);
stubFetch(fetchMock);
const dir = makeTempDir();
const first = downloadToTemp(
+64 -2
View File
@@ -17,7 +17,9 @@ import {
unlinkSync,
} from "fs";
import { createHash, randomUUID } from "crypto";
import { BlockList, isIP } from "node:net";
import dns from "node:dns";
import { fetchGoesThroughEnvProxy } from "@hyperframes/core/env-proxy";
import { BlockList, isIP, type LookupFunction } from "node:net";
import { dirname, extname, join } from "path";
import { Readable, Transform } from "stream";
import { pipeline } from "stream/promises";
@@ -467,6 +469,57 @@ async function acquireCachePathLock(
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
const MAX_REDIRECTS = 5;
const NON_PUBLIC_ADDRESS_CODE = "HF_NON_PUBLIC_ADDRESS";
/** Resolves a host and refuses to connect when any answer is a non-public address. */
const publicOnlyLookup: LookupFunction = (hostname, options, callback) => {
dns.lookup(hostname, { ...options, all: true }, (error, answers) => {
if (error) return callback(error, "", 0);
if (
answers.length === 0 ||
answers.some((a) => isBlockedNetworkHost(a.address.split("%")[0]!))
) {
const refused = Object.assign(new Error("host resolves to a non-public address"), {
code: NON_PUBLIC_ADDRESS_CODE,
});
return callback(refused, "", 0);
}
if (options.all) return callback(null, answers as never);
return callback(null, answers[0]!.address, answers[0]!.family);
});
};
type DownloadFetch = (url: string, init: RequestInit) => Promise<Response>;
let undiciTransport: Promise<DownloadFetch> | undefined;
const fetchPublic: DownloadFetch = async (url, init) => {
// Behind an env proxy the proxy resolves the name; the URL and redirect checks are the guard then.
if (fetchGoesThroughEnvProxy()) return fetch(url, init);
undiciTransport ??= import("undici").then(({ Agent, fetch: undiciFetch }) => {
const dispatcher = new Agent({ connect: { lookup: publicOnlyLookup } });
return (target, options) =>
undiciFetch(target, { ...(options as object), dispatcher }) as unknown as Promise<Response>;
});
return (await undiciTransport)(url, init);
};
let downloadFetch: DownloadFetch = fetchPublic;
/** Test seam: replaces the transport every download goes through; no argument restores it. */
export function _setDownloadFetchForTests(replacement?: DownloadFetch): void {
downloadFetch = replacement ?? fetchPublic;
}
function refusedNonPublicAddress(error: unknown): boolean {
for (let depth = 0; error && depth < 4; depth += 1) {
if (typeof error === "object" && "code" in error && error.code === NON_PUBLIC_ADDRESS_CODE) {
return true;
}
error = typeof error === "object" && "cause" in error ? error.cause : undefined;
}
return false;
}
function assertAllowedDownloadUrl(url: string, redirect: boolean): void {
try {
assertPublicHttpsUrl(url);
@@ -517,10 +570,19 @@ async function fetchWithValidatedRedirects(
// lgtm[js/file-access-to-http] — every redirect hop is fetched manually
// only after the HTTPS/private-host guard above; automatic redirect
// following is disabled so an allowed host cannot bounce into IMDS.
const response = await fetch(currentUrl, {
const response = await downloadFetch(currentUrl, {
signal: controller.signal,
redirect: "manual",
headers: { "accept-encoding": "identity" },
}).catch((error: unknown) => {
if (!refusedNonPublicAddress(error)) throw error;
throw new UrlDownloadError(
"http_rejected",
false,
redirects > 0
? "Download redirect target is not permitted"
: "Download URL is not permitted",
);
});
if (!REDIRECT_STATUSES.has(response.status)) return { response, finalUrl: currentUrl };
+3 -3
View File
@@ -3,6 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { _setDownloadFetchForTests } from "@hyperframes/engine";
import { parseRenderOptions, prepareRenderBody } from "./server.js";
describe("parseRenderOptions — variables", () => {
@@ -191,14 +192,13 @@ describe("prepareRenderBody — validation", () => {
});
describe("prepareRenderBody — previewUrl", () => {
const realFetch = globalThis.fetch;
const stubFetch = (impl: () => Promise<Response>) => {
const spy = mock(impl);
globalThis.fetch = spy as unknown as typeof fetch;
_setDownloadFetchForTests(spy as unknown as Parameters<typeof _setDownloadFetchForTests>[0]);
return spy;
};
afterEach(() => {
globalThis.fetch = realFetch;
_setDownloadFetchForTests();
});
it.each([
@@ -30,6 +30,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import {
applyConcreteGpuScreenshotClamp,
_setDownloadFetchForTests,
buildChromeArgs,
FFMPEG_PATH_ENV,
getFfmpegBinary,
@@ -707,12 +708,11 @@ describe("plan() — golden planDir + planHash determinism", () => {
<video id="hero" src="https://cdn.example/missing.mp4" data-start="0"></video>
</div>`,
);
const originalFetch = globalThis.fetch;
let fetchCalls = 0;
globalThis.fetch = (async () => {
_setDownloadFetchForTests((async () => {
fetchCalls += 1;
return new Response(null, { status: 404, statusText: "Not Found" });
}) as typeof fetch;
}) as never);
let caught: unknown;
try {
@@ -724,7 +724,7 @@ describe("plan() — golden planDir + planHash determinism", () => {
} catch (err) {
caught = err;
} finally {
globalThis.fetch = originalFetch;
_setDownloadFetchForTests();
}
expect(fetchCalls).toBeGreaterThan(0);
@@ -9,7 +9,7 @@ import { parseHTML } from "linkedom";
import { interpolateVolumeGain } from "@hyperframes/core/media-volume-envelope";
import { redactTelemetryString } from "@hyperframes/core";
import { defaultLogger } from "../logger.js";
import { NotMediaPayloadError } from "@hyperframes/engine";
import { NotMediaPayloadError, _setDownloadFetchForTests } from "@hyperframes/engine";
import {
collectExternalAssets,
compileForRender,
@@ -1945,11 +1945,16 @@ describe("crossorigin attribute stripping", () => {
// avoid invoking ffprobe / the full compileForRender pipeline. fetch is patched
// in-process for success cases; the unit lane's refused network covers fallback.
// Downloads go through the engine's fetch seam, not the global fetch.
const stubDownloadFetch = {
set fetch(impl: unknown) {
_setDownloadFetchForTests(impl as never);
},
};
describe("localizeRemoteMediaSources", () => {
it("rewrites remote <video> src to _remote_media path when download succeeds", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => validTestMediaResponse();
stubDownloadFetch.fetch = async () => validTestMediaResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-dl-ok-"));
const html = `<video id="v1" src="https://media-ok.example.com/a/clip.mp4" data-start="0" data-end="10" muted></video>`;
@@ -1958,8 +1963,7 @@ describe("localizeRemoteMediaSources", () => {
expect(result).toContain("_remote_media/");
expect(remoteMediaAssets.size).toBe(1);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
_setDownloadFetchForTests();
}
});
@@ -1973,11 +1977,9 @@ describe("localizeRemoteMediaSources", () => {
});
it("logs only a safe fingerprint and host for a signed-URL media failure", async () => {
const originalFetch = globalThis.fetch;
const originalWarn = defaultLogger.warn;
const warnings: unknown[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () =>
stubDownloadFetch.fetch = async () =>
new Response("<!doctype html><html><body>expired</body></html>", { status: 200 });
defaultLogger.warn = (message, meta) => warnings.push({ message, meta });
try {
@@ -1995,17 +1997,14 @@ describe("localizeRemoteMediaSources", () => {
expect(serialized).not.toContain("customer.mp4");
expect(serialized).not.toContain("super-secret-signature");
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = originalFetch;
_setDownloadFetchForTests();
defaultLogger.warn = originalWarn;
}
});
it("deduplicates: two tags with the same src URL → one download", async () => {
const orig = globalThis.fetch;
let fetchCount = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => {
stubDownloadFetch.fetch = async () => {
fetchCount++;
return validTestMediaResponse();
};
@@ -2016,8 +2015,7 @@ describe("localizeRemoteMediaSources", () => {
await localizeRemoteMediaSources(html, dl);
expect(fetchCount).toBe(1);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
_setDownloadFetchForTests();
}
});
@@ -2031,9 +2029,7 @@ describe("localizeRemoteMediaSources", () => {
});
it("localizes remote <source> children of a <video>", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => validTestMediaResponse();
stubDownloadFetch.fetch = async () => validTestMediaResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-dl-src-"));
const html = `<video id="rec" data-start="0" data-end="5" muted>
@@ -2044,15 +2040,12 @@ describe("localizeRemoteMediaSources", () => {
expect(result).not.toContain("https://src-ok.example.com/");
expect(remoteMediaAssets.size).toBe(2);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
_setDownloadFetchForTests();
}
});
it("rewrites src in both double-quoted and single-quoted attributes", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => validTestMediaResponse();
stubDownloadFetch.fetch = async () => validTestMediaResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-dl-quotes-"));
const html = `<video id="v1" src="https://q.example.com/c/dq.mp4" data-start="0" data-end="10" muted></video>
@@ -2061,8 +2054,7 @@ describe("localizeRemoteMediaSources", () => {
expect(result).not.toContain("https://q.example.com/");
expect(result.match(/_remote_media\//g)?.length).toBe(2);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
_setDownloadFetchForTests();
}
});
@@ -2091,9 +2083,7 @@ describe("localizeRemoteMediaSources", () => {
describe("localizeRemoteImageSources", () => {
it("rewrites remote <img> src to _remote_media path when download succeeds", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => validTestImageResponse();
stubDownloadFetch.fetch = async () => validTestImageResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-img-ok-"));
const html = `<img class="hero" src="https://img-ok.example.com/photo.png" />`;
@@ -2102,8 +2092,7 @@ describe("localizeRemoteImageSources", () => {
expect(result).toContain("_remote_media/");
expect(remoteMediaAssets.size).toBe(1);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
_setDownloadFetchForTests();
}
});
@@ -2117,10 +2106,8 @@ describe("localizeRemoteImageSources", () => {
});
it("deduplicates: two <img> tags with the same src URL → one download", async () => {
const orig = globalThis.fetch;
let fetchCount = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => {
stubDownloadFetch.fetch = async () => {
fetchCount++;
return validTestImageResponse();
};
@@ -2131,8 +2118,7 @@ describe("localizeRemoteImageSources", () => {
await localizeRemoteImageSources(html, dl);
expect(fetchCount).toBe(1);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
_setDownloadFetchForTests();
}
});
@@ -2154,9 +2140,7 @@ describe("localizeRemoteImageSources", () => {
});
it("rewrites both double-quoted and single-quoted src attributes", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => validTestImageResponse();
stubDownloadFetch.fetch = async () => validTestImageResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-img-quotes-"));
const html = `<img src="https://q-img.example.com/dq.png" />
@@ -2165,8 +2149,7 @@ describe("localizeRemoteImageSources", () => {
expect(result).not.toContain("https://q-img.example.com/");
expect(result.match(/_remote_media\//g)?.length).toBe(2);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
_setDownloadFetchForTests();
}
});
@@ -2175,10 +2158,8 @@ describe("localizeRemoteImageSources", () => {
// `src`. We must localise what Chrome actually paints (the real `src`),
// not the `data-src` URL — matching `data-src` would download an asset the
// render never shows and could break the loader's runtime swap.
const orig = globalThis.fetch;
let fetchCount = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => {
stubDownloadFetch.fetch = async () => {
fetchCount++;
return validTestImageResponse();
};
@@ -2191,17 +2172,14 @@ describe("localizeRemoteImageSources", () => {
expect(result).not.toContain("https://cdn.example.com/placeholder.png");
expect(fetchCount).toBe(1);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
_setDownloadFetchForTests();
}
});
it("handles src attribute not as the first attribute (agent-pipeline shape)", async () => {
// The 02_kobe astral-pipeline composition that surfaced this bug emits
// <img> tags with `class` before `src`. Regex must not assume src position.
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => validTestImageResponse();
stubDownloadFetch.fetch = async () => validTestImageResponse();
try {
const dl = mkdtempSync(join(tmpdir(), "hf-img-attr-order-"));
const html = `<img class="kobe-cutout" alt="kobe" src="https://astral.example.com/d828bca.png" />`;
@@ -2210,8 +2188,7 @@ describe("localizeRemoteImageSources", () => {
expect(result).toContain("_remote_media/");
expect(remoteMediaAssets.size).toBe(1);
} finally {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = orig;
_setDownloadFetchForTests();
}
});
});
@@ -2222,9 +2199,7 @@ describe("localizeRemoteFontFaces", () => {
const FONT_URL = "https://gen-os-static.s3.us-east-2.amazonaws.com/fonts/komika-axis.ttf";
it("rewrites @font-face url() inside <style> to _remote_media/ path", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => new Response(new Uint8Array(16), { status: 200 });
stubDownloadFetch.fetch = async () => new Response(new Uint8Array(16), { status: 200 });
try {
const dl = mkdtempSync(join(tmpdir(), "hf-ff-"));
const html = `<style>
@@ -2238,14 +2213,12 @@ describe("localizeRemoteFontFaces", () => {
expect(result).toContain("_remote_media/");
expect(remoteMediaAssets.size).toBe(1);
} finally {
globalThis.fetch = orig;
_setDownloadFetchForTests();
}
});
it("ignores url() references outside @font-face (e.g. background-image)", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => new Response(new Uint8Array(16), { status: 200 });
stubDownloadFetch.fetch = async () => new Response(new Uint8Array(16), { status: 200 });
try {
const dl = mkdtempSync(join(tmpdir(), "hf-ff-bg-"));
const BG_URL = "https://cdn.example.com/bg.png";
@@ -2258,14 +2231,12 @@ body { background-image: url("${BG_URL}"); }
expect(result).not.toContain(FONT_URL);
expect(result).toContain(BG_URL);
} finally {
globalThis.fetch = orig;
_setDownloadFetchForTests();
}
});
it("preserves original URL when download fails", async () => {
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => new Response(null, { status: 403 });
stubDownloadFetch.fetch = async () => new Response(null, { status: 403 });
try {
const dl = mkdtempSync(join(tmpdir(), "hf-ff-fail-"));
const FAIL_URL = "https://fail-font.example.com/f.ttf";
@@ -2274,15 +2245,13 @@ body { background-image: url("${BG_URL}"); }
expect(result).toContain(FAIL_URL);
expect(remoteMediaAssets.size).toBe(0);
} finally {
globalThis.fetch = orig;
_setDownloadFetchForTests();
}
});
it("deduplicates: same font URL in two @font-face blocks → 1 download", async () => {
const orig = globalThis.fetch;
let fetchCount = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => {
stubDownloadFetch.fetch = async () => {
fetchCount++;
return new Response(new Uint8Array(16), { status: 200 });
};
@@ -2297,7 +2266,7 @@ body { background-image: url("${BG_URL}"); }
expect(fetchCount).toBe(1);
expect(remoteMediaAssets.size).toBe(1);
} finally {
globalThis.fetch = orig;
_setDownloadFetchForTests();
}
});
@@ -2355,9 +2324,7 @@ body { margin: 0; }
}
h1 { font-size: 2rem; }`;
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async (url: string) => {
stubDownloadFetch.fetch = async (url: string) => {
if (url === STYLESHEET_URL) {
return new Response(fakeCss, { status: 200 });
}
@@ -2380,15 +2347,13 @@ h1 { font-size: 2rem; }`;
expect(result).toContain("_remote_media/");
expect(remoteMediaAssets.size).toBe(1);
} finally {
globalThis.fetch = orig;
_setDownloadFetchForTests();
}
});
it("keeps <link> tag when external stylesheet fetch fails (graceful degradation)", async () => {
const STYLESHEET_URL = "https://down.example.com/fonts.css";
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => new Response(null, { status: 503 });
stubDownloadFetch.fetch = async () => new Response(null, { status: 503 });
try {
const dl = mkdtempSync(join(tmpdir(), "hf-ff-extfail-"));
const html = `<link rel="stylesheet" href="${STYLESHEET_URL}">`;
@@ -2398,16 +2363,14 @@ h1 { font-size: 2rem; }`;
expect(result).toContain("<link");
expect(remoteMediaAssets.size).toBe(0);
} finally {
globalThis.fetch = orig;
_setDownloadFetchForTests();
}
});
it("rejects a stylesheet redirect to a private host before the second request", async () => {
const STYLESHEET_URL = "https://styles.example.com/fonts.css";
const orig = globalThis.fetch;
let fetchCount = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () => {
stubDownloadFetch.fetch = async () => {
fetchCount++;
return new Response(null, {
status: 302,
@@ -2423,18 +2386,16 @@ h1 { font-size: 2rem; }`;
expect(result).toBe(html);
expect(remoteMediaAssets.size).toBe(0);
} finally {
globalThis.fetch = orig;
_setDownloadFetchForTests();
}
});
it("does not log a signed stylesheet path or query on failure", async () => {
const STYLESHEET_URL =
"https://styles.example.com/private/customer.css?X-Amz-Signature=super-secret";
const originalFetch = globalThis.fetch;
const originalWarn = defaultLogger.warn;
const warnings: unknown[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () =>
stubDownloadFetch.fetch = async () =>
new Response(null, { status: 503, statusText: STYLESHEET_URL });
defaultLogger.warn = (message, meta) => warnings.push({ message, meta });
try {
@@ -2448,16 +2409,14 @@ h1 { font-size: 2rem; }`;
expect(serialized).not.toContain("customer.css");
expect(serialized).not.toContain("super-secret");
} finally {
globalThis.fetch = originalFetch;
_setDownloadFetchForTests();
defaultLogger.warn = originalWarn;
}
});
it("keeps <link> tag when external stylesheet has no @font-face rules", async () => {
const STYLESHEET_URL = "https://cdn.example.com/reset.css";
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async () =>
stubDownloadFetch.fetch = async () =>
new Response("body { margin: 0; } h1 { color: blue; }", { status: 200 });
try {
const dl = mkdtempSync(join(tmpdir(), "hf-ff-noff-"));
@@ -2468,7 +2427,7 @@ h1 { font-size: 2rem; }`;
expect(result).toContain("<link");
expect(remoteMediaAssets.size).toBe(0);
} finally {
globalThis.fetch = orig;
_setDownloadFetchForTests();
}
});
@@ -2477,9 +2436,7 @@ h1 { font-size: 2rem; }`;
const FONT_FILE_URL = "https://use.typekit.net/af/font.woff2";
const fontCss = `@font-face { font-family: "AdobeFont"; src: url("${FONT_FILE_URL}") format("woff2"); }`;
const orig = globalThis.fetch;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = async (url: string) => {
stubDownloadFetch.fetch = async (url: string) => {
if (url === FONT_CDN_URL) return new Response(fontCss, { status: 200 });
return new Response(new Uint8Array(16), { status: 200 });
};
@@ -2496,7 +2453,7 @@ h1 { font-size: 2rem; }`;
expect(result).toContain("_remote_media/");
expect(remoteMediaAssets.size).toBe(1);
} finally {
globalThis.fetch = orig;
_setDownloadFetchForTests();
}
});
});