/** * @file ai-routers.js (routes) * @description CRUD for AI Router profiles — the endpoints behind the * **AI Router** tab of Settings. CHOOSING a profile for a lane is not here: it * happens once, when the lane is created (`POST /api/lanes/:id/router/apply`), * because the write target is the lane's own directory. * * An edit or a delete here does reach the lanes that already chose it, though. * Attaching COPIES the profile into the lane's own config, so a store-only * write would be inert exactly where it matters most — a rotated key would * never reach the lanes still authenticating with the revoked one. * * No response on this router carries key material. `GET /` returns the store's * public view (`hasKey: true|false`), and there is deliberately no * `GET /:name` — a route that returned one profile in full would be the one * way an API key could leave this process. * * @author Nguyễn Ngọc Trí Vĩ */ const express = require("express"); const routers = require("../lib/ai-routers"); const { reapplyToLanes, detachFromLanes } = require("../lib/router-apply"); const { sameOriginGuard } = require("../lib/same-origin"); const router = express.Router(); const ERR_TO_STATUS = { EBADNAME: 400, EBADLABEL: 400, EBADURL: 400, EBADMODEL: 400, EBADJSON: 409, EBUILTIN: 409, ENOTFOUND: 404, }; function fail(res, e) { const status = ERR_TO_STATUS[e.code] || 500; return res.status(status).json({ error: { code: e.code || "EINTERNAL", message: e.message } }); } // GET /api/ai-routers — every profile, built-in `native` first. Never a key. router.get("/", (_req, res) => { try { res.json(routers.listRouters()); } catch (e) { return fail(res, e); } }); // POST /api/ai-routers — body: { label, baseUrl, apiKey?, model, models? } router.post("/", sameOriginGuard, (req, res) => { try { res.status(201).json(routers.createRouter(req.body || {})); } catch (e) { return fail(res, e); } }); // PATCH /api/ai-routers/:name — same fields, all optional. An omitted `apiKey` // keeps the stored one; the edit form cannot show a key, so it cannot echo one. // // Attaching COPIES a profile into the lane's own config, so an edit that only // wrote the store here would be inert: rotating a leaked key would leave every // claude lane on the revoked one. `reapplyToLanes` pushes the change out, and // reports per-lane failures instead of failing the edit. router.patch("/:name", sameOriginGuard, (req, res) => { try { const saved = routers.updateRouter(req.params.name, req.body || {}); res.json({ ...saved, lanes: reapplyToLanes(saved.name) }); } catch (e) { return fail(res, e); } }); // DELETE /api/ai-routers/:name — every lane using it is reset to native, in its // config on disk as well as in the DB. Leaving the lanes alone would keep the // deleted router's key and base URL live in each claude lane's settings file. router.delete("/:name", sameOriginGuard, (req, res) => { try { const removed = routers.removeRouter(req.params.name); res.json({ ...removed, lanes: detachFromLanes(req.params.name) }); } catch (e) { return fail(res, e); } }); module.exports = router;