From 8a11e9776d3c65c2512c595bbde3dfc5bb194f39 Mon Sep 17 00:00:00 2001 From: "Vance Ingalls (via Via)" Date: Tue, 4 Aug 2026 22:42:36 +0000 Subject: [PATCH 1/3] feat(producer): sniff HTML payload before ffprobe in resolveMediaDuration STUDIO-5433 defense: when the downloaded media file begins with --- .../src/services/htmlCompiler.test.ts | 194 ++++++++++++++++++ .../producer/src/services/htmlCompiler.ts | 88 ++++++++ 2 files changed, 282 insertions(+) diff --git a/packages/producer/src/services/htmlCompiler.test.ts b/packages/producer/src/services/htmlCompiler.test.ts index a69be7e02b..d45fc1d23c 100644 --- a/packages/producer/src/services/htmlCompiler.test.ts +++ b/packages/producer/src/services/htmlCompiler.test.ts @@ -7,8 +7,10 @@ import { parseHTML } from "linkedom"; import { interpolateVolumeGain } from "@hyperframes/core/media-volume-envelope"; import { defaultLogger } from "../logger.js"; import { + assertNotHtmlPayload, collectExternalAssets, compileForRender, + HtmlNotVideoError, injectSdkPositionEditsRenderScript, detectAncestorBackgroundImage, detectRenderModeHints, @@ -2267,3 +2269,195 @@ describe("sub-composition variable injection (render path, #2064)", () => { expect(compiled.html).not.toMatch(/window\.__hfVariablesByComp\s*=\s*Object\.assign/); }); }); + +// ── HTML payload sniff (STUDIO-5433) ─────────────────────────────────────── +// +// Producer's `resolveMediaDuration` is a two-step pipeline (download → probe) +// that runs on every media element without an authored duration. Prior to +// this defense, an authoring bug that handed a `.html` payload through as a +// video src produced an opaque `[mov,mp4,m4a,3gp,3g2,mj2 @ ...] moov atom +// not found` from ffprobe — the `mov,mp4,…` prefix was ffprobe's default +// demuxer probe order, NOT the file's true format, so every alert routed as +// a codec/ffmpeg bug. The sniff below converts the class into a domain-typed +// `HtmlNotVideoError` naming the src so it can be alerted and routed +// correctly, independent of the (separate) authoring-side root cause fix. + +describe("assertNotHtmlPayload", () => { + it("throws HtmlNotVideoError when the file starts with ", async () => { + const dir = mkdtempSync(join(tmpdir(), "hf-html-sniff-doctype-")); + const filePath = join(dir, "nested.html"); + writeFileSync( + filePath, + "\nstreamed-preview", + ); + + let caught: unknown; + try { + // URL src (kept verbatim by redactTelemetryString apart from the query + // string) — makes the src-attribution assertion below meaningful. + await assertNotHtmlPayload(filePath, "https://cdn.example.com/streamed-preview.html"); + } catch (err) { + caught = err; + } + expect(caught).toBeInstanceOf(HtmlNotVideoError); + const err = caught as HtmlNotVideoError; + // The host of a plain URL survives redactTelemetryString; the trailing + // `.html` basename gets replaced with `[file]` by the asset-basename rule. + // The `[src=…]` framing is what matters for observability. + expect(err.message).toContain("[src=https://cdn.example.com/"); + expect(err.message).toContain("cdn.example.com"); + // Sample of the file's first bytes appears in the message (case-insensitive + // "html" comes from either `` or ``). + expect(err.message.toLowerCase()).toContain("html"); + expect(err.code).toBe("HTML_NOT_VIDEO"); + }); + + it("redacts a relative-path src through redactTelemetryString before emitting", async () => { + // A bare-relative path (`assets/nested.html`) is exactly the shape the + // producer telemetry-redaction rules collapse to `[path]`. The error + // message must go through redactTelemetryString so we neither leak the + // path structure nor drop the `[src=…]` framing. + const dir = mkdtempSync(join(tmpdir(), "hf-html-sniff-redact-")); + const filePath = join(dir, "nested.html"); + writeFileSync(filePath, ""); + + let caught: unknown; + try { + await assertNotHtmlPayload(filePath, "assets/nested.html"); + } catch (err) { + caught = err; + } + expect(caught).toBeInstanceOf(HtmlNotVideoError); + const err = caught as HtmlNotVideoError; + expect(err.message).toContain("[src=[path]]"); + expect(err.message).not.toContain("assets/nested.html"); + }); + + it("throws when the file starts with (no doctype, missing lang, upper/lower case)", async () => { + const dir = mkdtempSync(join(tmpdir(), "hf-html-sniff-htmltag-")); + const filePath = join(dir, "raw.html"); + writeFileSync(filePath, "hi"); + + await expect(assertNotHtmlPayload(filePath, "raw.html")).rejects.toThrow(HtmlNotVideoError); + }); + + it("throws when the file starts with { + const dir = mkdtempSync(join(tmpdir(), "hf-html-sniff-xml-")); + const filePath = join(dir, "asset.svg"); + writeFileSync( + filePath, + '', + ); + + await expect(assertNotHtmlPayload(filePath, "asset.svg")).rejects.toThrow(HtmlNotVideoError); + }); + + it("tolerates a UTF-8 BOM and leading whitespace before the prefix", async () => { + const dir = mkdtempSync(join(tmpdir(), "hf-html-sniff-bom-")); + const filePath = join(dir, "bom.html"); + // 0xEF 0xBB 0xBF + newline + spaces + ")]), + ); + + await expect(assertNotHtmlPayload(filePath, "bom.html")).rejects.toThrow(HtmlNotVideoError); + }); + + it("does NOT throw for a real MP4 container (ftypmp42 header)", async () => { + // A minimal MP4 file signature: `\x00\x00\x00\x18 ftypmp42 ...`. + // We only care that the sniff prefix-match ignores it — downstream + // ffprobe is what actually parses the container, and this test does + // not exercise ffprobe. + const dir = mkdtempSync(join(tmpdir(), "hf-html-sniff-mp4-")); + const filePath = join(dir, "clip.mp4"); + const mp4Header = Buffer.from([ + 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, + 0x00, 0x6d, 0x70, 0x34, 0x32, 0x69, 0x73, 0x6f, 0x6d, + ]); + writeFileSync(filePath, mp4Header); + + // Should resolve without throwing. + await assertNotHtmlPayload(filePath, "clip.mp4"); + }); + + it("does NOT throw for a WebM container (EBML header)", async () => { + const dir = mkdtempSync(join(tmpdir(), "hf-html-sniff-webm-")); + const filePath = join(dir, "clip.webm"); + // Matroska/WebM starts with EBML header: 0x1A 0x45 0xDF 0xA3 + const webmHeader = Buffer.from([0x1a, 0x45, 0xdf, 0xa3, 0x9f, 0x42, 0x86, 0x81, 0x01]); + writeFileSync(filePath, webmHeader); + + await assertNotHtmlPayload(filePath, "clip.webm"); + }); + + it("does NOT throw for an empty file (0 bytes — separate code path handles it)", async () => { + const dir = mkdtempSync(join(tmpdir(), "hf-html-sniff-empty-")); + const filePath = join(dir, "empty.bin"); + writeFileSync(filePath, ""); + + await assertNotHtmlPayload(filePath, "empty.bin"); + }); + + it("does NOT throw when the html prefix is deep inside the file, not at the start", async () => { + // Regression guard: the check must be a prefix match, not a substring + // scan. A legitimate media container that happens to contain the + // substring ` { + it("aborts with HtmlNotVideoError when a