♻️ refactor: Share AUDIT_ACTIONS, tighten audit dep types, document route order

Exports a runtime AUDIT_ACTIONS array from packages/data-schemas alongside the
AuditAction type so the Mongoose schema enum and the HTTP handler's whitelist
consume one source of truth instead of duplicating the literal pair.

Switches the grants handler's recordAuditEntry dep typing from a duplicated
inline object literal returning Promise<unknown> to the published
RecordAuditEntryInput type returning Promise<void>, and tightens the local
emitAudit args to AuditAction. Replaces the local ParsedFilters interface in
the audit-log handler with Omit<AuditLogFilters, 'offset' | 'limit'> to drop
the duplicate definition.

Drops the optional marker on AuditLog.createdAt. Mongoose always sets it at
insert time, so callers treating it as nullable were guarding against a state
the schema does not produce.

Adds a comment on api/server/routes/admin/audit.js noting that /export.csv
must precede /:id so a future contributor does not accidentally reorder them
into a 404 trap.
This commit is contained in:
Dustin Healy 2026-05-14 14:49:28 -07:00 • committed by Danny Avila
parent 2682d70dff
commit 9dd56f259d
7 changed files with 27 additions and 29 deletions

View file

@ -18,6 +18,7 @@ const handlers = createAdminAuditLogHandlers({
router.use(requireJwtAuth, requireAuditLogRead);
router.get('/', handlers.listAuditLog);
/** `/export.csv` MUST precede `/:id` so it isn't matched as `{ id: 'export.csv' }`. */
router.get('/export.csv', handlers.exportAuditLogCsv);
router.get('/:id', handlers.getAuditLogEntry);

View file

@ -1,5 +1,5 @@
import { PrincipalType } from 'librechat-data-provider';
import { logger, MAX_AUDIT_LOG_LIMIT } from '@librechat/data-schemas';
import { AUDIT_ACTIONS, logger, MAX_AUDIT_LOG_LIMIT } from '@librechat/data-schemas';
import type {
AdminAuditLogEntry,
AuditAction,
@ -12,7 +12,7 @@ import type { ServerRequest } from '~/types/http';
const FORMULA_PREFIX = /^[=+\-@\t\r]/;
const CSV_BOM = '';
const VALID_ACTIONS = new Set<AuditAction>(['grant_assigned', 'grant_removed']);
const VALID_ACTIONS = new Set<AuditAction>(AUDIT_ACTIONS);
const VALID_PRINCIPAL_TYPES = new Set<string>(Object.values(PrincipalType));
/**
@ -154,16 +154,7 @@ function formatCsvRow(entry: AdminAuditLogEntry): string {
return CSV_COLUMNS.map((c) => escapeCsvCell(String(entry[c.key] ?? ''))).join(',');
}
interface ParsedFilters {
search?: string;
action?: AuditAction[];
from?: Date;
to?: Date;
actorQuery?: string;
targetPrincipalType?: PrincipalType;
targetQuery?: string;
capability?: string;
}
type ParsedFilters = Omit<AuditLogFilters, 'offset' | 'limit'>;
interface AuditLogQuery {
search?: string;

View file

@ -5,7 +5,12 @@ import {
SystemCapabilities,
expandImplications,
} from '@librechat/data-schemas';
import type { ISystemGrant, SystemCapability } from '@librechat/data-schemas';
import type {
AuditAction,
ISystemGrant,
RecordAuditEntryInput,
SystemCapability,
} from '@librechat/data-schemas';
import type { Response } from 'express';
import type { Types } from 'mongoose';
import type { ResolvedPrincipal } from '~/types/principal';
@ -73,16 +78,7 @@ export interface AdminGrantsDeps {
}) => ResolvedPrincipal[] | undefined;
checkRoleExists?: (roleId: string) => Promise<boolean>;
/** Optional audit emission. Failure is logged but does not roll back the grant. */
recordAuditEntry?: (input: {
action: 'grant_assigned' | 'grant_removed';
actorId: string | Types.ObjectId;
actorName: string;
targetPrincipalType: PrincipalType;
targetPrincipalId: string | Types.ObjectId;
targetName: string;
capability: string;
tenantId?: string;
}) => Promise<unknown>;
recordAuditEntry?: (input: RecordAuditEntryInput) => Promise<void>;
/** Resolves a User document's display name from its id (for audit actor name). */
resolveUserName?: (userId: string | Types.ObjectId) => Promise<string | null>;
}
@ -115,7 +111,7 @@ export function createAdminGrantsHandlers(deps: AdminGrantsDeps): {
} = deps;
async function emitAudit(args: {
action: 'grant_assigned' | 'grant_removed';
action: AuditAction;
caller: { userId: string; role: string; tenantId?: string };
principalType: PrincipalType;
principalId: string;

View file

@ -28,6 +28,7 @@ export {
} from './methods';
export type * from './types';
export type * from './methods';
export { AUDIT_ACTIONS } from './types/admin';
export { default as logger } from './config/winston';
export { default as meiliLogger } from './config/meiliLogger';
export { redactMessage } from './config/parsers';

View file

@ -1,5 +1,6 @@
import { Schema } from 'mongoose';
import { PrincipalType } from 'librechat-data-provider';
import { AUDIT_ACTIONS } from '~/types/admin';
import type { IAuditLog } from '~/types';
/**
@ -12,7 +13,7 @@ const auditLogSchema = new Schema<IAuditLog>(
{
action: {
type: String,
enum: ['grant_assigned', 'grant_removed'],
enum: [...AUDIT_ACTIONS],
required: true,
immutable: true,
},

View file

@ -65,8 +65,13 @@ export type AdminConfigDeleteResponse = {
success: boolean;
};
/** Single source of truth for the audit-action enum. Both the Mongoose schema
* enum and the HTTP handler's whitelist consume this constant so they cannot
* drift apart. */
export const AUDIT_ACTIONS = ['grant_assigned', 'grant_removed'] as const;
/** Audit action types for grant changes. */
export type AuditAction = 'grant_assigned' | 'grant_removed';
export type AuditAction = (typeof AUDIT_ACTIONS)[number];
/** SystemGrant document as returned by the admin API. */
export type AdminSystemGrant = {

View file

@ -3,9 +3,13 @@ import type { PrincipalType } from 'librechat-data-provider';
import type { AdminAuditLogEntry, AuditAction } from './admin';
/**
* AuditLog is an append-only collection — no `updatedAt`, no mutations.
* AuditLog is an append-only collection: no `updatedAt`, no mutations.
* See `~/schema/auditLog` for the enforcement (immutable fields plus
* pre-update / pre-delete / pre-save hooks).
*
* `createdAt` is always set by Mongoose at insert time, so this type
* intentionally declares it required. The `IAuditLog` intersection adds
* the document identity (`_id`).
*/
export type AuditLog = {
action: AuditAction;
@ -19,13 +23,12 @@ export type AuditLog = {
/** Absent = platform-operator audit; present = tenant-scoped audit */
tenantId?: string;
/** Mongoose auto-managed via `timestamps: { createdAt: true, updatedAt: false }` */
createdAt?: Date;
createdAt: Date;
};
export type IAuditLog = AuditLog &
Document & {
_id: Types.ObjectId;
createdAt: Date;
};
export interface RecordAuditEntryInput {