From 09f8cfb7187baa4207dd77201be1e7b51082dd49 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 29 Sep 2026 07:36:03 +0900 Subject: [PATCH] fix: reject unresolved worker console targets --- .../console/worker-console.ui.test.ts | 15 +++-- .../lib/workspace/session-observation.test.ts | 25 +++++++++ .../src/lib/workspace/session-observation.ts | 24 +++++++- .../workers/[workerRef]/console/+page.svelte | 56 +++++++++++++------ .../workers/[workerRef]/console/+page.ts | 8 ++- .../console/console-display.browser.spec.ts | 23 ++++++++ 6 files changed, 124 insertions(+), 27 deletions(-) diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index 1200041a7..245b47400 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -972,9 +972,14 @@ Deno.test("Worker Console route resolves logical Worker authority before Runtime routeLoad.includes("resourceKey(params.workerRef)") && routeLoad.includes("/workers/${encodeURIComponent(reference)}") && routeLoad.includes("canonicalResourceReference") && - routeLoad.includes("runtimeId: result.data?.runtime_id") && - routeLoad.includes("workerId: result.data?.worker_id"), - "route load should resolve the logical Worker reference before exposing its execution target", + routeLoad.includes("runtimeId: result.data?.runtime_id ?? null") && + routeLoad.includes("workerId: result.data?.worker_id ?? null") && + !routeLoad.includes('result.data?.runtime_id ?? ""') && + consolePage.includes( + "resolveWorkerSessionTarget(workspaceId, runtimeId, workerId)", + ) && + consolePage.includes("if (!target) return;"), + "route load should retain an unresolved Worker target as null and the Console must fail closed before Runtime APIs", ); assert( consolePage.includes("workspaceApiPath(workspaceId, path)") && @@ -1228,9 +1233,9 @@ Deno.test("Workspace Worker list and Console share the multiplexed connection", consolePage.includes("const targetWorker = data.worker") && consolePage.includes("worker = targetWorker") && consolePage.includes( - "const consoleTarget = $derived({ workspaceId, runtimeId, workerId })", + "resolveWorkerSessionTarget(workspaceId, runtimeId, workerId)", ), - "A reused Console route should subscribe immediately on the live Workspace socket and install the new route Worker", + "A reused Console route should subscribe immediately only after resolving the new route Worker execution target", ); }); diff --git a/web/workspace/src/lib/workspace/session-observation.test.ts b/web/workspace/src/lib/workspace/session-observation.test.ts index 51e31cf02..27a7a2171 100644 --- a/web/workspace/src/lib/workspace/session-observation.test.ts +++ b/web/workspace/src/lib/workspace/session-observation.test.ts @@ -1,5 +1,6 @@ import { isCurrentWorkerSessionRequest, + resolveWorkerSessionTarget, workerSessionAction, type WorkerSessionRequestIdentity, workerSessionRequestInit, @@ -24,6 +25,30 @@ const snapshot: SessionSnapshot = { entries: [], }; +Deno.test("worker session target requires every routing identity", () => { + const target = resolveWorkerSessionTarget( + "workspace-a", + "runtime-a", + "worker-a", + ); + assert( + target?.runtimeId === "runtime-a" && target.workerId === "worker-a", + "resolved identities must produce an API target", + ); + for ( + const unresolved of [ + resolveWorkerSessionTarget("", "runtime-a", "worker-a"), + resolveWorkerSessionTarget("workspace-a", "", "worker-a"), + resolveWorkerSessionTarget("workspace-a", "runtime-a", ""), + resolveWorkerSessionTarget("workspace-a", null, "worker-a"), + resolveWorkerSessionTarget("workspace-a", "runtime-a", undefined), + resolveWorkerSessionTarget("workspace-a", " ", "worker-a"), + ] + ) { + assert(unresolved === null, "incomplete identities must fail closed"); + } +}); + Deno.test("worker session request includes same-origin credentials", () => { const controller = new AbortController(); const init = workerSessionRequestInit(controller.signal); diff --git a/web/workspace/src/lib/workspace/session-observation.ts b/web/workspace/src/lib/workspace/session-observation.ts index 9c5894507..861b0e5d7 100644 --- a/web/workspace/src/lib/workspace/session-observation.ts +++ b/web/workspace/src/lib/workspace/session-observation.ts @@ -10,13 +10,33 @@ export type WorkerSessionAction = | { kind: "apply_retained"; snapshot: SessionSnapshot } | { kind: "show_unavailable"; message: string }; -export interface WorkerSessionRequestIdentity { - token: number; +export interface WorkerSessionTarget { workspaceId: string; runtimeId: string; workerId: string; } +export interface WorkerSessionRequestIdentity extends WorkerSessionTarget { + token: number; +} + +export function resolveWorkerSessionTarget( + workspaceId: string, + runtimeId: string | null | undefined, + workerId: string | null | undefined, +): WorkerSessionTarget | null { + if ( + workspaceId.trim().length === 0 || + runtimeId?.trim().length === 0 || + workerId?.trim().length === 0 || + runtimeId == null || + workerId == null + ) { + return null; + } + return { workspaceId, runtimeId, workerId }; +} + export function workerSessionRequestInit(signal: AbortSignal): RequestInit { return { signal, diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte index 2b5abd0ac..af1b0eb5c 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte @@ -1,4 +1,5 @@