diff --git a/src/web-server/usage/profile-filter.ts b/src/web-server/usage/profile-filter.ts index a319daa1..c3d0f2fd 100644 --- a/src/web-server/usage/profile-filter.ts +++ b/src/web-server/usage/profile-filter.ts @@ -9,8 +9,13 @@ export interface ProfileScopedUsageData { const PROFILE_NAME_REGEX = /^[A-Za-z0-9._-]+$/; -export function normalizeProfileQuery(profile?: string): string | undefined { - const value = profile?.trim(); +export function normalizeProfileQuery(profile?: unknown): string | undefined { + if (profile === undefined) return undefined; + if (typeof profile !== 'string') { + throw new Error('Invalid profile filter'); + } + + const value = profile.trim(); if (!value || value === 'all') return undefined; if (!PROFILE_NAME_REGEX.test(value)) { throw new Error('Invalid profile filter'); diff --git a/tests/unit/web-server/usage-handlers-semantics.test.ts b/tests/unit/web-server/usage-handlers-semantics.test.ts index e91aec59..dfb1de23 100644 --- a/tests/unit/web-server/usage-handlers-semantics.test.ts +++ b/tests/unit/web-server/usage-handlers-semantics.test.ts @@ -83,11 +83,7 @@ function writeAssistantEntriesToDir(baseClaudeDir: string, entries: AssistantFix }, }); - fs.writeFileSync( - path.join(projectDir, `${entry.sessionId}.jsonl`), - `${line}\n`, - 'utf-8' - ); + fs.writeFileSync(path.join(projectDir, `${entry.sessionId}.jsonl`), `${line}\n`, 'utf-8'); } } @@ -222,7 +218,10 @@ describe('usage handlers semantics', () => { res as never ); - const payload = res.payload as { success: boolean; data: Array<{ hour: string; requests: number }> }; + const payload = res.payload as { + success: boolean; + data: Array<{ hour: string; requests: number }>; + }; const targetHour = payload.data.find((row) => row.hour === '2026-03-02 10:00'); expect(targetHour?.requests).toBe(3); @@ -415,6 +414,19 @@ describe('usage handlers semantics', () => { }); }); + it('rejects non-string profile filters as validation errors', async () => { + for (const profile of [['work', 'default'], { name: 'work' }]) { + const res = createMockResponse(); + await handlers.handleSummary({ query: { profile } } as never, res as never); + + expect(res.statusCode).toBe(400); + expect(res.payload).toMatchObject({ + success: false, + error: 'Invalid profile filter', + }); + } + }); + it('rejects reversed date ranges before computing summary totals', async () => { const res = createMockResponse();