diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index ba88da6..606f070 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -17,37 +17,11 @@ import { getCustomOpenaiModelName, } from '@/lib/config'; import { searchHandlers } from '@/lib/search'; +import { ChatApiBody as Body, Message, safeValidateBody } from './validation'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; -type Message = { - messageId: string; - chatId: string; - content: string; -}; - -type ChatModel = { - provider: string; - name: string; -}; - -type EmbeddingModel = { - provider: string; - name: string; -}; - -type Body = { - message: Message; - optimizationMode: 'speed' | 'balanced' | 'quality'; - focusMode: string; - history: Array<[string, string]>; - files: Array; - chatModel: ChatModel; - embeddingModel: EmbeddingModel; - systemInstructions: string; -}; - const handleEmitterEvents = async ( stream: EventEmitter, writer: WritableStreamDefaultWriter, @@ -187,7 +161,17 @@ const handleHistorySave = async ( export const POST = async (req: Request) => { try { - const body = (await req.json()) as Body; + const reqBody = (await req.json()) as Body; + + const parseBody = safeValidateBody(reqBody); + if (!parseBody.success) { + return Response.json( + { message: 'Invalid request body', error: parseBody.error }, + { status: 400 }, + ); + } + + const body = parseBody.data as Body; const { message } = body; if (message.content === '') { diff --git a/src/app/api/chat/validation.ts b/src/app/api/chat/validation.ts new file mode 100644 index 0000000..8bed73f --- /dev/null +++ b/src/app/api/chat/validation.ts @@ -0,0 +1,70 @@ +import { z } from 'zod'; + +// Message schema +const messageSchema = z.object({ + messageId: z.string().min(1, 'Message ID is required'), + chatId: z.string().min(1, 'Chat ID is required'), + content: z.string().min(1, 'Message content is required'), +}); + +// ChatModel schema +const chatModelSchema = z.object({ + provider: z.string().optional(), + name: z.string().optional(), +}); + +// EmbeddingModel schema +const embeddingModelSchema = z.object({ + provider: z.string().optional(), + name: z.string().optional(), +}); + +// Main Body schema +export const bodySchema = z.object({ + message: messageSchema, + optimizationMode: z.enum(['speed', 'balanced', 'quality'], { + errorMap: () => ({ + message: 'Optimization mode must be one of: speed, balanced, quality', + }), + }), + focusMode: z.string().min(1, 'Focus mode is required'), + history: z + .array( + z.tuple([z.string(), z.string()], { + errorMap: () => ({ + message: 'History items must be tuples of two strings', + }), + }), + ) + .optional() + .default([]), + files: z.array(z.string()).optional().default([]), + chatModel: chatModelSchema.optional().default({}), + embeddingModel: embeddingModelSchema.optional().default({}), + systemInstructions: z.string().optional().default(''), +}); + +export type Message = z.infer; +export type ChatModel = z.infer; +export type EmbeddingModel = z.infer; +export type ChatApiBody = z.infer; + +// Safe validation that returns success/error +export function safeValidateBody(data: unknown) { + const result = bodySchema.safeParse(data); + + if (!result.success) { + return { + success: false, + error: result.error.errors.map((e) => ({ + path: e.path.join('.'), + message: e.message, + })), + }; + } + + return { + success: true, + data: result.data, + }; +}