// F3: the Console's trail of human operations (Console restart, org bus restart, route and // instance changes made from the page). One JSON line per use: who, when, what, outcome. // The file is /console/actions.jsonl, owner-only. A write that cannot be made safely // throws, and the caller refuses the operation (fail closed). import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, writeSync } from 'node:fs'; import { join } from 'node:path'; // The person, never a token: provider, issuer, subject and display name of the session. export const actor = session => ({ provider: session.provider, iss: session.iss, sub: session.sub, ...(typeof session.name === 'string' ? { name: session.name } : {}) }); export function actionTrail(dir) { const file = join(dir, 'actions.jsonl'); return entry => { mkdirSync(dir, { recursive: true, mode: 0o700 }); const st = lstatSync(dir); if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== process.getuid() || (st.mode & 0o077) !== 0) throw new Error('unsafe trail directory'); const fd = openSync(file, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600); try { const f = fstatSync(fd); if (!f.isFile() || f.uid !== process.getuid() || (f.mode & 0o777) !== 0o600 || f.nlink !== 1) throw new Error('unsafe trail file'); writeSync(fd, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`); } finally { closeSync(fd); } }; }