feat(audit): append-only audit log for spaces
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
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.
This commit is contained in:
@@ -73,6 +73,7 @@ import type {
|
||||
ReattachResponse,
|
||||
Activity,
|
||||
} from '@backspace/shared';
|
||||
import type { AuditEvent } from '@backspace/shared/src/audit.js';
|
||||
import { getApiForOrigin, getOwnerInstanceForDm } from '../utils/crossStoreResolvers';
|
||||
|
||||
export type { FederationPeer, FederationOrphanedAccount, FederationResetEvent, FederationResetEventsResponse, ApprovalRequest, PeeringSubscription, PeeringNotification };
|
||||
@@ -285,6 +286,10 @@ export class BackspaceApiClient {
|
||||
removeFavorite: (id: string) => Promise<void>;
|
||||
};
|
||||
|
||||
readonly audit: {
|
||||
log: (spaceId: string, before?: string) => Promise<{ events: AuditEvent[]; hasMore: boolean }>;
|
||||
};
|
||||
|
||||
readonly spotify: {
|
||||
status: () => Promise<{ configured: boolean; connected: boolean }>;
|
||||
authorizeUrl: () => Promise<{ url: string }>;
|
||||
@@ -699,6 +704,16 @@ export class BackspaceApiClient {
|
||||
},
|
||||
};
|
||||
|
||||
this.audit = {
|
||||
log: (spaceId: string, before?: string) => {
|
||||
const params = new URLSearchParams();
|
||||
if (before) params.set('before', before);
|
||||
const qs = params.toString();
|
||||
return request<{ events: AuditEvent[]; hasMore: boolean }>(
|
||||
'GET', `/spaces/${spaceId}/audit-log${qs ? `?${qs}` : ''}`);
|
||||
},
|
||||
};
|
||||
|
||||
this.spotify = {
|
||||
status: () => request<{ configured: boolean; connected: boolean }>('GET', '/connections/spotify/status'),
|
||||
authorizeUrl: () => request<{ url: string }>('GET', '/connections/spotify/authorize'),
|
||||
|
||||
Reference in New Issue
Block a user