feat(audit): append-only audit log for spaces
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
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) Waiting to run
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
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.
This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
"./src/activities": "./src/activities.ts",
|
||||
"./src/activities.js": "./src/activities.ts",
|
||||
"./src/constants": "./src/constants.ts",
|
||||
"./src/constants.js": "./src/constants.ts"
|
||||
"./src/constants.js": "./src/constants.ts",
|
||||
"./src/audit": "./src/audit.ts",
|
||||
"./src/audit.js": "./src/audit.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Audit action vocabulary, shared so the server writes and the client renders
|
||||
* the same strings. Values are stored in the database, so renaming one
|
||||
* rewrites history — add new actions instead.
|
||||
*/
|
||||
export const AUDIT_ACTIONS = [
|
||||
'space.update',
|
||||
'space.transfer_ownership',
|
||||
'channel.create',
|
||||
'channel.update',
|
||||
'channel.delete',
|
||||
'member.kick',
|
||||
'member.leave',
|
||||
'member.ban',
|
||||
'member.unban',
|
||||
'role.create',
|
||||
'role.update',
|
||||
'role.delete',
|
||||
'invite.create',
|
||||
'message.delete',
|
||||
] as const;
|
||||
|
||||
export type AuditAction = (typeof AUDIT_ACTIONS)[number];
|
||||
|
||||
export interface AuditEventActor {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string | null;
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
export interface AuditEvent {
|
||||
id: string;
|
||||
spaceId: string;
|
||||
action: AuditAction;
|
||||
actor: AuditEventActor | null;
|
||||
targetType: string | null;
|
||||
targetId: string | null;
|
||||
/** Shape depends on `action`; used for display only. */
|
||||
metadata: Record<string, unknown> | null;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export const AUDIT_PAGE_SIZE = 50;
|
||||
Reference in New Issue
Block a user