feat: open workspace HTML files in a sandboxed browser tab - #1290
Conversation
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>
|
Reviewed and pushed a hardening pass to this branch ( Sandbox SVG/XML in the raw route — the route previously sandboxed only Tests: workspace-files 37/37 (added SVG + XML sandbox cases), typecheck + decision:check green; decision record updated. Note: this branch still lacks the |
… 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>
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.
GET /workspace/files/raw/{scope}/{path...}: the scope segment is the literalhomeor a base64url-encoded directory (decoded inprovideRequestScope, no header/query scope needed). Because the URL is path-shaped and ends in the file name, relativesrc/hrefreferences in the HTML (assets/cover/image1.jpeg, CSS, JS, fonts) resolve to siblings under the same prefix automatically — the query-style/content?path=...&raw=trueform broke this and 400'd via the scope-required prefix (plus Chrome ORB on the JSON body)..html/.htm) carryContent-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 applocalStorage, 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/contentreturns to PDF-only duty; OpenAPI/SDK regenerated..html/.htmonly) that builds the URL viabuildWorkspaceFileBrowserUrl(baseUrl, path, scope)and opens it throughplatform.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.mdTest 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 passbun run typecheck(app:tsgo -b; synergy pkg clean) — no errorsbun run i18n:check— contract passesassets/cover/image1.jpeg) in a new tab — page renders with scripts executing and images loadingNote:
bun run test:changedreports pre-existing failures in embedding/config/snapshot domains on this machine (unrelated to these files; present on the base as well).