SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
5 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
2.7 KB · 55 lines typescript
Raw Blame History
1/**2 * Admin API (/api/admin): header `x-dci-admin-token` must equal DCI_ADMIN_TOKEN (constant-time compare).3 * 60 req/min per IP. Responses use the same envelope `{ data, meta }`; errors `{ error, statusCode }`.4 */5import { createHash, timingSafeEqual } from "node:crypto";6import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";7import { getEnv } from "../../env.js";8import { HttpError } from "../../lib/http.js";9import { connectorAdminRoutes } from "./connectors.js";10import { documentAdminRoutes } from "./documents.js";11import { matchAdminRoutes } from "./matches.js";12import { curationAdminRoutes } from "./curation.js";13import { devtoolAdminRoutes } from "./devtool.js";14import { opsAdminRoutes } from "./ops.js";15import { qualityAdminRoutes } from "./quality.js";16import { proxyAdminRoutes } from "./proxy.js";17import { claimAdminRoutes } from "./claims.js";18import { projectAdminRoutes } from "./projects.js";1920export function tokenMatches(provided: string | undefined, expected: string | null): boolean {21  if (!expected || !provided) return false;22  const a = createHash("sha256").update(provided).digest();23  const b = createHash("sha256").update(expected).digest();24  return timingSafeEqual(a, b);25}2627export function adminAuth(req: FastifyRequest, _reply: FastifyReply, done: (err?: Error) => void): void {28  const expected = getEnv().adminToken;29  if (!expected) return done(new HttpError(503, "admin API disabled: DCI_ADMIN_TOKEN is not configured (missing, empty or the \"change-me\" placeholder)"));30  const header = req.headers["x-dci-admin-token"];31  const provided = Array.isArray(header) ? header[0] : header;32  if (!tokenMatches(provided, expected)) return done(new HttpError(401, "unauthorized"));33  done();34}3536export async function adminRoutes(app: FastifyInstance): Promise<void> {37  app.addHook("onRequest", adminAuth);38  app.addHook("onSend", async (_req, reply) => { reply.header("cache-control", "no-store"); });39  // route-level rate limit override for the admin scope40  app.addHook("onRoute", (route) => {41    route.config = { ...(route.config as Record<string, unknown> | undefined), rateLimit: { max: getEnv().adminRateLimit, timeWindow: "1 minute" } };42    route.schema = { ...(route.schema ?? {}), tags: ["admin"], security: [{ adminToken: [] }] };43  });44  await app.register(connectorAdminRoutes);45  await app.register(documentAdminRoutes);46  await app.register(matchAdminRoutes);47  await app.register(curationAdminRoutes);48  await app.register(devtoolAdminRoutes);49  await app.register(opsAdminRoutes);50  await app.register(qualityAdminRoutes);51  await app.register(proxyAdminRoutes);52  await app.register(claimAdminRoutes);53  await app.register(projectAdminRoutes);54}55