Repository navigation
fix(studio): run the composition hover preview in an opaque origin - #7522
Conversation
The Compositions panel hover preview framed the composition with allow-scripts and allow-same-origin, so the composition's scripts ran in the Studio's own origin. The frame now gets only allow-scripts. The card no longer reads the frame: it plays the preview muted when the runtime posts "ready", and sizes the stage from the runtime's "timeline" message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d of commenting them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Verdict: APPROVE · 0 blocker · 1 important · 0 nits · head 7a54638
The opaque-origin frame and the bridge wiring check out. The first timeline is posted before the bridge's ready, so stage size arrives before play starts and isn't held. One gap in the PR body's Known difference list ↓ inline.
Scope
Read the full diff, runtime bridge.ts, postTimeline ordering in init.ts, mediaProxy/proxyWait, colorGrading, captionOverrides, and the /preview/comp/* route (no CORS headers anywhere). CI Test (studio), Lint and Typecheck are green on this head. I did not run a browser.
— Genesis
| ref={iframeRef} | ||
| src={previewUrl} | ||
| sandbox="allow-scripts allow-same-origin" | ||
| sandbox={OPAQUE_ORIGIN_SCRIPTS_ONLY_SANDBOX} |
There was a problem hiding this comment.
important: the Known difference list misses unplayable-codec video. It says video loads as before, but a hostile-codec clip (ProRes, or HEVC where Chrome can't decode it) is swapped to a proxy by maybeProxyProactively → makeCopy, and that waits on waitForServedProxy (core/src/runtime/proxyWait.ts:49). That is a CORS-mode fetch from this null origin, and the preview server sends no Access-Control-Allow-Origin, so it fails like the font does. After 3 failed asks (about 2 s apart) the copy is marked failed and the clip never plays in the hover preview. The same applies to .cube LUT fetches (colorGrading.ts:2029) and caption-overrides.json (captionOverrides.ts:120).
Fix: add these to Known difference and to the font follow-up's scope. The follow-up's fix (a null-origin CORS allowance or a parent-side fetch bridge) covers all of them.
The Compositions panel hover preview now runs each composition in an opaque origin (
sandbox="allow-scripts", noallow-same-origin), so a composition's own scripts no longer share the Studio's origin. The card drives the preview only over the runtime's existing postMessage bridge: it plays muted on the runtime'sreadymessage and sizes the stage from itstimelinemessage.What
CompositionsTab.tsx: the hover-preview iframe dropsallow-same-origin.readymessage the card postsset-mutedthenplay. This replaces the old loop that polledcontentWindow.__playerup to ten times.timelinemessage'scompositionWidth/compositionHeight. The old code readdata-width/data-heightfromcontentDocument. The runtime fills those fields from the same attributes, with the same 1920x1080 default.syncIframePlayback,resolveIframeDuration, the__playerwindow type and the sync timer. The pause-and-seek branch was never called, because the iframe unmounts when the pointer leaves.Why
Hardening. The hover preview executes the composition's scripts (from the project, a registry block or a script the composition loads), and nothing in it needs the Studio's origin. An opaque origin keeps those scripts away from the Studio page, its storage and its same-origin requests.
Related work
Refs the same pattern in
<hyperframes-player sandbox-origin="opaque">. The main Studio preview still frames same-origin. It needs a larger change, because the editing hooks read the frame's document directly.How
Messages are accepted only when
event.sourceis this card's iframe window andsourceis"hf-preview". The listener is installed when the hover starts, before the frame loads, so the runtime's onereadymessage is not missed.readyis posted again on every reload. Fade-in and display-scale timing are unchanged: both still key off the iframe'sload.Known difference. A document with an opaque origin makes CORS-mode requests with
Origin: null, and the local preview server sends no CORS headers. So everyfetchand CORS-mode load the runtime makes from the hover preview fails. Measured: project-hosted web fonts fall back to a system font (below). The same mechanism, from the code and not separately run, means:fetch(proxyWait.ts), and that fetch fails;.cubeLUT files (colorGrading.ts) andcaption-overrides.json(captionOverrides.ts) don't load.Classic scripts, images, and video and audio the browser can play directly load as before. The resting card thumbnail is rendered server-side and is unaffected. A follow-up will restore these loads for opaque frames, either by allowing null-origin CORS for asset requests or by having the parent fetch on the frame's behalf. One fix covers all of them.
Before
After
Both are captured in headless Chrome while hovering the
title-cardcard. The runtime reportsisPlaying: true, muted: trueat about 2.3s in each. The card looks the same: only the frame's origin changed.Test plan
Unit tests added/updated. New
CompositionsTab.hoverPreview.test.tsx:sandboxis exactlyallow-scripts, with noallow-same-originreadyfrom the frame postsset-muted(muted: true) thenplaytimelinesizes the stage, while the frame'scontentDocumentgetter throws, so any read fails the testMutation check: with the component reverted to
mainand the new tests kept, 3 of 4 fail. Puttingallow-same-originback fails the sandbox test. The removedsyncIframePlaybackunit test is replaced by the ordering assertion above.Manual testing performed, in headless Chrome 153 against the Studio dev server with a two-composition project, hovering the non-root card:
sandbox="allow-scripts", stage 640x360 from thetimelinemessage. The runtime reportsisPlaying: true, muted: true, and time advances 0 to 2.5s within 2.5s of hover. Inside the frame,self.originis"null", andwindow.parent.documentandlocalStoragethrowSecurityError.main, same project: same playback (0 to 2.4s, muted). The frame's script sees the Studio origin and can readwindow.parent.documentandlocalStorage.@font-faceserved from the project loads onmainand fails withNetworkErroron this branch (the known difference above).vitest runonsrc/components/sidebar/,src/player/lib/,pointerTargetSizeanduseRenderClipContent: 39 files, 412 tests pass.tsc --noEmit, oxlint, oxfmt, fallow and the studio copy check are clean.Documentation updated (not applicable)
Comments say why, not what
Not exercised: a headed browser or the packaged CLI
hyperframes preview(the dev server uses the samestudio-serverroutes), Safari and Firefox, and a composition that uses module scripts or runtime-fetched nested compositions (same CORS mechanism as the font, not separately run).— Rames Jusso
🤖 Generated with Claude Code