Skip to content

Commit 0154b40

Browse files
fix(captions): derive preview GSAP integrity (#3631)
Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com> Co-authored-by: miguel.sierra <229591595+miguel-heygen@users.noreply.github.com>
1 parent a3954aa commit 0154b40

3 files changed

Lines changed: 61 additions & 12 deletions

File tree

skills-manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"source": "heygen-com/hyperframes",
33
"skills": {
44
"embedded-captions": {
5-
"hash": "ab099eb22321e865",
6-
"files": 138
5+
"hash": "84d986fddfc717a1",
6+
"files": 139
77
},
88
"faceless-explainer": {
99
"hash": "a7080101c30ceeb7",

skills/embedded-captions/scripts/preview-frames.cjs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@
2323
const path = require("path");
2424
const fs = require("fs");
2525
const os = require("os");
26+
const crypto = require("crypto");
27+
const { pathToFileURL } = require("url");
28+
29+
function sriSha384(source) {
30+
return `sha384-${crypto.createHash("sha384").update(source).digest("base64")}`;
31+
}
32+
33+
function withPreviewGsapSri(html, gsapSource) {
34+
const integrity = sriSha384(gsapSource);
35+
return html.replace(/<script\b(?=[^>]*\bsrc=["'][^"']*gsap[^"']*["'])[^>]*>/gi, (tag) =>
36+
tag
37+
.replace(/\s+integrity\s*=\s*(?:"[^"]*"|'[^']*')/gi, "")
38+
.replace(/>$/, ` integrity="${integrity}">`),
39+
);
40+
}
2641

2742
const HF_ROOTS = [
2843
process.env.HYPERFRAMES_ROOT,
@@ -63,10 +78,10 @@ for (const r of HF_ROOTS) {
6378
}
6479
if (!gsapSource) {
6580
const g = findInBun(r, "gsap", path.join("dist", "gsap.min.js"));
66-
if (g) gsapSource = fs.readFileSync(g, "utf8");
81+
if (g) gsapSource = fs.readFileSync(g);
6782
}
6883
}
69-
if (!puppeteer || !sharp) {
84+
if (require.main === module && (!puppeteer || !sharp)) {
7085
console.error("[preview] need puppeteer+sharp — set HYPERFRAMES_ROOT");
7186
process.exit(0);
7287
}
@@ -80,17 +95,23 @@ async function shotAt(browser, file, W, H, t) {
8095
// evaluation runs while document.head is still null, and gsap's init then
8196
// throws "appendChild of null" — which killed previews for theme projects.
8297
await page.setRequestInterception(true);
98+
const documentUrl = pathToFileURL(file).href;
99+
const documentHtml = gsapSource
100+
? withPreviewGsapSri(fs.readFileSync(file, "utf8"), gsapSource)
101+
: fs.readFileSync(file, "utf8");
83102
page.on("request", (req) => {
84103
const u = req.url();
85-
if (req.resourceType() === "script" && /gsap/i.test(u) && /^https?:/i.test(u)) {
104+
if (req.isNavigationRequest() && u === documentUrl) {
105+
req.respond({ status: 200, contentType: "text/html", body: documentHtml });
106+
} else if (req.resourceType() === "script" && /gsap/i.test(u) && /^https?:/i.test(u)) {
86107
if (gsapSource)
87108
req.respond({ status: 200, contentType: "application/javascript", body: gsapSource });
88109
else req.continue(); // no local bundle — let the CDN load (online machines)
89110
} else if (req.resourceType() === "media")
90111
req.abort(); // a-roll pixels come from frames_bg
91112
else req.continue();
92113
});
93-
await page.goto(`file://${file}`, { waitUntil: "load", timeout: 15000 });
114+
await page.goto(documentUrl, { waitUntil: "load", timeout: 15000 });
94115
const t0 = Date.now();
95116
let tlReady = false;
96117
while (Date.now() - t0 < 15000) {
@@ -260,9 +281,13 @@ async function main() {
260281
await Promise.race([browser.close().catch(() => {}), new Promise((r) => setTimeout(r, 8000))]);
261282
}
262283
}
263-
main()
264-
.then(() => process.exit(0))
265-
.catch((e) => {
266-
console.error("[preview]", e.message);
267-
process.exit(1);
268-
});
284+
if (require.main === module) {
285+
main()
286+
.then(() => process.exit(0))
287+
.catch((e) => {
288+
console.error("[preview]", e.message);
289+
process.exit(1);
290+
});
291+
}
292+
293+
module.exports = { sriSha384, withPreviewGsapSri };
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import assert from "node:assert/strict";
2+
import crypto from "node:crypto";
3+
import { createRequire } from "node:module";
4+
import test from "node:test";
5+
6+
const require = createRequire(import.meta.url);
7+
const { sriSha384, withPreviewGsapSri } = require("./preview-frames.cjs");
8+
9+
test("preview GSAP integrity matches the intercepted asset bytes", () => {
10+
const servedAsset = Buffer.from("the exact local GSAP response body");
11+
const html = `
12+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js" integrity="sha384-stale" crossorigin="anonymous"></script>
13+
<script src="https://cdn.example.test/app.js" integrity="sha384-app" crossorigin="anonymous"></script>
14+
`;
15+
16+
const emitted = withPreviewGsapSri(html, servedAsset);
17+
const expected = `sha384-${crypto.createHash("sha384").update(servedAsset).digest("base64")}`;
18+
const gsapTag = emitted.match(/<script[^>]*gsap[^>]*>/i)[0];
19+
20+
assert.equal(sriSha384(servedAsset), expected);
21+
assert.match(gsapTag, new RegExp(`integrity="${expected}"`));
22+
assert.match(gsapTag, /crossorigin="anonymous"/);
23+
assert.match(emitted, /app\.js" integrity="sha384-app" crossorigin="anonymous"/);
24+
});

0 commit comments

Comments
 (0)