import crypto from "crypto"; import express from "express"; import config from "../config.js"; import { Patch } from "../db/models/index.js"; import { getConfiguredDevice } from "../services/devices.js"; import { normalizeProduceIcon } from "../services/patches.js"; import { NodeInputError, listNodes, setNodePatches, upsertNodes, } from "../services/nodes.js"; import { getLogger } from "../logger.js"; const logger = getLogger("HubApi"); function keysMatch(given, expected) { const a = Buffer.from(given); const b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b); } function requireHubKey(req, res, next) { const expected = config.hub?.apiKey; if (!expected) { res.status(503).json({ status: "error", error: "Hub API not configured" }); return; } const match = /^Bearer\s+(.+)$/i.exec(req.get("authorization") || ""); if (!match || !keysMatch(match[1].trim(), expected)) { res.status(401).json({ status: "error", error: "Unauthorized" }); return; } next(); } function sendError(res, e, fallback) { if (e instanceof NodeInputError) { res.status(400).json({ status: "error", error: e.message }); return; } logger.error(fallback, e); res.status(500).json({ status: "error", error: e.message || fallback }); } /** HTTP API used by the hub firmware to relay BLE patch assignments (BLE_PROTOCOL.md Appendix A). */ export function createHubRouter() { const router = express.Router(); router.use(requireHubKey); router.use(express.json()); router.get("/patches", async (_req, res) => { try { const device = await getConfiguredDevice(); const docs = device ? await Patch.find({ deviceId: device._id }) .select("patchId name icon") .sort({ patchId: 1 }) .lean() : []; const patches = docs.map((p) => ({ id: p.patchId, name: p.name ?? "", icon: normalizeProduceIcon(p.icon), })); res.json({ status: "ok", data: { patches } }); } catch (e) { sendError(res, e, "list patches failed"); } }); router.get("/nodes", async (_req, res) => { try { res.json({ status: "ok", data: { nodes: await listNodes() } }); } catch (e) { sendError(res, e, "list nodes failed"); } }); router.post("/nodes", async (req, res) => { try { const nodes = await upsertNodes(req.body?.serials); res.json({ status: "ok", data: { nodes } }); } catch (e) { sendError(res, e, "register nodes failed"); } }); router.put("/nodes/:serial", async (req, res) => { try { const node = await setNodePatches(req.params.serial, req.body?.patches); res.json({ status: "ok", data: { node } }); } catch (e) { sendError(res, e, "set node patches failed"); } }); return router; }