fix: align cloud web workbench bootstrap entry

This commit is contained in:
lyon
2026-06-15 15:42:05 +08:00
parent 2c6574e625
commit e984074de0
7 changed files with 13 additions and 60 deletions
+1 -2
View File
@@ -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",
@@ -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,
@@ -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;
+1 -1
View File
@@ -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) {
+4
View File
@@ -61,6 +61,8 @@ const appSource = readCloudWebAppSource(rootDir);
assert.match(html, /<div id="app"><\/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");
-1
View File
@@ -11,7 +11,6 @@ export interface WorkspaceBootstrapResponse extends AuthSession {
export const authAPI = {
session: (): Promise<ApiResult<AuthSession>> => fetchJson("/auth/session", { timeoutName: "auth session" }),
bootstrap: (): Promise<ApiResult<AuthSession>> => fetchJson("/auth/bootstrap", { timeoutName: "auth bootstrap" }),
workspaceBootstrap: (projectId: string): Promise<ApiResult<WorkspaceBootstrapResponse>> => fetchJson(`/auth/workspace-bootstrap?projectId=${encodeURIComponent(projectId)}`, { timeoutName: "workspace bootstrap", timeoutMs: 8000 }),
register: (payload: { email: string; username: string; displayName?: string; password: string }): Promise<ApiResult<AuthSession & { initialCredits?: number | null }>> => fetchJson("/auth/register", {
method: "POST",
body: JSON.stringify(payload),
@@ -19,32 +19,30 @@ export async function consumeEarlyWorkspaceBootstrap(projectId: string): Promise
export async function resolveWorkspaceBootstrap(projectId: string): Promise<ApiResult<WorkspaceBootstrapResponse>> {
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<WorkspaceBootstrapResponse>): Promise<ApiResult<WorkspaceBootstrapResponse>> {
async function resolveWorkspaceBootstrapFromCurrentApi(projectId: string, earlyWorkspace: ApiResult<WorkspaceBootstrapResponse> | null): Promise<ApiResult<WorkspaceBootstrapResponse>> {
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
},