CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Records who changed what, and is the mechanism statistics will read — one event table rather than two logs that drift apart. The table is deliberately generic (action + target + JSON metadata) so a new action needs no migration. Writes never throw: a kick must not fail because its log entry could not be written, since the kick already happened. Leaving is recorded as a different action from being removed. The same route serves both, and a log that conflates them misleads exactly when it matters. Actor is nullable with ON DELETE SET NULL: the event outlives the account, and a log that vanished with its actor would be worthless. Reads are gated on MANAGE_SPACE rather than a new permission bit, which would default to nobody until every role was re-edited. Paging uses the snowflake id, stable even for two events in the same millisecond, and an action this build does not know still renders a row.
37 lines
1.2 KiB
TypeScript
37 lines
1.2 KiB
TypeScript
import { getDb, schema } from '../db/index.js';
|
|
import { generateSnowflake } from './snowflake.js';
|
|
import type { AuditAction } from '@backspace/shared/src/audit.js';
|
|
|
|
interface RecordAuditEventInput {
|
|
spaceId: string;
|
|
actorId: string | null;
|
|
action: AuditAction;
|
|
targetType?: string | null;
|
|
targetId?: string | null;
|
|
metadata?: Record<string, unknown> | null;
|
|
}
|
|
|
|
/**
|
|
* Appends one entry to a space's audit log.
|
|
*
|
|
* Never throws: an audit write must not be able to fail the action it is
|
|
* describing. A moderator kicking someone must not see the kick fail because
|
|
* the log could not be written — the kick already happened.
|
|
*/
|
|
export function recordAuditEvent(input: RecordAuditEventInput): void {
|
|
try {
|
|
getDb().insert(schema.auditEvents).values({
|
|
id: generateSnowflake(),
|
|
spaceId: input.spaceId,
|
|
actorId: input.actorId,
|
|
action: input.action,
|
|
targetType: input.targetType ?? null,
|
|
targetId: input.targetId ?? null,
|
|
metadata: input.metadata ? JSON.stringify(input.metadata) : null,
|
|
createdAt: Date.now(),
|
|
}).run();
|
|
} catch (err) {
|
|
console.warn('[audit] failed to record event', input.action, err);
|
|
}
|
|
}
|