fix: align cloud web workbench bootstrap entry
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user