From f6357811b03d279b15f6b698f287a15c83b85cfd Mon Sep 17 00:00:00 2001 From: root Date: Sat, 18 Jul 2026 15:01:18 +0200 Subject: [PATCH] fix: align workbench cloud auth validation --- internal/cloud/server.ts | 3 ++- internal/cloud/workbench-command-proxy.test.ts | 7 +++++++ internal/workbench/cloud-application.ts | 6 +++--- internal/workbench/cloud-authorization.ts | 5 +++++ 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 internal/workbench/cloud-authorization.ts diff --git a/internal/cloud/server.ts b/internal/cloud/server.ts index c1acc0ef..1a2080cb 100644 --- a/internal/cloud/server.ts +++ b/internal/cloud/server.ts @@ -101,6 +101,7 @@ import { handleTaskTreeProxyHttp } from "./tasktree-proxy.ts"; import { handleDashboardSummaryHttp } from "./server-dashboard-http.ts"; import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts"; import { proxyWorkbenchCommandHttp, workbenchActivityDispatchRequested, workbenchTemporalProxyEnabled } from "./workbench-command-proxy.ts"; +import { normalizeWorkbenchCloudAuthorization } from "../workbench/cloud-authorization.ts"; const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024; const HWLAB_WEB_SESSION_COOKIE = "hwlab_session"; @@ -1078,7 +1079,7 @@ function navIdForRestPath(pathname, method = "GET") { async function codeAgentOptions(request, response, options, authOptions = {}) { if (workbenchActivityDispatchRequested(request)) { - const expected = String(options.env?.WORKBENCH_CLOUD_API_AUTHORIZATION ?? "").trim(); + const expected = normalizeWorkbenchCloudAuthorization(String(options.env?.WORKBENCH_CLOUD_API_AUTHORIZATION ?? "")); const actual = String(request.headers?.authorization ?? "").trim(); const actorId = String(request.headers?.["x-hwlab-actor-id"] ?? "").trim(); if (!expected || actual !== expected || !actorId) { diff --git a/internal/cloud/workbench-command-proxy.test.ts b/internal/cloud/workbench-command-proxy.test.ts index 298c66e3..5f09d649 100644 --- a/internal/cloud/workbench-command-proxy.test.ts +++ b/internal/cloud/workbench-command-proxy.test.ts @@ -1,9 +1,16 @@ import { expect, test } from "bun:test"; import { workbenchActivityDispatchRequested } from "./workbench-command-proxy.ts"; +import { normalizeWorkbenchCloudAuthorization } from "../workbench/cloud-authorization.ts"; test("accepts activity dispatch header from Node and Fetch request shapes", () => { expect(workbenchActivityDispatchRequested({ headers: { "x-workbench-activity-dispatch": "1" } })).toBe(true); expect(workbenchActivityDispatchRequested({ headers: new Headers({ "x-workbench-activity-dispatch": "1" }) })).toBe(true); expect(workbenchActivityDispatchRequested({ headers: new Headers({ "x-workbench-activity-dispatch": "0" }) })).toBe(false); }); + +test("normalizes the unified Workbench Cloud authorization on both sides", () => { + expect(normalizeWorkbenchCloudAuthorization(" unified-internal-key ")).toBe("Bearer unified-internal-key"); + expect(normalizeWorkbenchCloudAuthorization("Bearer unified-internal-key")).toBe("Bearer unified-internal-key"); + expect(normalizeWorkbenchCloudAuthorization(" ")).toBe(""); +}); diff --git a/internal/workbench/cloud-application.ts b/internal/workbench/cloud-application.ts index 61f9e142..d3005130 100644 --- a/internal/workbench/cloud-application.ts +++ b/internal/workbench/cloud-application.ts @@ -1,4 +1,5 @@ import type { WorkbenchApplication } from "./contracts.ts"; +import { normalizeWorkbenchCloudAuthorization } from "./cloud-authorization.ts"; export function createCloudWorkbenchApplication(options: { baseUrl: string; @@ -6,15 +7,14 @@ export function createCloudWorkbenchApplication(options: { fetchImpl?: typeof fetch; }): WorkbenchApplication { if (!options.baseUrl) throw codedError("workbench_cloud_api_url_required", "WORKBENCH_CLOUD_API_URL is required"); - const authorization = options.authorization.trim(); + const authorization = normalizeWorkbenchCloudAuthorization(options.authorization); if (!authorization) throw codedError("workbench_cloud_api_auth_required", "WORKBENCH_CLOUD_API_AUTHORIZATION is required"); - const authorizationHeader = /^Bearer\s+/iu.test(authorization) ? authorization : `Bearer ${authorization}`; const fetchImpl = options.fetchImpl ?? fetch; const request = async (method: string, route: string, body: Record | undefined, actor: { id: string; role?: string | null }) => { const response = await fetchImpl(`${options.baseUrl.replace(/\/$/u, "")}${route}`, { method, headers: { - authorization: authorizationHeader, + authorization, "content-type": "application/json", "x-workbench-activity-dispatch": "1", "x-hwlab-actor-id": actor.id, diff --git a/internal/workbench/cloud-authorization.ts b/internal/workbench/cloud-authorization.ts new file mode 100644 index 00000000..1b8221bf --- /dev/null +++ b/internal/workbench/cloud-authorization.ts @@ -0,0 +1,5 @@ +export function normalizeWorkbenchCloudAuthorization(value: string) { + const authorization = value.trim(); + if (!authorization) return ""; + return /^Bearer\s+/iu.test(authorization) ? authorization : `Bearer ${authorization}`; +}