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 authored and GitHub committed 2026-07-23 23:11:04 +08:00
1 parent cc1be054de
commit b18ae8f5f1
22 files changed
+842 -14

No files matched your search

+8 -3
View File
@@ -25,8 +25,11 @@ export function testConfig(root: string): ServerConfig {
return {
root,
host: "127.0.0.1",
port: 0,
// Nothing listens in tests, but the value is not inert: preview URLs are built from
// the server's own port, so keep it realistic rather than 0.
port: 7364,
dbPath: ":memory:",
previewOrigin: null,
// Points to a nonexistent directory: static hosting is disabled in tests.
webDist: path.join(root, "__no_web_dist__"),
authSessionTtlMs: 7 * DAY_MS,
@@ -44,13 +47,15 @@ export interface TestApp {
export interface TestAppOptions extends BuildDepsOverrides {
/** Runs before seeding the admin (for scenarios pre-populating a default_project config as the CLI would). */
beforeSeed?: (root: string) => Promise<void>;
/** Overrides merged onto the default test ServerConfig (e.g. `previewOrigin`). */
config?: Partial<ServerConfig>;
}
export async function createTestApp(options: TestAppOptions = {}): Promise<TestApp> {
const { beforeSeed, ...overrides } = options;
const { beforeSeed, config, ...overrides } = options;
const root = await makeTempRoot();
if (beforeSeed) await beforeSeed(root);
const deps = buildAppDeps(testConfig(root), { log: () => {}, ...overrides });
const deps = buildAppDeps({ ...testConfig(root), ...config }, { log: () => {}, ...overrides });
// Consistent with the startup entrypoint: seed the built-in admin (owning default_project).
await deps.authService.seedAdmin();
const app = createApp(deps);