268 lines
11 KiB
TypeScript
268 lines
11 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("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.
|
|
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);
|
|
assert.ok(
|
|
content.includes("const authError = await requireManagementAuth(request);"),
|
|
routePath
|
|
);
|
|
assert.ok(content.includes("if (authError) return authError;"), routePath);
|
|
}
|
|
});
|
|
|
|
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/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;"));
|
|
});
|