From 1a6dd66a97ba371c47851686bafb450fc69d8445 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:32:14 +0800 Subject: [PATCH] fix: proxy project management write resources (#2167) --- .../cloud/project-management-proxy.test.ts | 37 +++++++++++++++++++ internal/cloud/project-management-proxy.ts | 15 ++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 internal/cloud/project-management-proxy.test.ts diff --git a/internal/cloud/project-management-proxy.test.ts b/internal/cloud/project-management-proxy.test.ts new file mode 100644 index 00000000..eb038370 --- /dev/null +++ b/internal/cloud/project-management-proxy.test.ts @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import { test } from "bun:test"; + +import { isAllowedProjectManagementProxyMethod } from "./project-management-proxy.ts"; + +test("project management proxy forwards public MDTODO write resources", () => { + const allowed = [ + ["GET", "/v1/project-management/mdtodo/sources"], + ["HEAD", "/v1/project-management/mdtodo/tasks"], + ["POST", "/v1/project-management/workbench-links"], + ["POST", "/v1/project-management/mdtodo/sources"], + ["PATCH", "/v1/project-management/mdtodo/sources/d601-f103-v2-mdtodo"], + ["DELETE", "/v1/project-management/mdtodo/sources/d601-f103-v2-mdtodo"], + ["POST", "/v1/project-management/mdtodo/sources/d601-f103-v2-mdtodo/probe"], + ["POST", "/v1/project-management/mdtodo/sources/d601-f103-v2-mdtodo/reindex"], + ["PATCH", "/v1/project-management/mdtodo/files/file_demo/content"], + ["POST", "/v1/project-management/mdtodo/tasks"], + ["PATCH", "/v1/project-management/mdtodo/tasks/mdtodo%3Asource%3Afile%3AR1.1"], + ["DELETE", "/v1/project-management/mdtodo/tasks/mdtodo%3Asource%3Afile%3AR1.1"] + ]; + for (const [method, pathname] of allowed) { + assert.equal(isAllowedProjectManagementProxyMethod(method, pathname), true, `${method} ${pathname}`); + } +}); + +test("project management proxy keeps non-contract writes blocked", () => { + const blocked = [ + ["PUT", "/v1/project-management/mdtodo/sources/d601-f103-v2-mdtodo"], + ["POST", "/v1/project-management/mdtodo/sources/d601-f103-v2-mdtodo/delete"], + ["POST", "/v1/project-management/mdtodo/files/file_demo/content"], + ["PATCH", "/v1/project-management/mdtodo/tasks"], + ["POST", "/v1/project-management/internal/rebuild"] + ]; + for (const [method, pathname] of blocked) { + assert.equal(isAllowedProjectManagementProxyMethod(method, pathname), false, `${method} ${pathname}`); + } +}); diff --git a/internal/cloud/project-management-proxy.ts b/internal/cloud/project-management-proxy.ts index f5f8451e..27f82190 100644 --- a/internal/cloud/project-management-proxy.ts +++ b/internal/cloud/project-management-proxy.ts @@ -17,8 +17,8 @@ export async function handleProjectManagementProxyHttp(request, response, url, o return; } - if (!isAllowedProjectManagementMethod(request.method, url.pathname)) { - sendJson(response, 405, { ok: false, error: { code: "method_not_allowed", message: "Project management API only accepts read requests plus Workbench link writes" } }); + if (!isAllowedProjectManagementProxyMethod(request.method, url.pathname)) { + sendJson(response, 405, { ok: false, error: { code: "method_not_allowed", message: "Project management API method/path is not part of the public Project Management proxy contract" } }); return; } @@ -69,9 +69,16 @@ export async function handleProjectManagementProxyHttp(request, response, url, o } } -function isAllowedProjectManagementMethod(method, pathname) { +export function isAllowedProjectManagementProxyMethod(method, pathname) { if (method === "GET" || method === "HEAD") return true; - return method === "POST" && pathname === "/v1/project-management/workbench-links"; + if (method === "POST" && pathname === "/v1/project-management/workbench-links") return true; + if (method === "POST" && pathname === "/v1/project-management/mdtodo/sources") return true; + if ((method === "PATCH" || method === "DELETE") && /^\/v1\/project-management\/mdtodo\/sources\/[^/]+$/u.test(pathname)) return true; + if (method === "POST" && /^\/v1\/project-management\/mdtodo\/sources\/[^/]+\/(?:probe|reindex)$/u.test(pathname)) return true; + if (method === "PATCH" && /^\/v1\/project-management\/mdtodo\/files\/[^/]+\/content$/u.test(pathname)) return true; + if (method === "POST" && pathname === "/v1/project-management/mdtodo/tasks") return true; + if ((method === "PATCH" || method === "DELETE") && /^\/v1\/project-management\/mdtodo\/tasks\/[^/]+$/u.test(pathname)) return true; + return false; } function bridgeHeaders(request, auth) {