Skip to content

fix(catalog): 3D previews load their scripts under the docs host's script policy - #5090

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/catalog-previews-csp
Oct 5, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
fix/catalog-previews-csp

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

What

The four 3D-motion catalog previews that loaded their libraries through blob: script URLs (Code Slice Hero, Cuboid Carousel, Orbit Card, Frost Sequence Camera Orbit) now run every script as an inline <script>. The docs site's policy allows that, so the live player renders the composition instead of its empty base.

Why

The docs host sends script-src 'self' 'unsafe-inline' 'unsafe-eval' https: and no blob:. scripts/catalog-script-inlining.ts fetched GSAP and the three.js modules from /public/catalog/vendor/*.json, wrapped them in URL.createObjectURL(new Blob(...)) and loaded those as scripts and as import-map targets. Chrome refuses every one of them ("Loading the script 'blob:…' violates the following Content Security Policy directive"), the bootstrap's promise rejects with a bare Event, GSAP never defines window.gsap, and the composition stays on its empty base: a white player for Code Slice Hero on both the gallery and its own detail page. Reproduced in Chrome 154 on the deployed /catalog/blocks/code-slice-hero.

Related work

The same policy also refuses fetch() of a data: URL (connect-src), which Glass Shard Title uses for its HDR environment. That is a separate item and a separate fix; it is not in this PR. A failed-state UI for a preview whose scripts fail is also a follow-up.

How

  • Classic scripts (GSAP, frost.js, Code Slice Hero's shadows.js/surface.js, and each composition script) run through one bootstrap per item. It fetches each vendor JSON and appends the text as an inline <script> with a //# sourceURL=hyperframes-catalog://<item>/<name>, in order, so an error keeps a real source. This matches the separate <script> tags of the original composition more closely than the old indirect eval. A failed fetch is logged against the item instead of rejecting silently.
  • ES modules cannot run inline without a URL for their imports, so they are bundled to classic scripts with esbuild at generate time:
    • three.js, its three.core import and the two addons go into one shared vendor/three-modules.json. It defines globalThis.__hfCatalogModules keyed by import specifier, and the four raw three.js vendor files it replaces are removed.
    • Each item's entry module and its own modules (cuboid-motion.js, orbit-scene.js, orbit-motion.js) become one classic script per item. Their three.js imports stay external and resolve through that shared bundle via the require esbuild emits for an external import.
    • No import map and no module script are left.
  • esbuild (already in the tree through tsx and the package builds) is declared at the root, because the generator now imports it.
  • The four preview payloads and Glass Shard Title (which shares the string encoder) are regenerated with generate-catalog-payloads.ts --only <item>, so the fix ships when this merges, without waiting for the standing publish PR. The deployed payloads match main's committed files byte for byte, so the docs site serves docs/ from main. The next "Publish catalog" run regenerates the same files.

Test plan

  • Unit tests added/updated, in scripts/catalog-script-inlining.test.ts:

    • each of the four inlined blocks loads no script from a blob URL;
    • no committed blocks/ or components/ payload does either.

    All five fail on main, the generator tests and the committed payloads respectively, and pass here: 14/14, 3 runs. The import-map test is replaced by one that checks each 3D item takes only three.js specifiers from the shared bundle and keeps no import map or module script.

  • Script-string encoding regression: closing tags, mixed-case tags, HTML comment markers, and Unicode line separators stay data and round-trip unchanged. The test fails before the fix; all 15 inlining tests pass in three serial runs. Chrome 154 also executes the generated script and parses the following element.

  • The four superseded vendor payloads are listed explicitly in the existing deletion guard. Its 8 tests pass.

  • Manual testing performed: Chrome 154, each payload mounted in a srcdoc iframe under a page that sends the docs host's exact CSP.

    • Main: all four fail with the blob CSP refusal and register no timeline.
    • This branch: all four register their timeline with no CSP violation. Code Slice Hero paints its headline, and Cuboid Carousel paints (187 WebGL draws).
    • Without the CSP, Orbit Card and Frost behave the same on main and this branch: headless Chrome has no WebGPU for Frost, and Orbit's frame reads flat in this harness on both.
  • generate-catalog-payloads.test.ts, check-docs-catalog, tsc for scripts, format, lint, Fallow (no new findings from these files), the comment ratchet, comment citations and tracked artifacts pass.

  • Comments follow CONTRIBUTING.md "Comments".

Before

The live site, Chrome 154 with a mouse (the gallery mounts live players only for a hover-capable pointer), under the deployed CSP. On the gallery, the hovered Code Slice Hero card plays as a blank white player; its detail page plays blank white too.

before gallery

before detail

After

The same live pages and CSP, with only the /public/catalog/blocks/* and /public/catalog/vendor/* requests answered from this branch's files: the card plays its headline, the detail page plays to its rear headline, and no CSP refusal is logged.

after gallery

after detail

@mintlify

mintlify Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 5, 2026, 10:42 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

const code = outputFiles[0]?.text;
if (!code) throw new Error("catalog-script-inlining: esbuild produced no output.");
if (external.length === 0) return `"use strict";${code}`;
const shared = `(s)=>globalThis.${SHARED_MODULES_GLOBAL}[${JSON.stringify(SHARED_ALIASES)}[s]??s]`;
if (!code) throw new Error("catalog-script-inlining: esbuild produced no output.");
if (external.length === 0) return `"use strict";${code}`;
const shared = `(s)=>globalThis.${SHARED_MODULES_GLOBAL}[${JSON.stringify(SHARED_ALIASES)}[s]??s]`;
return `(function(require){"use strict";${code}})(${shared});`;
@miguel-heygen
miguel-heygen marked this pull request as ready for review October 5, 2026 22:51
@miguel-heygen
miguel-heygen merged commit f835f83 into main Oct 5, 2026
92 checks passed
@miguel-heygen
miguel-heygen deleted the fix/catalog-previews-csp branch October 5, 2026 22:52
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

Edit accuracy: accurate 2055 (base branch 2055), smooth 1582 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (2)

  • crop-none-px-r0-nested-z100: tracking 0.04, pressJump 0, drop 40.07, reload 40.07, render 40.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.03, renderKey -, undo true, teleport true
  • crop-none-px-r0-nested-z200: tracking 0.02, pressJump 0, drop 40.03, reload 40.05, render 40.03, renderKey -, undo true, teleport true / tracking 0.02, pressJump 0, drop 0.04, reload 0.06, render 0.03, renderKey -, undo true, teleport true / tracking 0.02, pressJump 0, drop 0.04, reload 0.06, render 0.03, renderKey -, undo true, teleport true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants