feat(server,web): serve Workspace HTML previews from a separate origin (#46)

Serve "open in new tab" HTML previews from a separate origin (the loopback counterpart, or PENGUIN_PREVIEW_ORIGIN) with a signed, host-bound token, so localStorage/cookies/third-party embeds work while Agent-generated pages stay off the app origin. The app is canonicalized onto localhost and the preview host (127.0.0.1) serves only /preview/* — its /api answers 401 and app routes 302 to the canonical host — so the preview origin can neither set nor honor a session cookie.
This commit is contained in:
Yaowei Zheng
2026-07-23 23:11:04 +08:00
committed by GitHub
parent cc1be054de
commit b18ae8f5f1
22 changed files with 842 additions and 14 deletions
+14 -2
View File
@@ -337,9 +337,21 @@ export const listWorkspaceFiles = (sessionId: string, path: string) =>
export const workspaceFileUrl = (sessionId: string, path: string, download = false): string =>
`/api/sessions/${sessionId}/files/content?path=${encodeURIComponent(path)}${download ? "&download=1" : ""}`;
/** Sandboxed top-level preview URL (open an html file in a new tab): real content type under a CSP sandbox, see the server route. */
/**
* "Open in a new tab" for a Workspace html file: an App-origin link that mints a signed
* token and 302s to the separate preview origin, where the page gets a real origin with
* working storage, cookies and third-party embeds.
*
* A link (not a fetch + `window.open`) on purpose — opening a tab after an await trips
* popup blockers, and a script-opened window keeps an `opener` handle back to the App,
* which is precisely the reference the separate origin exists to deny. Use it with
* `rel="noopener noreferrer"`.
*
* Falls back server-side to the sandboxed same-origin preview when the deployment has no
* usable preview origin; `previewIsolated` from /api/me says so in advance.
*/
export const workspaceFilePreviewUrl = (sessionId: string, path: string): string =>
`/api/sessions/${sessionId}/files/content?path=${encodeURIComponent(path)}&preview=1`;
`/api/sessions/${sessionId}/files/preview-redirect?path=${encodeURIComponent(path)}`;
export const uploadWorkspaceFile = (sessionId: string, path: string, dataBase64: string) =>
apiFetch<void>(`/api/sessions/${sessionId}/files/content`, {