/** * Append a row to the hash-chained audit log. * * Each row's `this_hash` is computed over its own fields plus the previous * row's `this_hash`. If a later reader finds row N's `prev_hash` disagreeing * with row N-1's `this_hash`, the log has been tampered with — the chain * does not stop edits, it just makes them loud. * * The chain starts at 64 zeros for the first row. */ import { createHash } from 'node:crypto'; import { auditTailHash, insertAuditRow } from './db.js'; /** * Record an admin action. * * @param {object} entry * @param {string} entry.actor admin BNS name * @param {string} entry.action verb slug ('withdraw_listing', 'set_mode', 'login', …) * @param {string} [entry.target] the thing being acted on (listing id, mode key, …) * @param {object} [entry.details] arbitrary JSON, canonicalised before hashing */ export function logAudit({ actor, action, target = null, details = null }) { const at = new Date().toISOString(); const prev_hash = auditTailHash(); const detailsStr = details == null ? null : canonicalJson(details); const h = createHash('sha256'); h.update(at); h.update('|'); h.update(String(actor)); h.update('|'); h.update(String(action)); h.update('|'); h.update(target == null ? '' : String(target)); h.update('|'); h.update(detailsStr == null ? '' : detailsStr); h.update('|'); h.update(prev_hash); const this_hash = h.digest('hex'); insertAuditRow({ at, actor, action, target, details: detailsStr, prev_hash, this_hash }); return { at, actor, action, target, details: detailsStr, prev_hash, this_hash }; } function canonicalJson(value) { if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return '[' + value.map(canonicalJson).join(',') + ']'; const keys = Object.keys(value).sort(); return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(value[k])).join(',') + '}'; }