openclaw/src/gateway/control-ui-routing.ts

52 lines
1.7 KiB
TypeScript

import { isReadHttpMethod } from "./control-ui-http-utils.js";
export type ControlUiRequestClassification =
| { kind: "not-control-ui" }
| { kind: "not-found" }
| { kind: "redirect"; location: string }
| { kind: "serve" };
const ROOT_MOUNTED_GATEWAY_PROBE_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz"]);
export function classifyControlUiRequest(params: {
basePath: string;
pathname: string;
search: string;
method: string | undefined;
}): ControlUiRequestClassification {
const { basePath, pathname, search, method } = params;
if (!basePath) {
if (pathname === "/ui" || pathname.startsWith("/ui/")) {
return { kind: "not-found" };
}
// Keep core probe routes outside the root-mounted SPA catch-all so the
// gateway probe handler can answer them even when the Control UI owns `/`.
if (ROOT_MOUNTED_GATEWAY_PROBE_PATHS.has(pathname)) {
return { kind: "not-control-ui" };
}
// Keep plugin-owned HTTP routes outside the root-mounted Control UI SPA
// fallback so untrusted plugins cannot claim arbitrary UI paths.
if (pathname === "/plugins" || pathname.startsWith("/plugins/")) {
return { kind: "not-control-ui" };
}
if (pathname === "/api" || pathname.startsWith("/api/")) {
return { kind: "not-control-ui" };
}
if (!isReadHttpMethod(method)) {
return { kind: "not-control-ui" };
}
return { kind: "serve" };
}
if (!pathname.startsWith(`${basePath}/`) && pathname !== basePath) {
return { kind: "not-control-ui" };
}
if (!isReadHttpMethod(method)) {
return { kind: "not-control-ui" };
}
if (pathname === basePath) {
return { kind: "redirect", location: `${basePath}/${search}` };
}
return { kind: "serve" };
}