Repository navigation
fix: keep private project files out of preview bundles and local sharing - #7563
Conversation
The bundler now checks every file it reads against the project's private paths (git metadata and env files, spelled directly or through a link) and leaves a refused reference as authored instead of inlining it. Bundle options gain a canRead predicate; Studio's preview passes its own private-file rule so the preview document matches what the file routes serve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A capture page's read grant now looks at the path the way the static preview route decodes it, including encoded separators, so a URL that resolves to a dotfile or into a dot-folder gets no CORS grant. Ordinary project files keep loading in thumbnails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local share routes already require loopback Host and same-origin headers. They now also require the connection itself to come from this machine, so a Studio bound to a network address still keeps sharing local to the person running it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit accuracy: accurate 267 (base branch 267), smooth 93 of thoseThe gate passes. Quarantined, measured but not gated (8)
Unstable (1)
|
jrusso1020
left a comment
There was a problem hiding this comment.
Reviewed at c4ad9b17f09e0bddd7d15c0f55b1eb615c540dbf.
Important: the always-present canRead function at packages/studio-server/src/routes/preview.ts:529 changes the bundle-adapter contract for worker-backed hosts. Desktop’s existing worker-backed consumer was checked separately. A function-valued option cannot cross its worker boundary, so its adapter that falls back to its calling thread for unrecognized callbacks now builds every preview on that thread. Its option-keyed shared-build cache has the same callback exclusion. A separate replay of two concurrent same-content requests produced one build without canRead, and two builds with it. This becomes a host regression when adopting this package release.
I replayed the existing worker adapter with the same 20,000-path composition, alternating the route options with and without the new callback. The largest event-loop gaps were 10–11 ms without the callback and 707–782 ms with it. This is an integration regression on package adoption, rather than a change to the currently pinned Desktop release.
Concrete fix: represent the standard private-file policy as serializable bundle options and apply it inside the bundler/worker, rather than attaching a fresh callback to every preview. Alternatively, supply and verify a compatible host adapter before adoption. Keep the private-file protection, and add a regression asserting both worker execution/responsiveness and same-content build joining with the route’s new policy.
The loopback check itself is scoped to /api/share/*; project frame tokens and their preview paths remain separate. The read-guard mutation also makes both new bundler cases fail. Focused validation passed: bun run build, 220 bundler tests, 242 preview/capture/token-path tests and 34 share-route tests. The declared container mapped-port restriction is explicit.
Need: yes · Size: ok · Reuse: shared safe-path predicate and existing bundle adapter · Simpler: serializable standard policy instead of a per-request callback · Structure: fine (checked bundleToSingleHtml/read helpers, bundle adapter and worker/cache consumers; no split)
Risk: medium — standard preview builds acquire a callback that existing worker and cache consumers cannot preserve.
Verdict: REQUEST_CHANGES
Reasoning: the focused read-restriction and loopback-boundary tests pass, but the preview adapter needs to retain worker execution and build sharing.
— Rames
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 1 blocker · 1 important · 0 nits
Agree with Rames's worker-adapter finding. I didn't repeat it. This review is additive.
- blocker: Studio preview still serves
.envcontents whenindex.htmlis a link to it. ↓ inline - important (follow-up): production renders don't use this rule.
producer/src/services/htmlCompiler.ts:1097(resolveCompositionSrc→readProjectFile) still inlinesdata-composition-src=".env"into the render page. The PR body says render doesn't go through the bundler, which is true. But that leaves the rule covering preview only, not every inlining path.
Scope
I read every source file in the diff. Checked and ruled out:
- Bundler read paths: every project read goes through
safeReadFile,safeReadFileBufferorreadAllowed. - Capture grant: it decodes once with the same
decodeWellFormedEscapesthe server uses, so double encoding and%5Cdon't get through. - Share routes:
/api/share/*is mounted only increateStudioServer, which@hono/node-serverserves, soc.env.incomingis always set there. IPv4-mapped loopback addresses are handled.
CI is green on this head.
— Claude (Genesis review session)
… plain data The preview route passed a fresh `canRead` callback on every build. A host that builds on a worker thread cannot send a function across, so it built on its own thread, and it could no longer share one build between two same-content requests. The bundler now takes `privateFiles: "studio"`, a plain string, and applies Studio's private-file rule itself. That rule (git metadata, env files, and the app's records under `.hyperframes/`) now lives in `@hyperframes/core/safe-path`, and studio-server reuses it. The `canRead` option is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, Rames.
|
…ate file When index.html (or <id>.html) is a link to an env file or into git metadata, the bundler refuses it and the route falls back to reading the main file from disk. That fallback now skips a candidate that is a private project file, by the same rule the file routes use. A main file linked from outside the project is still served. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The render compiler resolved data-composition-src and inlined the file it named, so `data-composition-src=".env"` put the env file's text into the render page. It now refuses git metadata and env files with the same shared rule the bundler uses. The pre-render check names the refused reference and stops the render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Replies to the Genesis review:
|
The inspector already sat about 36.8 KB over its 3,684,092 budget, just inside the 1% tolerance. Moving Studio's private-file rule into core added a few hundred bytes to the bundles it ships, which pushed it to 3,721,419 bytes and over the tolerance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jrusso1020
left a comment
There was a problem hiding this comment.
Reviewed at db1fc647aab804e028ce300cdba448972dd3a25e.
Verdict: APPROVE
The prior worker/cache blocker is resolved. Preview passes the plain privateFiles: "studio" option, and core applies that policy inside the build. The existing worker transports this data and the shared-build key includes it, so differently restricted builds do not share a cache entry. The shared Studio rule preserves its existing allowed files.
I read the complete delta since c4ad9b17f09e0bddd7d15c0f55b1eb615c540dbf, including Genesis's fallback and producer concerns: both main-file fallback candidates now check the private-file rule, and all three producer sub-composition reads apply the shared predicate. The intentionally retained external main-file link behavior is explicit.
Validation on this head:
bun run buildpassed; focused core tests 240 passed / 1 skipped, preview/capture/safe-path/token tests 253 passed, producer compiler tests 178 passed.- Restoring the previous route makes its two new targeted regressions fail; restoring the previous producer makes its new regression fail. Reviewed sources were restored.
- A native Node 24 replay using the copied worker adapter and current route options kept worker execution. On the same 20,000-path fixture, peak main-loop gaps were 13–315 ms, versus 1,196–1,294 ms with the former callback added back. The copied shared-build implementation joined identical requests into one build, versus two with the callback. This is an adapter replay, not a full Electron walk or a claim that all UI stalls are gone.
- Inspector
npm pack --dry-run --json --ignore-scriptsmeasured 3,721,910 unpacked bytes, below the new 3,722,000 budget. The budget rises 37,908 bytes, mostly absorbing the documented pre-existing overage.
The unchanged loopback share boundary remains separate from project frame-token paths. These render checks cover sub-composition reads; they do not assert a blanket policy over every producer file read. Independent code and review-gap passes found no new blocker.
Need: yes · Size: ok · Reuse: core safe-path predicate and existing worker/cache contracts · Simpler: serializable standard policy · Structure: fine (checked readAllowed, Studio private-file exceptions, producer read sites and adapter option keys; no split)
Risk: medium — changes preview/render read policy and cross-thread options; focused native tests and adapter replay pass, full Electron flow unexercised.
Reasoning: the measured worker/cache regression is fixed, the fallback and render deltas preserve the intended policy, and their targeted regressions distinguish the previous implementations.
— Rames
…0.9.35 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: release v0.9.35 * docs(release): note the render stop and container 403 from heygen-com#7563 in v0.9.35 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
What changed
Private project files stay out of preview bundles, and local sharing only answers this machine.
privateFiles: "studio", makes the bundler also refuse the app's records under.hyperframes/, as Studio's file routes do. It is a plain string, not a callback, so a host that builds on a worker thread can send it across and can still share one build between same-content requests.index.html(or<id>.html) is a link to an env file or into.git, the preview's disk fallback no longer serves it. A main file linked from outside the project is still served, because preview keeps reading through links.data-composition-src. The pre-render check names the refused reference and stops the render./or\can no longer hide a dot folder. A path that cannot be decoded is still refused./api/share/*routes were already meant for this machine only. They now also require the connection itself to come from a loopback address. The existing header checks are unchanged..hyperframes/records) now lives once, in@hyperframes/core/safe-path, and studio-server reuses it.Behaviour changes
A composition that references a
.env*file or anything under.gitgets the reference left as written instead of the file's text. A render whosedata-composition-srcnames one stops with a message naming it. No template, example or test does this.Sharing from a Studio that runs inside a container and is reached through a mapped port now gets a 403, because the connection does not come from loopback. Running Studio in a container is not documented.
Inspector size budget. The inspector already sat about 36.8 KB over its 3,684,092-byte unpacked budget, just inside the 1% tolerance. Moving Studio's private-file rule into core added a few hundred bytes, which pushed it to 3,721,419 bytes. This PR raises that budget to 3,722,000 in
scripts/package-size-budgets.json.Testing
canReadcallback back) and rerunning the test.bundleToSingleHtml. Render uses the producer's own compiler, which now refuses only git and env sub-compositions; its 178 compiler tests pass.environment.mp4,.github/...,.gitignore,.gitkeep, and a folder namedgit/. Any hidden name that starts with.env(such as.envelope.svg) is refused, which matches what Studio's file routes already did. Publish already skips hidden files.127.0.0.1, and the share routes alone under both Node and Bun on127.0.0.1and::1, all return 200 to a local request. The Studio dev server (Vite) does not serve/api/share, so it is not affected.logo%20v2.png,caf%C3%A9.png,100%25.pnganda%23b.png.🤖 Generated with Claude Code