import express from "express"; import { actions } from "./actions.mjs"; import { enqueue, get_entry, list_pending, resolve } from "./queue.mjs"; import { load_secrets } from "./secrets.mjs"; import { create_auth_middleware, check_can_approve } from "./auth.mjs"; const PORT = process.env.CONDUIT_PORT || 3015; function get_arg(argv, flag) { const i = argv.indexOf(flag); return i !== -1 ? argv[i + 1] : null; } const secrets_path = get_arg(process.argv, "--secrets"); let secrets; try { secrets = load_secrets(secrets_path); } catch (err) { console.error(`Fatal: ${err.message}`); process.exit(1); } const { users } = secrets; const app = express(); app.use(express.json({ verify: (req, _res, buf) => { req.raw_body = buf.toString("utf8"); }, })); app.use(create_auth_middleware(users)); function validate_params(action_def, params) { const errors = []; for (const p of action_def.params) { if (p.required && (params[p.name] === undefined || params[p.name] === null)) { errors.push(`Missing required param: ${p.name}`); } } return errors; } // POST /action — main entry point app.post("/action", async (req, res) => { const { action, ...params } = req.body ?? {}; if (!action) { return res.status(400).json({ error: "Missing 'action' field" }); } const def = actions[action]; if (!def) { return res.status(404).json({ error: `Unknown action: ${action}` }); } const errors = validate_params(def, params); if (errors.length) { return res.status(400).json({ error: "Invalid params", details: errors }); } if (def.policy === "auto-deny") { return res.status(403).json({ status: "denied", reason: "Policy: auto-deny" }); } if (def.policy === "auto-accept") { try { const result = await def.handler(params); return res.json({ status: "accepted", result }); } catch (err) { return res.status(500).json({ status: "error", error: err.message }); } } if (def.policy === "queue") { const id = enqueue(action, params, req.conduit_user); return res.status(202).json({ status: "queued", id }); } }); // GET /queue — list pending items app.get("/queue", (req, res) => { res.json(list_pending()); }); // POST /queue/:id/approve — user approves a queued action app.post("/queue/:id/approve", async (req, res) => { const entry = get_entry(req.params.id); if (!entry) { return res.status(404).json({ error: "Not found" }); } if (entry.status !== "pending") { return res.status(409).json({ error: "Already resolved" }); } if (!check_can_approve(users, req.conduit_user, entry.submitted_by)) { return res.status(403).json({ error: "Not authorized to approve this entry" }); } resolve(req.params.id, "approved"); const def = actions[entry.action]; try { const result = await def.handler(entry.params); res.json({ status: "approved", result }); } catch (err) { res.status(500).json({ status: "error", error: err.message }); } }); // POST /queue/:id/deny — user denies a queued action app.post("/queue/:id/deny", (req, res) => { const entry = get_entry(req.params.id); if (!entry) { return res.status(404).json({ error: "Not found" }); } if (entry.status !== "pending") { return res.status(409).json({ error: "Already resolved" }); } if (!check_can_approve(users, req.conduit_user, entry.submitted_by)) { return res.status(403).json({ error: "Not authorized to deny this entry" }); } resolve(req.params.id, "denied"); res.json({ status: "denied" }); }); app.listen(PORT, () => { console.log(`claude-code-conduit server running on port ${PORT}`); console.log(`Workspace root: ${process.env.CONDUIT_ROOT || "/workspace"}`); });