/** * admin-relay.ts — Admin endpoints for the relay account whitelist + stats. * * All routes are protected by ADMIN_SECRET env var (Bearer token). * If ADMIN_SECRET is not set, the routes reject all requests in production * and accept only from localhost in development. * * Routes: * GET /api/admin/relay/stats — event counts + account total * GET /api/admin/relay/accounts — list all relay accounts * POST /api/admin/relay/accounts/:pubkey/grant — grant access to a pubkey * POST /api/admin/relay/accounts/:pubkey/revoke — revoke access from a pubkey */ import { Router, type Request, type Response, type NextFunction } from "express"; import { makeLogger } from "../lib/logger.js"; import { relayAccountService } from "../lib/relay-accounts.js"; import { RELAY_ACCESS_LEVELS, type RelayAccessLevel, db, relayEventQueue, relayAccounts } from "@workspace/db"; import { eq, gte, sql } from "drizzle-orm"; const logger = makeLogger("admin-relay"); const router = Router(); const ADMIN_SECRET = process.env["ADMIN_SECRET"] ?? ""; const IS_PROD = process.env["NODE_ENV"] === "production"; if (!ADMIN_SECRET) { if (IS_PROD) { logger.error( "ADMIN_SECRET is not set in production — admin relay routes are unprotected. " + "Set ADMIN_SECRET in the API server environment immediately.", ); } else { logger.warn("ADMIN_SECRET not set — admin relay routes accept local-only requests (dev mode)"); } } // ── Admin auth middleware ────────────────────────────────────────────────────── function requireAdmin(req: Request, res: Response, next: NextFunction): void { if (ADMIN_SECRET) { const authHeader = req.headers["authorization"] ?? ""; const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : ""; if (token !== ADMIN_SECRET) { res.status(401).json({ error: "Unauthorized" }); return; } } else { const ip = req.ip ?? ""; const isLocal = ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1"; if (!isLocal) { logger.warn("admin-relay: no secret configured, rejecting non-local call", { ip }); res.status(401).json({ error: "Unauthorized" }); return; } } next(); } // ── GET /admin/relay/stats ──────────────────────────────────────────────────── // Returns event counts by status for today + all-time pending, plus account total. router.get("/admin/relay/stats", requireAdmin, async (_req: Request, res: Response) => { const todayStart = new Date(); todayStart.setUTCHours(0, 0, 0, 0); const [queueCounts, accountCount, approvedToday] = await Promise.all([ db .select({ status: relayEventQueue.status, count: sql`count(*)::int` }) .from(relayEventQueue) .groupBy(relayEventQueue.status), db.select({ count: sql`count(*)::int` }).from(relayAccounts), db .select({ count: sql`count(*)::int` }) .from(relayEventQueue) .where( gte(relayEventQueue.decidedAt, todayStart), ), ]); const statusMap: Record = {}; for (const row of queueCounts) { statusMap[row.status] = row.count; } res.json({ pending: statusMap["pending"] ?? 0, approved: statusMap["approved"] ?? 0, autoApproved: statusMap["auto_approved"] ?? 0, rejected: statusMap["rejected"] ?? 0, approvedToday: approvedToday[0]?.count ?? 0, totalAccounts: accountCount[0]?.count ?? 0, }); }); // ── GET /admin/relay/accounts ───────────────────────────────────────────────── router.get("/admin/relay/accounts", requireAdmin, async (_req: Request, res: Response) => { const accounts = await relayAccountService.list(); res.json({ accounts }); }); // ── POST /admin/relay/accounts/:pubkey/grant ────────────────────────────────── router.post( "/admin/relay/accounts/:pubkey/grant", requireAdmin, async (req: Request, res: Response) => { const { pubkey } = req.params as { pubkey: string }; if (!pubkey || pubkey.length !== 64 || !/^[0-9a-f]+$/.test(pubkey)) { res.status(400).json({ error: "pubkey must be a 64-char lowercase hex string" }); return; } const body = req.body as { level?: string; notes?: string }; const level = (body.level ?? "write").toLowerCase() as RelayAccessLevel; if (!RELAY_ACCESS_LEVELS.includes(level)) { res.status(400).json({ error: `Invalid access level '${level}'. Must be one of: ${RELAY_ACCESS_LEVELS.join(", ")}`, }); return; } const notes = typeof body.notes === "string" ? body.notes : "admin grant"; await relayAccountService.grant(pubkey, level, notes, "manual"); logger.info("admin granted relay access", { pubkey: pubkey.slice(0, 8), level, notes, }); res.json({ ok: true, pubkey, accessLevel: level, notes }); }, ); // ── POST /admin/relay/accounts/:pubkey/revoke ───────────────────────────────── router.post( "/admin/relay/accounts/:pubkey/revoke", requireAdmin, async (req: Request, res: Response) => { const { pubkey } = req.params as { pubkey: string }; if (!pubkey || pubkey.length !== 64 || !/^[0-9a-f]+$/.test(pubkey)) { res.status(400).json({ error: "pubkey must be a 64-char lowercase hex string" }); return; } const body = req.body as { reason?: string }; const reason = typeof body.reason === "string" ? body.reason : "admin revoke"; await relayAccountService.revoke(pubkey, reason); logger.info("admin revoked relay access", { pubkey: pubkey.slice(0, 8), reason, }); res.json({ ok: true, pubkey, revokedAt: new Date().toISOString(), reason }); }, ); export default router;