From 0125975ccb585c33a8e3f0c8fb5b7a6240577209 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 9 Sep 2026 10:33:54 +0100 Subject: [PATCH] fix(confidence-engine): sanitize public health response --- app/api/health/route.js | 38 +++++++++++-------------------------- tests/auth-boundary.test.js | 21 +++++++++++++++++--- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/app/api/health/route.js b/app/api/health/route.js index 04a63d6..84b0190 100644 --- a/app/api/health/route.js +++ b/app/api/health/route.js @@ -3,47 +3,31 @@ import { getConfig } from "@/lib/config"; export async function GET() { try { const result = getConfig(); - + if (!result.ok) { - return Response.json({ - configPresent: false, - baseUrl: null, - model: null, - reachable: false, - error: "Missing or invalid environment configuration", - }, { status: 500 }); + return Response.json({ healthy: false }, { status: 500 }); } const { OLLAMA_BASE_URL, OLLAMA_MODEL } = result.config; // Test reachability with a short timeout let reachable = false; - let reachError = null; - + try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); - - const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, { - signal: controller.signal + + const res = await fetch(`${OLLAMA_BASE_URL}/api/tags`, { + signal: controller.signal }); clearTimeout(timeout); reachable = res.ok; - } catch (e) { - reachError = e.message || "Connection failed"; + } catch { + reachable = false; } - return Response.json({ - configPresent: true, - baseUrl: OLLAMA_BASE_URL, - 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 } - ); + return Response.json({ healthy: reachable }, { status: reachable ? 200 : 503 }); + } catch { + return Response.json({ healthy: false }, { status: 500 }); } } diff --git a/tests/auth-boundary.test.js b/tests/auth-boundary.test.js index 3ccedb0..c26c38f 100644 --- a/tests/auth-boundary.test.js +++ b/tests/auth-boundary.test.js @@ -58,15 +58,30 @@ describe("authenticated product boundary", () => { 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 }); const { GET } = await import("@/app/api/health/route.js"); const response = await GET(); expect(response.status).toBe(500); - await expect(response.json()).resolves.toMatchObject({ configPresent: false }); - expect(mockGetAuthenticatedUser).not.toHaveBeenCalled(); + const body = await response.json(); + expect(body).toEqual({ healthy: false }); }); it("does not convert /api/health to 401 via middleware when unauthenticated", async () => {