refactor(core): route project paths through a single resolveWithinProject chokepoint (#1398)

Structural follow-up to the symlink-escape fix. The recurring miss (#465
fixed isSafePath but left render.ts; the sweep then turned up play.ts,
htmlBundler, ...) is because containment was enforced by convention —
"remember to call isSafePath after every resolve()" — which a new call site
can silently skip.

Add resolveWithinProject(base, relativePath) -> string | null (resolve +
containment in one call) and route the studio-api + bundler sites through
it, so a caller cannot resolve a project-relative path without the guard:

- studio-api routes/files.ts (read, rename, duplicate, upload-dir), preview.ts
  (sub-comp + static asset), render.ts (composition) — all the
  resolve()+isSafePath() pairs collapse to a single call.
- compiler/htmlBundler.ts: its local safePath helper was exactly this; drop
  it for the shared one.

Left intentionally on isSafePath: files.ts upload (resolves a name against a
validated sub-dir but contains against the project root) and htmlBundler's
CSS @import (resolves against the CSS file's dir, contains against the root) —
these resolve and contain against *different* bases, which the single-base
chokepoint doesn't model.

Exported from @hyperframes/core and re-exported from studio-api/helpers for
back-compat. Adds resolveWithinProject unit tests; all existing studio-api
route tests pass unchanged (behavior is identical — same resolve, same
containment, same reject paths).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
James Russo
2026-06-12 21:10:36 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent b9f8a30ee6
commit 1c47ba9981
8 changed files with 100 additions and 49 deletions
+1 -1
View File
@@ -163,7 +163,7 @@ export {
type MediaVisualStyleProperty,
} from "./inline-scripts/parityContract";
export { redactTelemetryString } from "./telemetryRedaction";
export { isSafePath } from "./safePath";
export { isSafePath, resolveWithinProject } from "./safePath";
export type {
HyperframePickerApi,
HyperframePickerBoundingBox,