From e984074de03160941a8872ac69457622d827a0c4 Mon Sep 17 00:00:00 2001 From: lyon Date: Mon, 15 Jun 2026 15:42:05 +0800 Subject: [PATCH] fix: align cloud web workbench bootstrap entry --- internal/dev-entrypoint/cloud-web-routes.mjs | 3 +- .../dev-entrypoint/cloud-web-routes.test.mjs | 6 --- internal/dev-entrypoint/cloud-web-runtime.mjs | 41 ------------------- web/hwlab-cloud-web/index.html | 2 +- web/hwlab-cloud-web/scripts/check.ts | 4 ++ web/hwlab-cloud-web/src/api/auth.ts | 1 - .../src/composables/useWorkspaceBootstrap.ts | 16 ++++---- 7 files changed, 13 insertions(+), 60 deletions(-) diff --git a/internal/dev-entrypoint/cloud-web-routes.mjs b/internal/dev-entrypoint/cloud-web-routes.mjs index da9fa6ec..49f8a067 100644 --- a/internal/dev-entrypoint/cloud-web-routes.mjs +++ b/internal/dev-entrypoint/cloud-web-routes.mjs @@ -1,5 +1,5 @@ const GET_PROXY_PREFIXES = Object.freeze(["/v1/"]); -const GET_PROXY_ROUTES = new Set(["/v1", "/auth/session", "/auth/bootstrap", "/auth/workspace-bootstrap"]); +const GET_PROXY_ROUTES = new Set(["/v1", "/auth/session", "/auth/bootstrap"]); const POST_PROXY_ROUTES = new Set([ "/auth/login", "/auth/register", @@ -17,7 +17,6 @@ const POST_PROXY_ROUTES = new Set([ const PUBLIC_PROXY_ROUTES = new Set([ "GET /auth/session", "GET /auth/bootstrap", - "GET /auth/workspace-bootstrap", "POST /auth/login", "POST /auth/register", "POST /auth/logout", diff --git a/internal/dev-entrypoint/cloud-web-routes.test.mjs b/internal/dev-entrypoint/cloud-web-routes.test.mjs index c48fc867..f8163481 100644 --- a/internal/dev-entrypoint/cloud-web-routes.test.mjs +++ b/internal/dev-entrypoint/cloud-web-routes.test.mjs @@ -61,12 +61,6 @@ test("cloud web exposes auth bootstrap as a public auth route", () => { publicRoute: true, routeKey: "GET /auth/bootstrap" }); - assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/auth/workspace-bootstrap"), { - proxy: true, - authRequired: false, - publicRoute: true, - routeKey: "GET /auth/workspace-bootstrap" - }); assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/auth/register"), { proxy: true, authRequired: false, diff --git a/internal/dev-entrypoint/cloud-web-runtime.mjs b/internal/dev-entrypoint/cloud-web-runtime.mjs index d8bfa32a..5d0c3b4a 100644 --- a/internal/dev-entrypoint/cloud-web-runtime.mjs +++ b/internal/dev-entrypoint/cloud-web-runtime.mjs @@ -15,7 +15,6 @@ const readOnlyRpcMethods = new Set([ ]); const gzipAsync = promisify(gzip); const GZIP_MIN_BYTES = 1024; -const DEFAULT_WORKBENCH_PROJECT_ID = "prj_hwpod_workbench"; const CLIENT_DISCONNECT_ERROR_CODES = new Set([ "ECONNRESET", "EPIPE", @@ -191,10 +190,6 @@ function logCloudWebRuntimeEvent(event, { serviceId, request = null, error = nul export async function handleCloudWebAuth(options) { const { request, response, url } = options; - if (url.pathname === "/auth/workspace-bootstrap" && request.method === "GET") { - await handleWorkspaceBootstrap(options); - return true; - } if (url.pathname === "/auth/bootstrap" && request.method === "GET") { await handleAuthBootstrap(options); return true; @@ -231,47 +226,11 @@ async function handleAuthBootstrap(options) { sendJsonWithForwardedCookies(response, auth.statusCode || 502, auth.body ?? { authenticated: false, error: "auth_bootstrap_failed" }, auth.headers); } -async function handleWorkspaceBootstrap(options) { - const { request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson } = options; - if (!cloudApiBaseUrl) { - sendJson(response, 503, { error: "cloud_api_base_url_missing", serviceId, path: "/auth/workspace-bootstrap" }); - return; - } - - const auth = await resolveAuthBootstrap({ request, cloudApiBaseUrl, cloudApiProxyTimeoutMs }); - if (auth.body?.authenticated !== true) { - sendJsonWithForwardedCookies(response, auth.statusCode || 200, auth.body ?? { authenticated: false }, auth.headers); - return; - } - - const projectId = authConfigValueFromSearch(url.searchParams.get("projectId"), DEFAULT_WORKBENCH_PROJECT_ID); - const workspace = await requestCloudApiJson({ - request, - method: "GET", - pathname: `/v1/workbench/workspace?projectId=${encodeURIComponent(projectId)}`, - headers: auth.cookieHeader ? { cookie: auth.cookieHeader } : {}, - cloudApiBaseUrl, - cloudApiProxyTimeoutMs - }); - const body = { - ...(auth.body ?? { authenticated: true }), - projectId, - workspace: workspace.ok ? workspace.body?.workspace ?? null : null, - workspaceStatus: workspace.statusCode || null, - workspaceError: workspace.ok ? null : workspace.body?.error ?? workspace.body?.message ?? null - }; - sendJsonWithForwardedCookies(response, auth.statusCode || 200, body, auth.headers); -} - async function resolveAuthBootstrap({ request, cloudApiBaseUrl, cloudApiProxyTimeoutMs }) { const session = await requestCloudApiJson({ request, method: "GET", pathname: "/auth/session", cloudApiBaseUrl, cloudApiProxyTimeoutMs }); return withCookieHeader(request, session); } -function authConfigValueFromSearch(value, fallback) { - return typeof value === "string" && value.trim() ? value.trim() : fallback; -} - function requestCloudApiJson({ request, method, pathname, body = "", headers = {}, cloudApiBaseUrl, cloudApiProxyTimeoutMs }) { return new Promise((resolve) => { let settled = false; diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 8b5f94de..3e022d9d 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -16,7 +16,7 @@ try { projectId = window.localStorage.getItem("hwlab.workbench.projectId.v1"); } catch (_) { projectId = null; } } if (!/^prj_[A-Za-z0-9_.:-]{3,96}$/.test(projectId || "")) projectId = "prj_hwpod_workbench"; - window.HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP = fetch("/auth/workspace-bootstrap?projectId=" + encodeURIComponent(projectId), { + window.HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP = fetch("/v1/workbench/workspace?projectId=" + encodeURIComponent(projectId), { credentials: "same-origin", headers: { accept: "application/json" } }).then(function (response) { diff --git a/web/hwlab-cloud-web/scripts/check.ts b/web/hwlab-cloud-web/scripts/check.ts index d0a2dafa..40334ebd 100644 --- a/web/hwlab-cloud-web/scripts/check.ts +++ b/web/hwlab-cloud-web/scripts/check.ts @@ -61,6 +61,8 @@ const appSource = readCloudWebAppSource(rootDir); assert.match(html, /
<\/div>/u, "index.html must expose Vue mount root"); assert.match(html, /src="\/src\/main\.ts"/u, "index.html must load Vue TS entry"); assert.match(html, /HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP/u, "workspace bootstrap must still start during HTML parse"); +assert.match(html, /\/v1\/workbench\/workspace/u, "HTML parse bootstrap must use the same-origin workbench API"); +assert.doesNotMatch(html, /\/auth\/workspace-bootstrap/u, "HTML parse bootstrap must not use stale auth workspace bootstrap route"); assert.doesNotMatch(html, /src="\/src\/main\.tsx"|id="root"/u, "React entry must be gone"); for (const dep of ["vue", "pinia", "vue-router", "axios", "@vueuse/core", "vue-i18n"]) { @@ -93,6 +95,8 @@ assertIncludes(appSource, "activityRef: () => activityRef.value", "Workbench mus assertIncludes(appSource, "getAgentChatResult(resultUrl, inactivityTimeoutMs, activityRef)", "Trace result polling must use activityRef-backed inactivity timeout"); assertIncludes(appSource, "for (;;) ", "trace subscription must keep unbounded polling without total timeout"); assertIncludes(appSource, "/v1/agent/chat/trace/", "trace replay must use Cloud Web same-origin trace API"); +assertIncludes(appSource, "/v1/workbench/workspace", "workspace bootstrap must use Cloud Web same-origin workbench API"); +assert.doesNotMatch(appSource, /\/auth\/workspace-bootstrap/u, "stale auth workspace bootstrap route must not remain in Cloud Web app source"); assertIncludes(appSource, "trace-meta-details", "Trace identity and raw-event controls must stay behind a compact details disclosure"); assertIncludes(appSource, "grid-template-columns: minmax(0, 1fr);", "Trace rows must give the readable body the full content width"); assertIncludes(appSource, "const primaryAction = computed", "Workbench composer must derive a single primary send/cancel/steer action"); diff --git a/web/hwlab-cloud-web/src/api/auth.ts b/web/hwlab-cloud-web/src/api/auth.ts index 452d5112..692732c7 100644 --- a/web/hwlab-cloud-web/src/api/auth.ts +++ b/web/hwlab-cloud-web/src/api/auth.ts @@ -11,7 +11,6 @@ export interface WorkspaceBootstrapResponse extends AuthSession { export const authAPI = { session: (): Promise> => fetchJson("/auth/session", { timeoutName: "auth session" }), bootstrap: (): Promise> => fetchJson("/auth/bootstrap", { timeoutName: "auth bootstrap" }), - workspaceBootstrap: (projectId: string): Promise> => fetchJson(`/auth/workspace-bootstrap?projectId=${encodeURIComponent(projectId)}`, { timeoutName: "workspace bootstrap", timeoutMs: 8000 }), register: (payload: { email: string; username: string; displayName?: string; password: string }): Promise> => fetchJson("/auth/register", { method: "POST", body: JSON.stringify(payload), diff --git a/web/hwlab-cloud-web/src/composables/useWorkspaceBootstrap.ts b/web/hwlab-cloud-web/src/composables/useWorkspaceBootstrap.ts index 0eeacdc5..0b9a1ab0 100644 --- a/web/hwlab-cloud-web/src/composables/useWorkspaceBootstrap.ts +++ b/web/hwlab-cloud-web/src/composables/useWorkspaceBootstrap.ts @@ -19,32 +19,30 @@ export async function consumeEarlyWorkspaceBootstrap(projectId: string): Promise export async function resolveWorkspaceBootstrap(projectId: string): Promise> { const normalizedProjectId = normalizeWorkbenchProjectId(projectId) ?? DEFAULT_WORKBENCH_PROJECT_ID; - const early = await consumeEarlyWorkspaceBootstrap(normalizedProjectId); - if (early && (early.ok || early.status !== 404)) return early; - const primary = await authAPI.workspaceBootstrap(normalizedProjectId); - if (primary.ok || primary.status !== 404) return primary; - return resolveWorkspaceBootstrapFallback(normalizedProjectId, primary); + const earlyWorkspace = await consumeEarlyWorkspaceBootstrap(normalizedProjectId); + return resolveWorkspaceBootstrapFromCurrentApi(normalizedProjectId, earlyWorkspace); } -async function resolveWorkspaceBootstrapFallback(projectId: string, primary: ApiResult): Promise> { +async function resolveWorkspaceBootstrapFromCurrentApi(projectId: string, earlyWorkspace: ApiResult | null): Promise> { const session = await authAPI.session(); const sessionPayload = session.data ?? { authenticated: false, mode: "server" as const }; if (!session.ok || sessionPayload.authenticated !== true) { return { ok: session.ok, status: session.status, - data: { ...sessionPayload, projectId, workspace: null, workspaceStatus: null, workspaceError: primary.error ?? primary.status }, + data: { ...sessionPayload, projectId, workspace: null, workspaceStatus: earlyWorkspace?.status ?? null, workspaceError: session.error ?? session.status }, error: session.error }; } - const workspace = await workbenchAPI.workspace(projectId); + const workspace = earlyWorkspace?.ok ? earlyWorkspace : await workbenchAPI.workspace(projectId); + const workspacePayload = workspace.ok ? asRecord(workspace.data) : null; return { ok: true, status: session.status, data: { ...sessionPayload, projectId, - workspace: workspace.ok ? workspace.data?.workspace ?? null : null, + workspace: (workspacePayload?.workspace as WorkspaceRecord | null | undefined) ?? null, workspaceStatus: workspace.status, workspaceError: workspace.ok ? null : workspace.error ?? workspace.status },