From c46b42121989c90c44eb934e0e0ecf4edae43451 Mon Sep 17 00:00:00 2001 From: ruturaj Date: Fri, 19 Sep 2025 16:06:46 +0530 Subject: [PATCH 1/3] validate the request body of api/chat to prevent malformed request body --- src/app/api/chat/route.ts | 40 ++++++------------- src/app/api/chat/validation.ts | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 28 deletions(-) create mode 100644 src/app/api/chat/validation.ts 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, + }; +} From fabb48cc2ff5ecca7486f47371afea27cb8abca3 Mon Sep 17 00:00:00 2001 From: ruturaj Date: Tue, 23 Sep 2025 14:57:55 +0530 Subject: [PATCH 2/3] move schema validation into route file and remove validation file --- src/app/api/chat/route.ts | 71 +++++++++++++++++++++++++++++++++- src/app/api/chat/validation.ts | 70 --------------------------------- 2 files changed, 69 insertions(+), 72 deletions(-) delete mode 100644 src/app/api/chat/validation.ts diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 606f070..d749cb7 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -17,11 +17,78 @@ import { getCustomOpenaiModelName, } from '@/lib/config'; import { searchHandlers } from '@/lib/search'; -import { ChatApiBody as Body, Message, safeValidateBody } from './validation'; +import { z } from 'zod'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; +// 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 +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().nullable().optional().default(''), +}); + +type Message = z.infer; +type Body = z.infer; + +// Safe validation that returns success/error +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, + }; +} + const handleEmitterEvents = async ( stream: EventEmitter, writer: WritableStreamDefaultWriter, @@ -267,7 +334,7 @@ export const POST = async (req: Request) => { embedding, body.optimizationMode, body.files, - body.systemInstructions, + body.systemInstructions as string, ); const responseStream = new TransformStream(); diff --git a/src/app/api/chat/validation.ts b/src/app/api/chat/validation.ts deleted file mode 100644 index 8bed73f..0000000 --- a/src/app/api/chat/validation.ts +++ /dev/null @@ -1,70 +0,0 @@ -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, - }; -} From 3b41905abbf8d1e839ae2c162ae874f54c799fc9 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 27 Sep 2025 19:51:36 +0530 Subject: [PATCH 3/3] feat(app): lint & beautify --- src/app/api/chat/route.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index d749cb7..65d1c6f 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -22,26 +22,22 @@ import { z } from 'zod'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; -// 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 const bodySchema = z.object({ message: messageSchema, optimizationMode: z.enum(['speed', 'balanced', 'quality'], { @@ -69,8 +65,7 @@ const bodySchema = z.object({ type Message = z.infer; type Body = z.infer; -// Safe validation that returns success/error -function safeValidateBody(data: unknown) { +const safeValidateBody = (data: unknown) => { const result = bodySchema.safeParse(data); if (!result.success) { @@ -87,7 +82,7 @@ function safeValidateBody(data: unknown) { success: true, data: result.data, }; -} +}; const handleEmitterEvents = async ( stream: EventEmitter,