fix(confidence-engine): sanitize public health response

This commit is contained in:
2026-09-09 10:33:54 +01:00
parent f557175bfb
commit 0125975ccb
2 changed files with 29 additions and 30 deletions
+11 -27
View File
@@ -3,47 +3,31 @@ import { getConfig } from "@/lib/config";
export async function GET() { export async function GET() {
try { try {
const result = getConfig(); const result = getConfig();
if (!result.ok) { if (!result.ok) {
return Response.json({ return Response.json({ healthy: false }, { status: 500 });
configPresent: false,
baseUrl: null,
model: null,
reachable: false,
error: "Missing or invalid environment configuration",
}, { status: 500 });
} }
const { OLLAMA_BASE_URL, OLLAMA_MODEL } = result.config; const { OLLAMA_BASE_URL, OLLAMA_MODEL } = result.config;
// Test reachability with a short timeout // Test reachability with a short timeout
let reachable = false; let reachable = false;
let reachError = null;
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000); const timeout = setTimeout(() => controller.abort(), 3000);
const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, { const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, {
signal: controller.signal signal: controller.signal
}); });
clearTimeout(timeout); clearTimeout(timeout);
reachable = res.ok; reachable = res.ok;
} catch (e) { } catch {
reachError = e.message || "Connection failed"; reachable = false;
} }
return Response.json({ return Response.json({ healthy: reachable }, { status: reachable ? 200 : 503 });
configPresent: true, } catch {
baseUrl: OLLAMA_BASE_URL, return Response.json({ healthy: false }, { status: 500 });
model: OLLAMA_MODEL,
reachable,
error: reachable ? null : (`Could not reach Ollama at ${OLLAMA_BASE_URL}: ${reachError || "timeout"}`),
});
} catch (e) {
return Response.json(
{ configPresent: false, error: e.message },
{ status: 500 }
);
} }
} }
+18 -3
View File
@@ -58,15 +58,30 @@ describe("authenticated product boundary", () => {
expect(magicLinkRedirectTo("http://localhost:3000")).toBe("http://localhost:3000/auth/callback"); expect(magicLinkRedirectTo("http://localhost:3000")).toBe("http://localhost:3000/auth/callback");
}); });
it("keeps infrastructure health public", async () => { it("keeps infrastructure health public and does not leak config details", async () => {
mockGetConfig.mockReturnValue({ ok: true, config: {} });
const { GET } = await import("@/app/api/health/route.js");
const response = await GET();
expect(mockGetAuthenticatedUser).not.toHaveBeenCalled();
const body = await response.json();
expect(body).toHaveProperty("healthy");
// Must not expose private config details in the public response
expect(JSON.stringify(body)).not.toContain("baseUrl");
expect(JSON.stringify(body)).not.toContain("model");
expect(JSON.stringify(body)).not.toContain("ollama");
});
it("reports unhealthy generic state when config is missing", async () => {
mockGetConfig.mockReturnValue({ ok: false }); mockGetConfig.mockReturnValue({ ok: false });
const { GET } = await import("@/app/api/health/route.js"); const { GET } = await import("@/app/api/health/route.js");
const response = await GET(); const response = await GET();
expect(response.status).toBe(500); expect(response.status).toBe(500);
await expect(response.json()).resolves.toMatchObject({ configPresent: false }); const body = await response.json();
expect(mockGetAuthenticatedUser).not.toHaveBeenCalled(); expect(body).toEqual({ healthy: false });
}); });
it("does not convert /api/health to 401 via middleware when unauthenticated", async () => { it("does not convert /api/health to 401 via middleware when unauthenticated", async () => {