diff --git a/apps/web/app/api/health/route.ts b/apps/web/app/api/health/route.ts index cd537d8..d49e617 100644 --- a/apps/web/app/api/health/route.ts +++ b/apps/web/app/api/health/route.ts @@ -1,35 +1,12 @@ import { NextResponse } from "next/server" +import { getHealthResponse } from "@/lib/api/health" import { prisma } from "@/lib/prisma" export const dynamic = "force-dynamic" -type HealthResponse = { - statusCode: 200 | 503 - status: "healthy" | "degraded" - checks: { - app: "ok" - database: "ok" | "unavailable" - } -} - export async function GET() { - const response: HealthResponse = { - statusCode: 200, - status: "healthy", - checks: { - app: "ok", - database: "ok", - }, - } - - try { - await prisma.$queryRaw`SELECT 1` - } catch { - response.statusCode = 503 - response.status = "degraded" - response.checks.database = "unavailable" - } + const response = await getHealthResponse(() => prisma.$queryRaw`SELECT 1`) return NextResponse.json(response, { status: response.statusCode }) } diff --git a/apps/web/lib/api/auth-smoke.ts b/apps/web/lib/api/auth-smoke.ts new file mode 100644 index 0000000..f2d118b --- /dev/null +++ b/apps/web/lib/api/auth-smoke.ts @@ -0,0 +1,33 @@ +type UnauthorizedResponse = { + statusCode: 401 + error: "Unauthorized" +} + +type InternalSessionResult = { + response: { status: number; body: UnauthorizedResponse } | null +} + +type AgentAuthResult = { + id: string + AgentId: string + name: string + company: Record +} | null + +export async function internalApiRejectsMissingSession( + requireSession: () => Promise +) { + const { response } = await requireSession() + + return response?.status === 401 && response.body.error === "Unauthorized" +} + +export async function agentApiRejectsInvalidAuth( + authenticate: () => Promise +) { + return (await authenticate()) === null +} + +export function responseTextLeaksBearerTokenHash(body: unknown) { + return JSON.stringify(body).includes("bearerTokenHash") +} diff --git a/apps/web/lib/api/health.ts b/apps/web/lib/api/health.ts new file mode 100644 index 0000000..0d8a239 --- /dev/null +++ b/apps/web/lib/api/health.ts @@ -0,0 +1,29 @@ +export type HealthResponse = { + statusCode: 200 | 503 + status: "healthy" | "degraded" + checks: { + app: "ok" + database: "ok" | "unavailable" + } +} + +export async function getHealthResponse(checkDatabase: () => Promise) { + const response: HealthResponse = { + statusCode: 200, + status: "healthy", + checks: { + app: "ok", + database: "ok", + }, + } + + try { + await checkDatabase() + } catch { + response.statusCode = 503 + response.status = "degraded" + response.checks.database = "unavailable" + } + + return response +} diff --git a/package.json b/package.json index 8a27279..33caba5 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "cli:dev": "corepack pnpm --filter agentbridge dev", "cli:build": "corepack pnpm --filter agentbridge build", "cli:typecheck": "corepack pnpm --filter agentbridge typecheck", - "cli:pack": "corepack pnpm --filter agentbridge pack:dry-run" + "cli:pack": "corepack pnpm --filter agentbridge pack:dry-run", + "smoke:auth-health": "tsx scripts/smoke-auth-health.ts" }, "devDependencies": { "@prisma/adapter-pg": "^7.8.0", diff --git a/scripts/smoke-auth-health.ts b/scripts/smoke-auth-health.ts new file mode 100644 index 0000000..f6b8471 --- /dev/null +++ b/scripts/smoke-auth-health.ts @@ -0,0 +1,64 @@ +import assert from "node:assert/strict" + +import { + agentApiRejectsInvalidAuth, + internalApiRejectsMissingSession, + responseTextLeaksBearerTokenHash, +} from "../apps/web/lib/api/auth-smoke" +import { getHealthResponse } from "../apps/web/lib/api/health" + +async function main() { + const healthy = await getHealthResponse(async () => undefined) + assert.equal(healthy.statusCode, 200) + assert.equal(healthy.status, "healthy") + assert.equal(healthy.checks.database, "ok") + + const degraded = await getHealthResponse(async () => { + throw new Error("database unavailable") + }) + assert.equal(degraded.statusCode, 503) + assert.equal(degraded.status, "degraded") + assert.equal(degraded.checks.database, "unavailable") + + const internalRejected = await internalApiRejectsMissingSession(async () => ({ + response: { + status: 401, + body: { statusCode: 401, error: "Unauthorized" }, + }, + })) + assert.equal(internalRejected, true) + + const agentRejected = await agentApiRejectsInvalidAuth(async () => null) + assert.equal(agentRejected, true) + + const safeAgentResponse = { + statusCode: 200, + agent: { + id: "agent-id", + AgentId: "kaito", + name: "Kaito", + company: { + id: "company-id", + name: "NotAnOrdinary Lab", + }, + }, + } + assert.equal(responseTextLeaksBearerTokenHash(safeAgentResponse), false) + + const unsafeAgentResponse = { + statusCode: 200, + agent: { + company: { + bearerTokenHash: "secret-hash", + }, + }, + } + assert.equal(responseTextLeaksBearerTokenHash(unsafeAgentResponse), true) + + console.log("PASS smoke-auth-health") +} + +main().catch((error) => { + console.error(error) + process.exit(1) +})