From 95af861dfd066a81b19e4c07adbae3dcf19cbf57 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sun, 2 Aug 2026 05:51:16 +0200 Subject: [PATCH] refactor(security): remove dormant SSR worker path --- .../orchestrator/ssr-orchestrator.ts | 125 --------------- src/security/sandbox/project-worker.test.ts | 54 ------- src/security/sandbox/project-worker.ts | 139 +--------------- src/security/sandbox/worker-pool.test.ts | 18 ++- src/security/sandbox/worker-pool.ts | 19 +-- src/security/sandbox/worker-script.ts | 148 ------------------ src/security/sandbox/worker-types.ts | 42 +---- 7 files changed, 21 insertions(+), 524 deletions(-) diff --git a/src/rendering/orchestrator/ssr-orchestrator.ts b/src/rendering/orchestrator/ssr-orchestrator.ts index ab00d1c0f4..cce9acfe75 100644 --- a/src/rendering/orchestrator/ssr-orchestrator.ts +++ b/src/rendering/orchestrator/ssr-orchestrator.ts @@ -10,9 +10,6 @@ import type { HTMLGenerationContext, HTMLGenerator } from "./html.ts"; import type { LayoutOrchestrator } from "./layout.ts"; import type { RenderOptions } from "./types.ts"; import { runWithHeadCollector } from "#veryfront/react/head-collector.ts"; -import { getWorkerPool, isSSRIsolationEnabled } from "#veryfront/security/sandbox/worker-pool.ts"; -import type { WorkerResponse } from "#veryfront/security/sandbox/worker-types.ts"; -import { requireActiveSourceIntegrationPolicy } from "#veryfront/integrations/source-policy-context.ts"; import { endRenderSession, hasRenderSession, @@ -36,24 +33,6 @@ export interface SSRRenderingResult { ssrHash: string; } -/** - * Options for isolated SSR rendering through the Worker pool. - * When provided and SSR isolation is enabled, the rendering happens - * in a per-project Worker instead of the main process. - */ -export interface SSRIsolationOptions { - /** Temp file path for the page component module */ - pageModulePath: string; - /** Ordered layout module temp paths (innermost to outermost) */ - layoutModulePaths: string[]; - /** Page component props */ - pageProps: Record; - /** Layout props (one entry per layout, matching layoutModulePaths order) */ - layoutProps: Record[]; - /** Project directory for worker scoping */ - projectDir: string; -} - function getElementTypeName(el: React.ReactElement | null | undefined): string { if (!el?.type) return "unknown"; if (typeof el.type === "string") return el.type; @@ -93,21 +72,7 @@ export class SSROrchestrator { pageElement: React.ReactElement, generationContext: Omit, options?: RenderOptions, - isolationOptions?: SSRIsolationOptions, ): Promise { - // Isolated SSR path: render in per-project Worker - if ( - isSSRIsolationEnabled() && - isolationOptions?.pageModulePath && - isolationOptions?.projectDir - ) { - // NOTE: the app-router error.tsx catch below is scoped to the main-process - // render path. Under SSR isolation (per-project Worker) a page throw is not - // yet routed to error.tsx — a follow-up, isolation being off by default. - return this.performIsolatedSSR(generationContext, options, isolationOptions); - } - - // Default path: render in main process logger.debug("performSSRRendering called", { elementType: getElementTypeName(pageElement), hasChildren: !!(pageElement.props as Record)?.children, @@ -299,96 +264,6 @@ export class SSROrchestrator { return { result: rendered.result, head: rendered.head, errorPath: errorInfo.path }; } - /** - * Perform SSR rendering in an isolated per-project Worker. - * - * The Worker imports user modules from their temp file paths, - * constructs the React element tree, and renders to HTML. - * For streaming, the Worker sends chunks via postMessage. - */ - private async performIsolatedSSR( - generationContext: Omit, - options: RenderOptions | undefined, - isolation: SSRIsolationOptions, - ): Promise { - const wantsStream = options?.delivery === "stream"; - const pool = getWorkerPool(); - const requestId = crypto.randomUUID(); - - return withSpan( - "ssr.isolated_render", - async () => { - const worker = pool.getOrCreateWorker(isolation.projectDir, [isolation.projectDir]); - - if (wantsStream) { - // Streaming mode: get a ReadableStream of chunks from the Worker - const stream = worker.executeStream({ - type: "render-ssr", - id: requestId, - pageModulePath: isolation.pageModulePath, - layoutModulePaths: isolation.layoutModulePaths, - pageProps: isolation.pageProps, - layoutProps: isolation.layoutProps, - delivery: "stream", - sourceIntegrationPolicy: requireActiveSourceIntegrationPolicy(), - }); - - const ssrHash = `stream-isolated-${Date.now()}`; - - // Generate HTML stream using the framework's HTML generator - const finalStream = await this.config.htmlGenerator.generateHTMLStream(stream, { - ...generationContext, - ssrHash, - options: { ...generationContext.options, ...options }, - collectedHead: undefined, - }); - - return { fullHtml: "", finalStream, ssrHash }; - } - - // String mode: render to HTML in Worker, get result back - const workerResponse: WorkerResponse = await worker.execute({ - type: "render-ssr", - id: requestId, - pageModulePath: isolation.pageModulePath, - layoutModulePaths: isolation.layoutModulePaths, - pageProps: isolation.pageProps, - layoutProps: isolation.layoutProps, - delivery: "string", - sourceIntegrationPolicy: requireActiveSourceIntegrationPolicy(), - }); - - if (workerResponse.type === "error") { - const err = new Error(workerResponse.error.message); - err.name = workerResponse.error.name; - throw err; - } - - if (workerResponse.type !== "ssr-result") { - throw new Error(`Unexpected worker response type: ${workerResponse.type}`); - } - - const html = workerResponse.html; - const ssrHash = await computeHash(html); - - const fullHtml = await this.config.htmlGenerator.generateFullHTML({ - ...generationContext, - html, - ssrHash, - options: { ...generationContext.options, ...options }, - collectedHead: undefined, - }); - - return { fullHtml, finalStream: null, ssrHash }; - }, - { - "ssr.isolated": true, - "ssr.wants_stream": wantsStream, - "ssr.project_dir": isolation.projectDir, - }, - ); - } - private createStream(html: string): ReadableStream | null { try { return new Response(html).body ?? null; diff --git a/src/security/sandbox/project-worker.test.ts b/src/security/sandbox/project-worker.test.ts index ff4843740c..745257cbe0 100644 --- a/src/security/sandbox/project-worker.test.ts +++ b/src/security/sandbox/project-worker.test.ts @@ -39,7 +39,6 @@ const TEST_WORKER_SCRIPT_URL = `data:application/typescript,${ return; } if (msg.type === "clear-cache") return; - if (msg.type === "render-ssr") return; self.postMessage({ type: "error", id: msg.id, @@ -432,15 +431,6 @@ testSuite("ProjectWorker - real worker request isolation", () => { url: serializedRequest.url, }, }, - { - type: "render-ssr", - id: "ssr", - pageModulePath: modulePath, - layoutModulePaths: [], - pageProps: {}, - layoutProps: [], - delivery: "string", - }, ]; worker.start(); @@ -1354,47 +1344,3 @@ testSuite("ProjectWorker - real worker request isolation", () => { } }); }); - -testSuite("ProjectWorker - executeStream", () => { - it("throws when worker is not started", () => { - const worker = createTestWorker("test-stream"); - let threw = false; - try { - worker.executeStream({ - type: "render-ssr", - id: "test-id", - pageModulePath: "/nonexistent.ts", - layoutModulePaths: [], - pageProps: {}, - layoutProps: [], - delivery: "stream", - sourceIntegrationPolicy: TEST_SOURCE_INTEGRATION_POLICY, - }); - } catch { - threw = true; - } - assert(threw, "should throw when worker is not available"); - }); - - it("returns a ReadableStream when worker is started", () => { - const worker = createTestWorker("test-stream"); - worker.start(); - try { - const stream = worker.executeStream({ - type: "render-ssr", - id: "test-id", - pageModulePath: "/nonexistent.ts", - layoutModulePaths: [], - pageProps: {}, - layoutProps: [], - delivery: "stream", - sourceIntegrationPolicy: TEST_SOURCE_INTEGRATION_POLICY, - }); - assert(stream instanceof ReadableStream, "should return a ReadableStream"); - // Cancel the stream to clean up - stream.cancel(); - } finally { - worker.terminate(); - } - }); -}); diff --git a/src/security/sandbox/project-worker.ts b/src/security/sandbox/project-worker.ts index 2b0fc3b731..7d606edd95 100644 --- a/src/security/sandbox/project-worker.ts +++ b/src/security/sandbox/project-worker.ts @@ -21,15 +21,9 @@ import { type WorkerEgressBroker, } from "./worker-egress-guard.ts"; import type { WorkerPermissions } from "./worker-permissions.ts"; -import type { - WorkerRequest, - WorkerResponse, - WorkerStreamChunk, - WorkerStreamEnd, -} from "./worker-types.ts"; +import type { WorkerRequest, WorkerResponse } from "./worker-types.ts"; const logger = serverLogger.component("project-worker"); -const textEncoder = new TextEncoder(); // Intersection with the DOM `WorkerOptions` so the value is assignable to the // `Worker` constructor without suppression — Deno reads the extra `deno` field @@ -56,12 +50,6 @@ interface PendingRequest { timer: ReturnType; } -interface StreamHandler { - onChunk: (chunk: Uint8Array) => void; - onEnd: () => void; - onError: (error: Error) => void; -} - /** * Status of a project worker. */ @@ -72,7 +60,6 @@ export class ProjectWorker { private worker: Worker | null = null; private pending = new Map(); - private streamHandlers = new Map(); private requestTimeoutMs: number; private permissions: WorkerPermissions; private workerScriptUrl?: string; @@ -224,109 +211,6 @@ export class ProjectWorker { ); } - /** - * Execute a streaming request. Returns a ReadableStream that yields - * chunks as they arrive from the Worker via postMessage. - * - * Used for streaming SSR where the Worker sends chunks progressively. - * Falls back to a single-chunk stream if the Worker returns a non-streaming - * response (ssr-result with full HTML). - */ - executeStream(request: WorkerRequest): ReadableStream { - if (!this.worker || this._status === "crashed" || this._status === "terminated") { - throw UNKNOWN_ERROR.create({ detail: `Worker not available (status: ${this._status})` }); - } - - this._requestCount++; - this._lastActivityAt = Date.now(); - this._status = "busy"; - - const requestId = request.id; - - return new ReadableStream({ - start: (controller) => { - let timer = setTimeout(() => { - this.streamHandlers.delete(requestId); - this.pending.delete(requestId); - this.updateIdleStatus(); - controller.error( - TIMEOUT_ERROR.create({ - detail: `Worker stream timed out after ${this.requestTimeoutMs}ms`, - }), - ); - }, this.requestTimeoutMs); - - const resetTimer = () => { - clearTimeout(timer); - timer = setTimeout(() => { - this.streamHandlers.delete(requestId); - this.pending.delete(requestId); - this.updateIdleStatus(); - controller.error( - TIMEOUT_ERROR.create({ - detail: `Worker stream timed out after ${this.requestTimeoutMs}ms`, - }), - ); - }, this.requestTimeoutMs); - }; - - // Register a stream handler for this request - this.streamHandlers.set(requestId, { - onChunk: (chunk: Uint8Array) => { - resetTimer(); - controller.enqueue(chunk); - }, - onEnd: () => { - clearTimeout(timer); - this.streamHandlers.delete(requestId); - this.pending.delete(requestId); - this.updateIdleStatus(); - controller.close(); - }, - onError: (error: Error) => { - clearTimeout(timer); - this.streamHandlers.delete(requestId); - this.pending.delete(requestId); - this.updateIdleStatus(); - controller.error(error); - }, - }); - - // Also register in pending for non-streaming responses (fallback) - this.pending.set(requestId, { - resolve: (response) => { - clearTimeout(timer); - this.streamHandlers.delete(requestId); - this.pending.delete(requestId); - this.updateIdleStatus(); - - // If we get an ssr-result, emit it as a single chunk - if (response.type === "ssr-result") { - controller.enqueue(textEncoder.encode(response.html)); - controller.close(); - } else if (response.type === "error") { - const err = new Error(response.error.message); - err.name = response.error.name; - controller.error(err); - } else { - controller.close(); - } - }, - reject: (error) => { - clearTimeout(timer); - this.streamHandlers.delete(requestId); - this.pending.delete(requestId); - this.updateIdleStatus(); - controller.error(error); - }, - timer, - }); - - this.worker!.postMessage(request); - }, - }); - } - /** * Health check — send a ping and wait for pong. */ @@ -416,8 +300,6 @@ export class ProjectWorker { private handleMessage( data: | WorkerResponse - | WorkerStreamChunk - | WorkerStreamEnd | { type: "worker-exit" } | { type: "pong"; id: string }, ): void { @@ -436,19 +318,6 @@ export class ProjectWorker { return; } - // Handle streaming SSR chunks - if (data.type === "stream-chunk") { - const handler = this.streamHandlers.get(data.id); - if (handler) handler.onChunk(data.chunk); - return; - } - - if (data.type === "stream-end") { - const handler = this.streamHandlers.get(data.id); - if (handler) handler.onEnd(); - return; - } - const response = data as WorkerResponse; const pending = this.pending.get(response.id); if (!pending) { @@ -478,11 +347,5 @@ export class ProjectWorker { pending.reject(UNKNOWN_ERROR.create({ detail: reason })); this.pending.delete(id); } - - // Clean up stream handlers - for (const [id, handler] of this.streamHandlers) { - handler.onError(UNKNOWN_ERROR.create({ detail: reason })); - this.streamHandlers.delete(id); - } } } diff --git a/src/security/sandbox/worker-pool.test.ts b/src/security/sandbox/worker-pool.test.ts index f0fadbd4f4..71401b47c9 100644 --- a/src/security/sandbox/worker-pool.test.ts +++ b/src/security/sandbox/worker-pool.test.ts @@ -1,12 +1,17 @@ import "#veryfront/schemas/_test-setup.ts"; -import { assert, assertEquals, assertExists, assertRejects } from "#veryfront/testing/assert.ts"; +import { + assert, + assertEquals, + assertExists, + assertRejects, + assertThrows, +} from "#veryfront/testing/assert.ts"; import { afterEach, beforeEach, describe, it } from "#veryfront/testing/bdd.ts"; import { isDeno } from "#veryfront/platform/compat/runtime.ts"; import { VeryfrontError } from "#veryfront/errors/types.ts"; import { __resetPoolForTests, isDataIsolationEnabled, - isSSRIsolationEnabled, isWorkerIsolationEnabled, WorkerPool, } from "./worker-pool.ts"; @@ -356,7 +361,6 @@ describe("Feature flag caching", () => { __resetPoolForTests(); assertEquals(isWorkerIsolationEnabled(), false); assertEquals(isDataIsolationEnabled(), false); - assertEquals(isSSRIsolationEnabled(), false); }); it("returns true for API isolation when both flags set", () => { @@ -373,11 +377,15 @@ describe("Feature flag caching", () => { assertEquals(isDataIsolationEnabled(), true); }); - it("returns true for SSR isolation when both flags set", () => { + it("fails closed when the removed SSR isolation flag is enabled", () => { __resetPoolForTests(); Deno.env.set("WORKER_ISOLATION_ENABLED", "1"); Deno.env.set("WORKER_ISOLATION_SSR", "1"); - assertEquals(isSSRIsolationEnabled(), true); + assertThrows( + () => isWorkerIsolationEnabled(), + Error, + "WORKER_ISOLATION_SSR is unsupported", + ); }); it("caches flag results across calls", () => { diff --git a/src/security/sandbox/worker-pool.ts b/src/security/sandbox/worker-pool.ts index 4b46618caa..85a0ce04d8 100644 --- a/src/security/sandbox/worker-pool.ts +++ b/src/security/sandbox/worker-pool.ts @@ -11,7 +11,7 @@ import { serverLogger } from "#veryfront/utils"; import { getEnvBoolean, getEnvNumber, unrefTimer } from "#veryfront/platform/compat/process.ts"; import { withSpan } from "#veryfront/observability/tracing/otlp-setup.ts"; -import { SECURITY_VIOLATION } from "#veryfront/errors"; +import { NOT_SUPPORTED, SECURITY_VIOLATION } from "#veryfront/errors"; import { ProjectWorker } from "./project-worker.ts"; import { buildWorkerEnvAllowlist, buildWorkerPermissions } from "./worker-permissions.ts"; import type { WorkerPoolConfig, WorkerRequest, WorkerResponse } from "./worker-types.ts"; @@ -424,14 +424,17 @@ export class WorkerPool { let _flagsResolved = false; let _apiIsolation = false; let _dataIsolation = false; -let _ssrIsolation = false; function resolveFlags(): void { if (_flagsResolved) return; const master = getEnvBoolean("WORKER_ISOLATION_ENABLED", false); + if (master && getEnvBoolean("WORKER_ISOLATION_SSR", false)) { + throw NOT_SUPPORTED.create({ + detail: "WORKER_ISOLATION_SSR is unsupported; SSR uses the bounded main-process renderer", + }); + } _apiIsolation = master && getEnvBoolean("WORKER_ISOLATION_API", false); _dataIsolation = master && getEnvBoolean("WORKER_ISOLATION_DATA", false); - _ssrIsolation = master && getEnvBoolean("WORKER_ISOLATION_SSR", false); _flagsResolved = true; } @@ -453,15 +456,6 @@ export function isDataIsolationEnabled(): boolean { return _dataIsolation; } -/** - * Whether worker isolation is enabled for SSR rendering. - * Controlled by WORKER_ISOLATION_SSR=1 (requires WORKER_ISOLATION_ENABLED=1). - */ -export function isSSRIsolationEnabled(): boolean { - resolveFlags(); - return _ssrIsolation; -} - /** Lazy singleton — created on first use when isolation is enabled */ let _pool: WorkerPool | null = null; @@ -491,5 +485,4 @@ export function __resetPoolForTests(): void { _flagsResolved = false; _apiIsolation = false; _dataIsolation = false; - _ssrIsolation = false; } diff --git a/src/security/sandbox/worker-script.ts b/src/security/sandbox/worker-script.ts index f588a9a6d9..088f51bbbe 100644 --- a/src/security/sandbox/worker-script.ts +++ b/src/security/sandbox/worker-script.ts @@ -14,7 +14,6 @@ import type { ExecuteAppRouteRequest, ExecutePagesRouteRequest, FetchDataRequest, - RenderSSRRequest, SerializedDataContext, SerializedDataResult, SerializedError, @@ -25,9 +24,6 @@ import type { WorkerErrorResponse, WorkerRequest, WorkerResultResponse, - WorkerSSRResultResponse, - WorkerStreamChunk, - WorkerStreamEnd, } from "./worker-types.ts"; import { installWorkerEgressGuard, type WorkerEgressGuardOptions } from "./worker-egress-guard.ts"; import { isAbsolute, relative, resolve as resolvePath, sep as PATH_SEP } from "node:path"; @@ -139,26 +135,6 @@ export function makeProjectPathGuard(projectDir: string): (path: string) => Prom }; } -// Load React lazily for SSR requests. API-only workers and health checks should -// start without resolving React, and the runtime caches dynamic imports after -// the first SSR request. -let _React: typeof import("react") | null = null; -let _ReactDOMServer: typeof import("react-dom/server") | null = null; -let _reactReady: Promise | null = null; - -function ensureReactReady(): Promise { - _reactReady ??= (async () => { - try { - _React = await import("react"); - _ReactDOMServer = await import("react-dom/server"); - } catch { - // React may not be available in all worker contexts (e.g., API-only workers). - // SSR handler will throw a clear error if React is needed but not loaded. - } - })(); - return _reactReady; -} - // --------------------------------------------------------------------------- // Serialization Helpers // --------------------------------------------------------------------------- @@ -507,114 +483,6 @@ async function handlePagesRoute(req: ExecutePagesRouteRequest): Promise { - return await runWithWorkerSourceIntegrationPolicy( - req.sourceIntegrationPolicy, - async () => await renderSSR(req), - ); -} - -async function renderSSR( - req: RenderSSRRequest, -): Promise<{ html: string } | "streaming"> { - // Load React only for SSR workers. API-only workers and health checks should - // not pay the React import cost or contend on it under parallel worker tests. - await ensureReactReady(); - - if (!_React || !_ReactDOMServer) { - throw new Error("React modules not available in this worker"); - } - - const React = _React; - const { renderToString } = _ReactDOMServer; - - // Import the page component - const pageMod = await loadModule(req.pageModulePath); - const PageComponent = (pageMod.default ?? pageMod) as React.ComponentType< - Record - >; - - // Import layout components (innermost → outermost order) - const layoutComponents: React.ComponentType>[] = []; - for (const layoutPath of req.layoutModulePaths) { - const layoutMod = await loadModule(layoutPath); - layoutComponents.push( - (layoutMod.default ?? layoutMod) as React.ComponentType< - Record - >, - ); - } - - // Build element tree: page is innermost, layouts wrap outward - const createElement = React.createElement as ( - type: unknown, - props: Record | null, - ...children: unknown[] - ) => React.ReactElement; - - let element: React.ReactElement = createElement(PageComponent, req.pageProps); - - for (let i = 0; i < layoutComponents.length; i++) { - const Layout = layoutComponents[i]; - const layoutProps = req.layoutProps[i] ?? {}; - element = createElement(Layout, layoutProps, element); - } - - // Streaming mode: send chunks via postMessage - if (req.delivery === "stream") { - // Use renderToReadableStream if available (React 18+) - const serverModule = _ReactDOMServer as unknown as Record; - const renderToReadableStream = serverModule.renderToReadableStream as - | ((element: React.ReactElement) => Promise>) - | undefined; - - if (renderToReadableStream) { - const stream = await renderToReadableStream(element); - const reader = stream.getReader(); - - while (true) { - const { done, value } = await reader.read(); - if (done) { - const endMsg: WorkerStreamEnd = { type: "stream-end", id: req.id }; - self.postMessage(endMsg); - break; - } - const chunkMsg: WorkerStreamChunk = { - type: "stream-chunk", - id: req.id, - chunk: value, - }; - // Transfer the Uint8Array for zero-copy - self.postMessage(chunkMsg, { transfer: [value.buffer] }); - } - - return "streaming"; - } - - // Fallback: render to string if streaming not available - } - - // String mode (or streaming fallback): render to string - const html = renderToString(element); - return { html }; -} - // --------------------------------------------------------------------------- // Message Handler // --------------------------------------------------------------------------- @@ -637,22 +505,6 @@ async function processWorkerRequest(request: WorkerRequest): Promise { return; } - // SSR rendering — may stream chunks or return HTML string - if (request.type === "render-ssr") { - const ssrResult = await handleRenderSSR(request); - - // If streaming, chunks were already sent via postMessage - if (ssrResult === "streaming") return; - - const ssrResponse: WorkerSSRResultResponse = { - type: "ssr-result", - id: request.id, - html: ssrResult.html, - }; - self.postMessage(ssrResponse); - return; - } - let serializedResponse: SerializedResponse; switch (request.type) { diff --git a/src/security/sandbox/worker-types.ts b/src/security/sandbox/worker-types.ts index 6bddb847b7..6e4ff0297a 100644 --- a/src/security/sandbox/worker-types.ts +++ b/src/security/sandbox/worker-types.ts @@ -86,8 +86,7 @@ export interface SerializedDataResult { export type WorkerRequest = | ExecuteAppRouteRequest | ExecutePagesRouteRequest - | FetchDataRequest - | RenderSSRRequest; + | FetchDataRequest; export interface ExecuteAppRouteRequest { type: "execute-app-route"; @@ -125,50 +124,11 @@ export interface FetchDataRequest { sourceIntegrationPolicy: SourceIntegrationPolicyManifest; } -export interface RenderSSRRequest { - type: "render-ssr"; - id: string; - /** Temp file path for the page component module */ - pageModulePath: string; - /** Ordered layout module temp paths (innermost → outermost) */ - layoutModulePaths: string[]; - /** Page component props (JSON-serializable) */ - pageProps: Record; - /** Layout props keyed by layout index (matching layoutModulePaths order) */ - layoutProps: Record[]; - /** Rendering delivery mode */ - delivery: "string" | "stream"; - /** Exact source-owned integration policy for this project execution. */ - sourceIntegrationPolicy: SourceIntegrationPolicyManifest; -} - -// --------------------------------------------------------------------------- -// Streaming SSR Protocol -// --------------------------------------------------------------------------- - -export interface WorkerStreamChunk { - type: "stream-chunk"; - id: string; - chunk: Uint8Array; -} - -export interface WorkerStreamEnd { - type: "stream-end"; - id: string; -} - export type WorkerResponse = | WorkerResultResponse | WorkerDataResultResponse - | WorkerSSRResultResponse | WorkerErrorResponse; -export interface WorkerSSRResultResponse { - type: "ssr-result"; - id: string; - html: string; -} - export interface WorkerResultResponse { type: "result"; id: string;