- server/policy_overrides.mjs: persistent group/action override store - Group toggle: disabling a group forces all its actions to auto-deny without touching individual overrides; re-enabling restores them intact - Action overrides: cycle auto-accept/queue/auto-deny/clear per action - Effective policy resolution in index.mjs: group > action override > hardcoded default - auth.mjs: add check_can_manage_policies (requires canApprove.length > 0) - Three new endpoints: GET /policies, POST /policies/group/:group, POST /policies/action/:action - client/conduit.mjs: get_policies, set_group_policy, set_action_policy - ccc-queue.mjs: policy view (Tab to switch); group/action panel with space/e/←/→ - actions.mjs: group fields on desktop/email/calendar actions; list-actions includes group - config.example.json: add policy_overrides key - Bump version to 1.3.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
55 lines
1.8 KiB
JavaScript
55 lines
1.8 KiB
JavaScript
import { createHmac, timingSafeEqual } from "crypto";
|
|
|
|
export function create_auth_middleware(users) {
|
|
return function hmac_auth(req, res, next) {
|
|
const username = req.headers["x-conduit-user"];
|
|
const timestamp = req.headers["x-conduit-timestamp"];
|
|
const signature = req.headers["x-conduit-signature"];
|
|
|
|
if (!username || !timestamp || !signature) {
|
|
return res.status(401).json({ error: "Missing auth headers" });
|
|
}
|
|
|
|
const ts = parseInt(timestamp, 10);
|
|
if (!Number.isFinite(ts) || Date.now() - ts > 30_000) {
|
|
return res.status(401).json({ error: "Request expired" });
|
|
}
|
|
|
|
const user = users[username];
|
|
if (!user) {
|
|
return res.status(401).json({ error: "Unknown user" });
|
|
}
|
|
|
|
const raw_body = req.raw_body ?? "";
|
|
const expected = createHmac("sha256", user.secret)
|
|
.update(timestamp + "." + raw_body)
|
|
.digest("hex");
|
|
|
|
const sig_buf = Buffer.from(signature, "hex");
|
|
const expected_buf = Buffer.from(expected, "hex");
|
|
|
|
if (sig_buf.length !== expected_buf.length || !timingSafeEqual(sig_buf, expected_buf)) {
|
|
return res.status(401).json({ error: "Invalid signature" });
|
|
}
|
|
|
|
req.conduit_user = username;
|
|
next();
|
|
};
|
|
}
|
|
|
|
export function check_can_approve(users, requester_name, submitter_name) {
|
|
const requester = users[requester_name];
|
|
if (!requester) {
|
|
return false;
|
|
}
|
|
return requester.canApprove.includes(submitter_name);
|
|
}
|
|
|
|
// A user can manage policy overrides if they have canApprove authority over
|
|
// at least one other user. This ensures only trusted humans (never agents)
|
|
// can change what agents are allowed to do. canApprove itself is never
|
|
// runtime-writable — it is loaded from secrets.json at startup.
|
|
export function check_can_manage_policies(users, caller) {
|
|
return (users[caller]?.canApprove?.length ?? 0) > 0;
|
|
}
|