53 lines
1.9 KiB
JavaScript
53 lines
1.9 KiB
JavaScript
|
|
/**
|
||
|
|
* 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(',') + '}';
|
||
|
|
}
|