/** * admin-relay.ts — Admin endpoints for the relay account whitelist. * * 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/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 } from "@workspace/db"; 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/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;