Skip to content

feat: open workspace HTML files in a sandboxed browser tab - #1290

Merged
yzxoi merged 5 commits into
devfrom
feat/workspace-html-open-in-browser
Aug 31, 2026
Merged

feat: open workspace HTML files in a sandboxed browser tab#1290
yzxoi merged 5 commits into
devfrom
feat/workspace-html-open-in-browser

Conversation

@SII-jake

Copy link
Copy Markdown
Contributor

Summary

Workspace HTML files in the file workbench can now be opened in a new browser tab with JavaScript executing, while staying isolated from the app by a sandboxed opaque origin.

  • New path-shaped route GET /workspace/files/raw/{scope}/{path...}: the scope segment is the literal home or a base64url-encoded directory (decoded in provideRequestScope, no header/query scope needed). Because the URL is path-shaped and ends in the file name, relative src/href references in the HTML (assets/cover/image1.jpeg, CSS, JS, fonts) resolve to siblings under the same prefix automatically — the query-style /content?path=...&raw=true form broke this and 400'd via the scope-required prefix (plus Chrome ORB on the JSON body).
  • HTML responses (.html/.htm) carry Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-modals, mirroring the attachment asset route: scripts run but the page lands in an opaque origin, so it cannot reach app localStorage, the unauthenticated HTTP control plane, the event WebSocket, or the Desktop preload bridge. Non-HTML resources pass through with their real MIME type and no sandbox CSP. Traversal/absolute/NUL segments are rejected (400/403/404) without leaking existence or content.
  • GET /workspace/files/content returns to PDF-only duty; OpenAPI/SDK regenerated.
  • File workbench toolbar gains an "Open in browser" pill (icon + label, .html/.htm only) that builds the URL via buildWorkspaceFileBrowserUrl(baseUrl, path, scope) and opens it through platform.openLink (new tab on web, shell open on Desktop). i18n for English / 中文 / pseudo.

Decision record: docs/decisions/implemented/feature/2026-08-31-workspace-html-open-in-browser.md

Test plan

  • cd packages/synergy && bun test test/server/workspace-files.test.ts — 35/35 pass (new: sandboxed HTML, .htm, static resource MIME + no sandbox, nested subdirectories, traversal/encoded-traversal rejection, missing scope token 404, missing file 404)
  • cd packages/app && bun test test/utils/workspace-file-url.test.ts test/components/file-workbench/open-in-browser.dom.test.ts — 9/9 pass
  • bun run typecheck (app: tsgo -b; synergy pkg clean) — no errors
  • bun run i18n:check — contract passes
  • Manual: opened a slide-deck HTML file with relative images (assets/cover/image1.jpeg) in a new tab — page renders with scripts executing and images loading

Note: bun run test:changed reports pre-existing failures in embedding/config/snapshot domains on this machine (unrelated to these files; present on the base as well).

Test User and others added 4 commits August 31, 2026 17:06
Serve workspace files over GET /workspace/files/raw/{scope}/{path...}
so relative resources in HTML resolve natively. The scope segment is
the literal home or a base64url-encoded directory, decoded in
provideRequestScope. HTML responses land in an opaque origin via a
sandbox CSP mirroring the attachment asset route; non-HTML resources
pass through with their real MIME type. Untrusted segments (absolute,
dot-dot, NUL) are rejected with 400/403/404. GET /workspace/files/content
goes back to PDF-only duty.

Co-authored-by: synergy-agent <299070056+synergy-agent@users.noreply.github.com>
Show a toolbar pill button (icon + label) for .html/.htm files in the
file workbench. buildWorkspaceFileBrowserUrl encodes the scope as a
home token or base64url directory and builds the path-shaped raw URL;
platform.openLink opens it as a new browser tab on web or via the
shell on Desktop. i18n strings for English, Chinese, and pseudo.

Co-authored-by: synergy-agent <299070056+synergy-agent@users.noreply.github.com>
Co-authored-by: synergy-agent <299070056+synergy-agent@users.noreply.github.com>
The raw route previously sandboxed only .html/.htm. SVG opened as a top-level document would run scripts same-origin under the global SPA baseline CSP (script-src 'unsafe-inline'), reaching app localStorage, the unauthenticated HTTP control plane, and the event WebSocket; XML with XSLT processing instructions is the same class of issue. Extend the sandbox CSP to .svg/.xml/.xhtml, mirroring packages/synergy/src/server/asset.ts. Static sub-resources keep their real MIME type and no sandbox, since a sub-resource's CSP does not apply to the parent page. Adds tests for SVG and XML sandboxing and updates the decision record.

Co-authored-by: synergy-agent <299070056+synergy-agent@users.noreply.github.com>
@yzxoi

yzxoi commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Reviewed and pushed a hardening pass to this branch (2b78ae510):

Sandbox SVG/XML in the raw route — the route previously sandboxed only .html/.htm, but the workbench's raw route serves any file as a top-level document. An SVG with <script> (or XML with XSLT processing instructions) opened in a new tab would run under the global SPA baseline CSP (script-src 'unsafe-inline') with full same-origin authority — app localStorage, the unauthenticated HTTP control plane, the event WebSocket, the Desktop preload bridge. Verified empirically: GET /workspace/files/raw/<token>/evil.svg returned the baseline CSP, not the sandbox. Now .svg/.xml/.xhtml get the same sandbox allow-scripts allow-forms allow-popups allow-modals CSP as HTML, mirroring asset.ts. Static sub-resources (images/CSS/JS/fonts) keep their real MIME type and no sandbox, since a sub-resource's CSP doesn't apply to the parent page.

Tests: workspace-files 37/37 (added SVG + XML sandbox cases), typecheck + decision:check green; decision record updated.

Note: this branch still lacks the /global/event/ws Origin gate from #1289 — worth merging #1289 (with its hardening) first, then rebasing this PR, since the sandbox's isolation claims depend on it.

@yzxoi
yzxoi enabled auto-merge (squash) August 31, 2026 10:55
@yzxoi
yzxoi disabled auto-merge August 31, 2026 10:56
@yzxoi
yzxoi merged commit 7ba2408 into dev Aug 31, 2026
9 of 10 checks passed
@yzxoi
yzxoi deleted the feat/workspace-html-open-in-browser branch August 31, 2026 10:56
yzxoi added a commit that referenced this pull request Aug 31, 2026
… gate (#1293)

The file-workbench open-in-browser suite (#1290) failed the ci-coverage
gate: its Vite fixture could not resolve @ericsanchezok/synergy-plugin/theme
(the exports map serves import from gitignored dist/) and the real Lingui
runtime pulls @messageformat/parser (CJS) whose named import breaks under
dependency pre-bundling, so the page 500'd and every case timed out.

Port the ThemePicker fixture mitigations: alias plugin theme to source,
stub the Lingui runtime, pre-bundle the Solid runtime with noDiscovery,
scope the optimizer cache to the fixture, warm the module graph before
the browser connects, surface page/console/HTTP errors, and register the
suite in playwrightIsolated so worker reaping cannot kill Chromium.

Co-authored-by: synergy-agent <299070056+synergy-agent@users.noreply.github.com>
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