Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
77 lines
2.3 KiB
JavaScript
77 lines
2.3 KiB
JavaScript
/**
|
|
* Audit chain API — ADR-0066-A §C.
|
|
*
|
|
* Owner-only integrity check over the `audit_log` hash chain.
|
|
*
|
|
* Mount in server.js:
|
|
* app.use('/api/v3/audit', authenticate, auditRouter);
|
|
*
|
|
* Authorization mirrors ADR-0040 / ADR-0066 §5: JWT-authenticated at the
|
|
* parent mount, then owner-only — `req.user.id === space(11).owner_id`.
|
|
* Admin role is NOT sufficient; only the Development-space owner may read
|
|
* the integrity surface. Rejects 403 otherwise.
|
|
*/
|
|
|
|
import express from 'express';
|
|
|
|
import { dbGet } from '../../database/connection.js';
|
|
import { verifyAuditChain } from '../../services/audit/verifyAuditChain.js';
|
|
import { apiLogger } from '../../utils/logger.js';
|
|
import { success, error, forbidden, badRequest } from '../../utils/response.js';
|
|
|
|
const log = apiLogger.child({ module: 'audit_api' });
|
|
|
|
const OWNER_SPACE_ID = 11; // Development — its owner owns the audit surface.
|
|
|
|
const router = express.Router();
|
|
|
|
async function requireOwner(req, res) {
|
|
if (!req.user?.id) {
|
|
forbidden(res, 'Authentication required');
|
|
return false;
|
|
}
|
|
const space = await dbGet('SELECT id, owner_id FROM spaces WHERE id = ?', [
|
|
OWNER_SPACE_ID,
|
|
]);
|
|
if (!space) {
|
|
error(res, 'OWNER_SPACE_MISSING', `Space ${OWNER_SPACE_ID} not found`, 500);
|
|
return false;
|
|
}
|
|
if (space.owner_id !== req.user.id) {
|
|
forbidden(res, 'Owner-only endpoint');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* GET /api/v3/audit/verify?from_id=<n>
|
|
* → { ok, checked, break_at_id, cutover_id, head }
|
|
*/
|
|
router.get('/verify', async (req, res) => {
|
|
if (!(await requireOwner(req, res))) return;
|
|
|
|
let fromId;
|
|
if (req.query.from_id !== undefined) {
|
|
fromId = Number(req.query.from_id);
|
|
if (!Number.isInteger(fromId) || fromId < 0) {
|
|
return badRequest(res, 'from_id must be a non-negative integer');
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await verifyAuditChain(fromId);
|
|
if (!result.ok) {
|
|
log.warn(
|
|
{ break_at_id: result.break_at_id, checked: result.checked },
|
|
'audit chain verification FAILED — possible tampering'
|
|
);
|
|
}
|
|
return success(res, result);
|
|
} catch (err) {
|
|
log.error({ err: { message: err?.message } }, 'audit verify failed');
|
|
return error(res, 'AUDIT_VERIFY_FAILED', 'Chain verification error', 500);
|
|
}
|
|
});
|
|
|
|
export default router;
|