mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 17:12:27 +03:00
Validated on the combined batch board over tip 17897cc3: gates clean (changelog, file-size 159 frozen, complexity 2628<=2774, cognitive 1184<=1223, dead-code 409<=416, provider-consistency 267/350/0), typecheck:core clean, 83/83 PR suites + neighbors green (mcp-route-scope-carveout 15 tests with bug-injection proof, management-auth-hardening per-route call-shape pinning). The route layer now honors the #9159 mcp:connect carve-out exactly like the policy layer — audit/audit-stats deliberately stay manage-only, oma_ tokens still require admin. Thank you @HouMinXi!
341 lines
14 KiB
TypeScript
341 lines
14 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
|
|
test("Codex apply-local auth route requires management authentication before local writes", () => {
|
|
const content = fs.readFileSync(
|
|
"src/app/api/providers/[id]/codex-auth/apply-local/route.ts",
|
|
"utf8"
|
|
);
|
|
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
assert.ok(content.includes("const authError = await requireManagementAuth(request);"));
|
|
assert.ok(content.includes("if (authError) return authError;"));
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") <
|
|
content.indexOf("ensureCliConfigWriteAllowed()")
|
|
);
|
|
});
|
|
|
|
test("admin concurrency route requires management authentication before read or reset", () => {
|
|
const content = fs.readFileSync("src/app/api/admin/concurrency/route.ts", "utf8");
|
|
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
assert.ok(content.includes("const authError = await requireManagementAuth(request);"));
|
|
assert.ok(content.includes("if (authError) return authError;"));
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("getAllRateLimitStatus()")
|
|
);
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("resetAllSemaphores()")
|
|
);
|
|
});
|
|
|
|
test("compression analytics route requires management authentication before returning metrics", () => {
|
|
const content = fs.readFileSync("src/app/api/analytics/compression/route.ts", "utf8");
|
|
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
assert.ok(!content.includes("enforceApiKeyPolicy"));
|
|
assert.ok(content.includes("const authError = await requireManagementAuth(req);"));
|
|
assert.ok(content.includes("if (authError) return authError;"));
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(req)") <
|
|
content.indexOf("getCompressionAnalyticsSummary(")
|
|
);
|
|
});
|
|
|
|
test("administrative pricing and routing routes require management authentication", () => {
|
|
const routePaths = [
|
|
"src/app/api/pricing/route.ts",
|
|
"src/app/api/pricing/sync/route.ts",
|
|
"src/app/api/model-combo-mappings/route.ts",
|
|
"src/app/api/model-combo-mappings/[id]/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
test("memory management routes require management authentication", () => {
|
|
const routePaths = ["src/app/api/memory/route.ts", "src/app/api/memory/[id]/route.ts"];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
test("provider validation routes require management authentication before reading credentials", () => {
|
|
const routePaths = [
|
|
"src/app/api/provider-nodes/validate/route.ts",
|
|
"src/app/api/providers/validate/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("request.json()"),
|
|
`${routePath} should authenticate before parsing submitted provider credentials`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("provider param-filters route requires management authentication on GET/PUT/DELETE (#6649)", () => {
|
|
const routePath = "src/app/api/providers/[id]/param-filters/route.ts";
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
|
|
// GET, PUT, and DELETE must each gate on requireManagementAuth() before
|
|
// touching the param filter config.
|
|
const handlers = ["GET", "PUT", "DELETE"];
|
|
for (const handler of handlers) {
|
|
const handlerStart = content.indexOf(`export async function ${handler}(`);
|
|
assert.ok(handlerStart >= 0, `${routePath} is missing a ${handler} handler`);
|
|
const nextHandlerStart = content.indexOf("export async function", handlerStart + 1);
|
|
const handlerBody = content.slice(
|
|
handlerStart,
|
|
nextHandlerStart === -1 ? content.length : nextHandlerStart
|
|
);
|
|
assert.ok(
|
|
handlerBody.includes("const authError = await requireManagementAuth(request);"),
|
|
`${routePath} ${handler} handler must call requireManagementAuth(request)`
|
|
);
|
|
assert.ok(
|
|
handlerBody.includes("if (authError) return authError;"),
|
|
`${routePath} ${handler} handler must short-circuit on authError`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("Antigravity CLI (agy) credential import routes require management authentication before reading the body", () => {
|
|
// Routes that parse a JSON body — auth MUST run before request.json().
|
|
const jsonBodyRoutes = [
|
|
"src/app/api/providers/agy-auth/import/route.ts",
|
|
"src/app/api/providers/agy-auth/import-bulk/route.ts",
|
|
];
|
|
for (const routePath of jsonBodyRoutes) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("request.json()"),
|
|
`${routePath} should authenticate before parsing the submitted token`
|
|
);
|
|
}
|
|
// Routes that read non-JSON bodies (local file / uploaded ZIP) — auth still comes first.
|
|
const otherBodyRoutes = [
|
|
"src/app/api/providers/agy-auth/apply-local/route.ts",
|
|
"src/app/api/providers/agy-auth/zip-extract/route.ts",
|
|
];
|
|
for (const routePath of otherBodyRoutes) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
test("usage analytics and request log routes require management authentication", () => {
|
|
const routePaths = [
|
|
"src/app/api/usage/analytics/route.ts",
|
|
"src/app/api/usage/history/route.ts",
|
|
"src/app/api/usage/request-logs/route.ts",
|
|
"src/app/api/usage/logs/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
test("middleware hook registry routes require management authentication before storing or running code", () => {
|
|
// /api/middleware/hooks accepts arbitrary JS code that is compiled with
|
|
// `new Function` in `src/lib/middleware/registry.ts` and executed on every
|
|
// chat-completion request via `runHooks()`. Without management auth, anyone
|
|
// who can reach the server can register a hook and achieve RCE. The
|
|
// centralized authz pipeline (src/proxy.ts) is dead code in production
|
|
// because Next.js requires the file to be named middleware.ts (see commit
|
|
// 3fb72b973), so each route must self-gate.
|
|
const routePaths = [
|
|
"src/app/api/middleware/hooks/route.ts",
|
|
"src/app/api/middleware/hooks/[name]/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
assert.ok(
|
|
content.indexOf("requireManagementAuth(request)") < content.indexOf("registerHook(saved"),
|
|
`${routePath} must authenticate before calling registerHook (RCE surface)`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("MCP transport and inspection routes require management authentication", () => {
|
|
// /api/mcp/* is classified as LOCAL_ONLY in routeGuard.ts, but that
|
|
// classification only takes effect inside runAuthzPipeline, which is not
|
|
// invoked by Next.js in production (src/proxy.ts vs middleware.ts). Each
|
|
// route must self-enforce. requireManagementAuth covers: CLI machine
|
|
// token (loopback), dashboard session cookie, and manage-scope API key —
|
|
// matching the documented bypasses in LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES.
|
|
// The transport routes (stream/sse/status/tools) pass
|
|
// { acceptMcpConnectScope: true }, the route-layer half of the #9159
|
|
// mcp:connect carve-out; audit/audit/stats stay manage-only.
|
|
const routePaths = [
|
|
"src/app/api/mcp/status/route.ts",
|
|
"src/app/api/mcp/tools/route.ts",
|
|
"src/app/api/mcp/audit/route.ts",
|
|
"src/app/api/mcp/audit/stats/route.ts",
|
|
"src/app/api/mcp/sse/route.ts",
|
|
"src/app/api/mcp/stream/route.ts",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'), routePath);
|
|
// Transport routes must use the carve-out form; audit routes must use
|
|
// the bare form. The per-route shape is pinned exactly — reverting a
|
|
// transport route to the bare call fails here.
|
|
const isAudit = routePath.includes("/audit");
|
|
const hasOption = content.includes(
|
|
"const authError = await requireManagementAuth(request, { acceptMcpConnectScope: true });"
|
|
);
|
|
// Audit routes must use the BARE call — any second argument at all
|
|
// (even an unrelated option) widens their auth surface.
|
|
const hasAnyOption = /requireManagementAuth\(request,\s*\{/.test(content);
|
|
const hasBare = content.includes("const authError = await requireManagementAuth(request);");
|
|
assert.equal(
|
|
hasOption,
|
|
!isAudit,
|
|
`${routePath} carve-out shape mismatch (hasOption=${hasOption}, audit=${isAudit})`
|
|
);
|
|
if (isAudit) {
|
|
assert.ok(hasBare, `${routePath} must keep the bare guard call`);
|
|
assert.ok(!hasAnyOption, `${routePath} must not pass ANY options to the guard (manage-only)`);
|
|
}
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
|
|
// Carve-out hygiene: only the four transport routes may accept
|
|
// mcp:connect; the audit inspection routes remain manage-only.
|
|
for (const routePath of [
|
|
"src/app/api/mcp/audit/route.ts",
|
|
"src/app/api/mcp/audit/stats/route.ts",
|
|
]) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(
|
|
!content.includes("acceptMcpConnectScope"),
|
|
`${routePath} must stay manage-only (no mcp:connect carve-out)`
|
|
);
|
|
}
|
|
for (const routePath of [
|
|
"src/app/api/mcp/status/route.ts",
|
|
"src/app/api/mcp/tools/route.ts",
|
|
"src/app/api/mcp/sse/route.ts",
|
|
"src/app/api/mcp/stream/route.ts",
|
|
]) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(
|
|
content.includes("acceptMcpConnectScope: true"),
|
|
`${routePath} must enable the mcp:connect carve-out`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("management routes sanitize error.message before returning it to clients", () => {
|
|
// Hard Rule #12 (docs/security/ERROR_SANITIZATION.md): HTTP responses must
|
|
// route every err.message/err.stack through sanitizeErrorMessage() or
|
|
// buildErrorBody(). Raw error.message can leak absolute paths, stack
|
|
// traces, or upstream provider internals into client-visible JSON bodies.
|
|
const routePaths = [
|
|
"src/app/api/analytics/diversity/route.ts",
|
|
"src/app/api/db-backups/export/route.ts",
|
|
"src/app/api/db-backups/import/route.ts",
|
|
"src/app/api/db-backups/route.ts",
|
|
"src/app/api/evals/route.ts",
|
|
"src/app/api/evals/[suiteId]/route.ts",
|
|
"src/app/api/providers/[id]/models/route.ts",
|
|
"src/app/api/providers/[id]/sync-models/route.ts",
|
|
"src/app/api/providers/[id]/param-filters/route.ts",
|
|
"src/app/api/sessions/route.ts",
|
|
"src/app/api/storage/health/route.ts",
|
|
"src/app/api/sync/cloud/route.ts",
|
|
"src/app/api/telemetry/summary/route.ts",
|
|
"src/app/api/translator/history/route.ts",
|
|
"src/app/api/cache/reasoning/route.ts",
|
|
"src/app/api/cache/route.ts",
|
|
"src/app/api/models/test/route.ts",
|
|
"src/app/api/settings/proxy/test/route.ts",
|
|
];
|
|
|
|
// The pattern we forbid is `error.message` (or `e.message`/`err.message`) appearing
|
|
// inside a JSON response body or being returned directly from a helper. We require
|
|
// these files to either:
|
|
// (a) import sanitizeErrorMessage / buildErrorBody from open-sse/utils/error, OR
|
|
// (b) import createErrorResponse from @/lib/api/errorResponse (which already wraps
|
|
// message through structured envelope).
|
|
const sanitizerHints = [
|
|
'from "@omniroute/open-sse/utils/error"',
|
|
'from "@/lib/api/errorResponse"',
|
|
"sanitizeErrorMessage(",
|
|
"buildErrorBody(",
|
|
];
|
|
|
|
for (const routePath of routePaths) {
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
const hasSanitizer = sanitizerHints.some((hint) => content.includes(hint));
|
|
assert.ok(
|
|
hasSanitizer,
|
|
`${routePath} must route error.message through sanitizeErrorMessage() or buildErrorBody() — Hard Rule #12`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("memory health endpoint requires management authentication", () => {
|
|
// verifyExtractionPipeline() exposes the memory subsystem state (Qdrant
|
|
// reachability, DB error paths). Same precedent as /api/db/health and
|
|
// /api/monitoring/health, which require management auth.
|
|
const routePath = "src/app/api/memory/health/route.ts";
|
|
const content = fs.readFileSync(routePath, "utf8");
|
|
assert.ok(content.includes('from "@/lib/api/requireManagementAuth"'));
|
|
assert.ok(content.includes("const authError = await requireManagementAuth(request);"));
|
|
assert.ok(content.includes("if (authError) return authError;"));
|
|
});
|