import { timingSafeEqual } from "node:crypto"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import BRAND from "../../../../brand.config.json" with { type: "json" }; import { errors } from "../lib/errors.js"; const CACHE_KEY = "ops:summary:cache"; const CACHE_TS_KEY = "ops:summary:computed_at"; function requireOpsToken(app: FastifyInstance) { return async (req: FastifyRequest, reply: FastifyReply): Promise => { if (app.config.NODE_ENV === "production" && req.protocol !== "https") { return reply .status(403) .send({ error: { code: "HTTPS_REQUIRED", message: "Ops-endpointen kräver HTTPS." } }); } const header = req.headers.authorization ?? ""; const match = /^Bearer\s+(.+)$/.exec(header); const provided = match?.[1] ?? ""; const expected = app.config.OPS_TOKEN ?? ""; if (!expected || expected.length < 20 || provided.length !== expected.length) { throw errors.unauthorized(); } const ok = timingSafeEqual(Buffer.from(provided), Buffer.from(expected)); if (!ok) throw errors.unauthorized(); }; } /** * Operations summary endpoint: read-only, token-protected, served from cache. * Heavy computation is done by the REFRESH_OPS_SUMMARY_CACHE worker job. */ export async function opsRoutes(app: FastifyInstance) { const preHandler = [requireOpsToken(app)]; app.get("/ops/v1/health", { preHandler }, async () => ({ ok: true, app: "cibello", })); app.get("/ops/v1/summary", { preHandler }, async (_req, reply) => { const [json, ts] = await app.redis.mget(CACHE_KEY, CACHE_TS_KEY); if (!json) { return reply.status(503).send({ error: { code: "CACHE_MISS", message: "Sammanfattningen är inte färdigberäknad än. Vänta på nästa canary-/cache-jobb.", }, }); } let summary: Record; try { summary = JSON.parse(json) as Record; } catch { return reply .status(500) .send({ error: { code: "CACHE_INVALID", message: "Cachen innehåller ogiltig JSON." } }); } const enriched = structuredClone(summary); enriched.gemini = { anrop_24h: null, prompt_tokens_24h: null, output_tokens_24h: null, total_tokens_24h: null, total_tokens_manad: null, kostnad_usd_mikrocent_24h: null, andel_pa_aamos: 0.0, aamos_agreement: null, }; if ( enriched.wall && typeof enriched.wall === "object" && Array.isArray((enriched.wall as Record).boards) ) { for (const board of (enriched.wall as { boards: Array<{ title?: string }> }).boards) { if (board.title === "{brand}") board.title = BRAND.name; } } return reply.send({ ...enriched, cached_at: ts ?? summary.as_of, }); }); }