Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions review-enrichment/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// Transport + contract here; the analysis lives in brief.ts (orchestrator) + analyzers/*, with each analyzer
// filling one findings key for renderer/prompt consumption.
import { serve } from "@hono/node-server";
import { pathToFileURL } from "node:url";
import { Hono } from "hono";
import { normalizeSharedSecret, verifyBearer } from "./auth.js";
import { buildBrief } from "./brief.js";
Expand Down Expand Up @@ -46,7 +47,13 @@ app.get("/health", (c) =>
c.json({ status: "ok", service: "review-enrichment" }),
);
app.get("/ready", (c) => c.json({ ready: true }));
app.get("/metrics", (c) => c.text(renderMetrics()));
app.get("/metrics", (c) => {
const secret = normalizeSharedSecret(process.env.REES_SHARED_SECRET);
if (!secret) return c.json({ error: "service_not_configured" }, 503);
if (!verifyBearer(c.req.header("authorization"), secret))
return c.json({ error: "unauthorized" }, 401);
return c.text(renderMetrics());
});

function recordEnrichOutcome(status: string, startedAtMs: number): void {
incr("rees_enrich_requests_total", { status });
Expand Down Expand Up @@ -108,22 +115,24 @@ app.post("/v1/enrich", async (c) => {
}
});

const port = Number(process.env.PORT ?? "8080");
serve({ fetch: app.fetch, port }, (info) => {
console.log(JSON.stringify({ event: "rees_listening", port: info.port }));
});
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
const port = Number(process.env.PORT ?? "8080");
serve({ fetch: app.fetch, port }, (info) => {
console.log(JSON.stringify({ event: "rees_listening", port: info.port }));
});

process.on("unhandledRejection", (reason) => {
captureUnhandledError(reason, { event: "rees_unhandled_rejection" });
});
process.on("unhandledRejection", (reason) => {
captureUnhandledError(reason, { event: "rees_unhandled_rejection" });
});

process.on("uncaughtException", (error) => {
captureUnhandledError(error, { event: "rees_uncaught_exception" });
void flushSentry().finally(() => process.exit(1));
});
process.on("uncaughtException", (error) => {
captureUnhandledError(error, { event: "rees_uncaught_exception" });
void flushSentry().finally(() => process.exit(1));
});

process.on("SIGTERM", () => {
void flushSentry().finally(() => process.exit(0));
});
process.on("SIGTERM", () => {
void flushSentry().finally(() => process.exit(0));
});
}

export { app };
30 changes: 30 additions & 0 deletions review-enrichment/test/server-metrics-auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import { after, test } from "node:test";

process.env.REES_SHARED_SECRET = "topsecret";

const { incr } = await import("../dist/metrics.js");
const { app } = await import("../dist/server.js");

after(() => {
delete process.env.REES_SHARED_SECRET;
});

test("/metrics rejects unauthenticated scrapes when the shared secret is configured", async () => {
const response = await app.request("/metrics");

assert.equal(response.status, 401);
assert.deepEqual(await response.json(), { error: "unauthorized" });
});

test("/metrics returns Prometheus text to callers with the shared bearer secret", async () => {
incr("rees_enrich_requests_total", { status: "ok" });

const response = await app.request("/metrics", {
headers: { authorization: "Bearer topsecret" },
});

assert.equal(response.status, 200);
assert.match(response.headers.get("content-type") ?? "", /^text\/plain/);
assert.match(await response.text(), /# HELP rees_enrich_requests_total/);
});