From a0f76f5a3d55c16a8b40340ee441e78aa0c626e0 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 5 Jun 2026 17:43:48 +0800 Subject: [PATCH] fix: proxy provider profile management routes --- internal/dev-entrypoint/cloud-web-routes.mjs | 10 +++ .../dev-entrypoint/cloud-web-routes.test.mjs | 21 ++++++ .../dev-entrypoint/cloud-web-runtime.test.mjs | 74 +++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/internal/dev-entrypoint/cloud-web-routes.mjs b/internal/dev-entrypoint/cloud-web-routes.mjs index 840fa418..9fb8faa9 100644 --- a/internal/dev-entrypoint/cloud-web-routes.mjs +++ b/internal/dev-entrypoint/cloud-web-routes.mjs @@ -52,6 +52,7 @@ export function isCloudApiProxyRoute(method, pathname) { if (normalizedMethod === "POST") { return POST_PROXY_ROUTES.has(normalizedPath) || isAdminAccessWriteProxyRoute(normalizedMethod, normalizedPath) || + isProviderProfileManagementProxyRoute(normalizedMethod, normalizedPath) || isApiKeyWriteProxyRoute(normalizedMethod, normalizedPath) || normalizedPath.startsWith("/v1/rpc/") || normalizedPath.startsWith("/v1/agent/sessions/") || @@ -59,6 +60,7 @@ export function isCloudApiProxyRoute(method, pathname) { } if (normalizedMethod === "PATCH" || normalizedMethod === "PUT") { return isAdminAccessWriteProxyRoute(normalizedMethod, normalizedPath) || + isProviderProfileManagementProxyRoute(normalizedMethod, normalizedPath) || isWorkbenchWorkspaceWriteProxyRoute(normalizedMethod, normalizedPath) || isAgentConversationWriteProxyRoute(normalizedMethod, normalizedPath); } @@ -81,6 +83,14 @@ function isAdminAccessWriteProxyRoute(method, pathname) { return false; } +function isProviderProfileManagementProxyRoute(method, pathname) { + const parts = pathname.split("/").filter(Boolean); + if (parts.length < 4 || parts[0] !== "v1" || parts[1] !== "admin" || parts[2] !== "provider-profiles") return false; + if (method === "PUT" && parts.length === 5 && parts[4] === "credential") return true; + if (method === "POST" && parts.length === 5 && parts[4] === "validate") return true; + return false; +} + function isApiKeyWriteProxyRoute(method, pathname) { if (pathname === "/v1/api-keys" && method === "POST") return true; if (!pathname.startsWith("/v1/api-keys/")) return false; diff --git a/internal/dev-entrypoint/cloud-web-routes.test.mjs b/internal/dev-entrypoint/cloud-web-routes.test.mjs index 25d98634..b84d8f1d 100644 --- a/internal/dev-entrypoint/cloud-web-routes.test.mjs +++ b/internal/dev-entrypoint/cloud-web-routes.test.mjs @@ -117,6 +117,27 @@ test("cloud web proxies authenticated Admin Access write routes", () => { }); }); +test("cloud web proxies authenticated provider profile management write routes", () => { + assert.deepEqual(cloudWebProxyRoutePolicy("PUT", "/v1/admin/provider-profiles/deepseek/credential"), { + proxy: true, + authRequired: true, + publicRoute: false, + routeKey: "PUT /v1/admin/provider-profiles/deepseek/credential" + }); + assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/admin/provider-profiles/deepseek/validate"), { + proxy: true, + authRequired: true, + publicRoute: false, + routeKey: "POST /v1/admin/provider-profiles/deepseek/validate" + }); + assert.deepEqual(cloudWebProxyRoutePolicy("DELETE", "/v1/admin/provider-profiles/deepseek/credential"), { + proxy: false, + authRequired: false, + publicRoute: false, + routeKey: "DELETE /v1/admin/provider-profiles/deepseek/credential" + }); +}); + test("cloud web proxies authenticated hwpod node-ops route", () => { assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/hwpod-node-ops"), { proxy: true, diff --git a/internal/dev-entrypoint/cloud-web-runtime.test.mjs b/internal/dev-entrypoint/cloud-web-runtime.test.mjs index 477c767b..35bef532 100644 --- a/internal/dev-entrypoint/cloud-web-runtime.test.mjs +++ b/internal/dev-entrypoint/cloud-web-runtime.test.mjs @@ -125,6 +125,80 @@ test("cloud web proxies Admin Access write routes", async () => { } }); +test("cloud web proxies provider profile management write routes", async () => { + const upstreamRequests = []; + const upstream = createServer(async (request, response) => { + let body = ""; + for await (const chunk of request) body += chunk; + upstreamRequests.push({ + method: request.method, + url: request.url, + authorization: request.headers.authorization, + body + }); + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({ ok: true, path: request.url })); + }); + await listen(upstream); + + const cloudWeb = createCloudWebServer({ + serviceId: "hwlab-cloud-web", + cloudApiBaseUrl: serverUrl(upstream), + cloudApiProxyTimeoutMs: 1000, + healthPayload: () => ({ status: "ok" }), + roots: [], + sendJson(response, statusCode, body) { + const payload = JSON.stringify(body); + response.writeHead(statusCode, { + "content-type": "application/json", + "content-length": Buffer.byteLength(payload) + }); + response.end(payload); + } + }); + await listen(cloudWeb); + + try { + const credentialBody = JSON.stringify({ apiKey: "sk-test-redacted" }); + const credentialResponse = await fetch(`${serverUrl(cloudWeb)}/v1/admin/provider-profiles/deepseek/credential`, { + method: "PUT", + headers: { + "content-type": "application/json", + authorization: "Bearer hwl_live_admin" + }, + body: credentialBody + }); + assert.equal(credentialResponse.status, 200); + + const validateResponse = await fetch(`${serverUrl(cloudWeb)}/v1/admin/provider-profiles/deepseek/validate`, { + method: "POST", + headers: { + "content-type": "application/json", + authorization: "Bearer hwl_live_admin" + }, + body: "{}" + }); + assert.equal(validateResponse.status, 200); + + assert.equal(upstreamRequests.length, 2); + assert.deepEqual(upstreamRequests[0], { + method: "PUT", + url: "/v1/admin/provider-profiles/deepseek/credential", + authorization: "Bearer hwl_live_admin", + body: credentialBody + }); + assert.deepEqual(upstreamRequests[1], { + method: "POST", + url: "/v1/admin/provider-profiles/deepseek/validate", + authorization: "Bearer hwl_live_admin", + body: "{}" + }); + } finally { + await close(cloudWeb); + await close(upstream); + } +}); + test("cloud web forwards hwpod node-ops plans to cloud-api", async () => { const upstreamRequests = []; const upstream = createServer(async (request, response) => {