diff --git a/server/src/app.ts b/server/src/app.ts index 356d07d9..d0f38a31 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -8,6 +8,7 @@ import { createAgentRoutes } from "./agents/routes"; import { type AuditReader, type AuditStore, + AuditQueryError, auditQueryFromUrl, recordAuditEvent, } from "./audit"; @@ -456,9 +457,16 @@ export function createApp( return context.json({ error: "Audit logging is not configured." }, 503); } - return context.json( - await auditReader.list(auditQueryFromUrl(new URL(context.req.url))), - ); + try { + return context.json( + await auditReader.list(auditQueryFromUrl(new URL(context.req.url))), + ); + } catch (error) { + if (error instanceof AuditQueryError) { + return context.json({ error: error.message }, 400); + } + throw error; + } }); /* * Who is here, and what they may do. diff --git a/server/src/audit.ts b/server/src/audit.ts index 3e42b2b3..e057138d 100644 --- a/server/src/audit.ts +++ b/server/src/audit.ts @@ -579,16 +579,33 @@ export function createAuditReader(database: Database): AuditReader { }; } +export class AuditQueryError extends Error { + constructor(message: string) { + super(message); + this.name = "AuditQueryError"; + } +} + export function auditQueryFromUrl(url: URL): AuditEventQuery { - const requestedLimit = Number.parseInt( - url.searchParams.get("limit") ?? "50", - 10, - ); + const rawLimit = url.searchParams.get("limit") ?? "50"; + const trimmedLimit = rawLimit.trim(); + const requestedLimit = /^\d+$/.test(trimmedLimit) + ? Number.parseInt(trimmedLimit, 10) + : Number.NaN; const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(requestedLimit, 1), 100) : 50; const optional = (name: string) => url.searchParams.get(name) ?? undefined; + const from = optional("from"); + if (from !== undefined && Number.isNaN(Date.parse(from))) { + throw new AuditQueryError('Query parameter "from" must be a valid date.'); + } + const to = optional("to"); + if (to !== undefined && Number.isNaN(Date.parse(to))) { + throw new AuditQueryError('Query parameter "to" must be a valid date.'); + } + return { cursor: optional("cursor"), limit, @@ -596,7 +613,7 @@ export function auditQueryFromUrl(url: URL): AuditEventQuery { actorUserId: optional("actorUserId"), targetType: optional("targetType"), targetId: optional("targetId"), - from: optional("from"), - to: optional("to"), + from, + to, }; }