Files
Void-Homelab/lib/api/cap.js
root 56805053f0 feat(api): capability enforcement on writes
Add lib/api/cap.js: requireWrite(entity_type) maps HTTP method to
action, runs canAct, and tags req.capTier as allow|suggest|deny→403.
Mutating routes (pages, projects, tasks, refs, resources, source_docs)
now check req.capTier and either run the repo (allow) or divert to
pending_changes returning 202 (suggest). Owner and worker actors stay
on the allow path. requireOwner helper added for Task 11.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 21:03:52 +10:00

32 lines
1.2 KiB
JavaScript

import { canAct } from '../auth/capability.js';
import * as pendingChanges from '../db/repos/pending_changes.js';
import { ForbiddenError } from './errors.js';
const METHOD_TO_ACTION = { POST: 'create', PATCH: 'update', PUT: 'update', DELETE: 'delete' };
export function requireWrite(entity_type) {
return (req, _res, next) => {
const action = METHOD_TO_ACTION[req.method] || 'update';
const tier = canAct(req.actor, action, entity_type);
if (tier === 'allow') { req.capTier = 'allow'; return next(); }
if (tier === 'suggest') { req.capTier = 'suggest'; return next(); }
return next(new ForbiddenError(`agent not permitted to ${action} ${entity_type}`));
};
}
export function requireOwner(req, _res, next) {
if (req.actor?.kind !== 'user') {
return next(new ForbiddenError('owner-only endpoint'));
}
next();
}
export async function divertToPending(req, res, { entity_type, entity_id = null, action, payload, reason = null }) {
const change = await pendingChanges.create({
agent_id: req.actor.id,
entity_type, entity_id, action, payload,
reason: reason ?? req.headers['x-reason'] ?? null
});
res.status(202).json({ pending: true, change_id: change.id });
}