From 9dd56f259dfe6f3a18219df0d489512fb25ff23a Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Thu, 14 May 2026 14:49:28 -0700 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Share=20AUDIT?= =?UTF-8?q?=5FACTIONS,=20tighten=20audit=20dep=20types,=20document=20route?= =?UTF-8?q?=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 to the published RecordAuditEntryInput type returning Promise, and tightens the local emitAudit args to AuditAction. Replaces the local ParsedFilters interface in the audit-log handler with Omit 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. --- api/server/routes/admin/audit.js | 1 + packages/api/src/admin/auditLog.ts | 15 +++------------ packages/api/src/admin/grants.ts | 20 ++++++++------------ packages/data-schemas/src/index.ts | 1 + packages/data-schemas/src/schema/auditLog.ts | 3 ++- packages/data-schemas/src/types/admin.ts | 7 ++++++- packages/data-schemas/src/types/auditLog.ts | 9 ++++++--- 7 files changed, 27 insertions(+), 29 deletions(-) diff --git a/api/server/routes/admin/audit.js b/api/server/routes/admin/audit.js index e94867631e..4e03023e1e 100644 --- a/api/server/routes/admin/audit.js +++ b/api/server/routes/admin/audit.js @@ -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); diff --git a/packages/api/src/admin/auditLog.ts b/packages/api/src/admin/auditLog.ts index 29fb39165d..2d151882c7 100644 --- a/packages/api/src/admin/auditLog.ts +++ b/packages/api/src/admin/auditLog.ts @@ -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(['grant_assigned', 'grant_removed']); +const VALID_ACTIONS = new Set(AUDIT_ACTIONS); const VALID_PRINCIPAL_TYPES = new Set(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; interface AuditLogQuery { search?: string; diff --git a/packages/api/src/admin/grants.ts b/packages/api/src/admin/grants.ts index 297efa620a..1209769ac4 100644 --- a/packages/api/src/admin/grants.ts +++ b/packages/api/src/admin/grants.ts @@ -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; /** 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; + recordAuditEntry?: (input: RecordAuditEntryInput) => Promise; /** Resolves a User document's display name from its id (for audit actor name). */ resolveUserName?: (userId: string | Types.ObjectId) => Promise; } @@ -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; diff --git a/packages/data-schemas/src/index.ts b/packages/data-schemas/src/index.ts index e63dc02a15..5cdf5654ce 100644 --- a/packages/data-schemas/src/index.ts +++ b/packages/data-schemas/src/index.ts @@ -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'; diff --git a/packages/data-schemas/src/schema/auditLog.ts b/packages/data-schemas/src/schema/auditLog.ts index de310257e2..91d9b1aa5a 100644 --- a/packages/data-schemas/src/schema/auditLog.ts +++ b/packages/data-schemas/src/schema/auditLog.ts @@ -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( { action: { type: String, - enum: ['grant_assigned', 'grant_removed'], + enum: [...AUDIT_ACTIONS], required: true, immutable: true, }, diff --git a/packages/data-schemas/src/types/admin.ts b/packages/data-schemas/src/types/admin.ts index 755bb7a2e6..98f1841bfe 100644 --- a/packages/data-schemas/src/types/admin.ts +++ b/packages/data-schemas/src/types/admin.ts @@ -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 = { diff --git a/packages/data-schemas/src/types/auditLog.ts b/packages/data-schemas/src/types/auditLog.ts index 271b9e77da..24f50828b5 100644 --- a/packages/data-schemas/src/types/auditLog.ts +++ b/packages/data-schemas/src/types/auditLog.ts @@ -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 {