import { Request, Response, NextFunction } from 'express'; import { z, ZodSchema, ZodError } from 'zod'; import { ValidationError } from './errorHandler.js'; export function validateBody(schema: ZodSchema) { return (req: Request, _res: Response, next: NextFunction): void => { try { req.body = schema.parse(req.body); next(); } catch (error) { if (error instanceof ZodError) { const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '); next(new ValidationError(messages)); } else { next(error); } } }; } export function validateQuery(schema: ZodSchema) { return (req: Request, _res: Response, next: NextFunction): void => { try { req.query = schema.parse(req.query) as any; next(); } catch (error) { if (error instanceof ZodError) { const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '); next(new ValidationError(messages)); } else { next(error); } } }; } export function validateParams(schema: ZodSchema) { return (req: Request, _res: Response, next: NextFunction): void => { try { req.params = schema.parse(req.params) as any; next(); } catch (error) { if (error instanceof ZodError) { const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '); next(new ValidationError(messages)); } else { next(error); } } }; } // Common validation schemas export const schemas = { uuid: z.string().uuid(), email: z.string().email(), // Auth schemas gmailCallback: z.object({ code: z.string().min(1, 'Authorization code is required'), }), deviceFlow: z.object({ device_code: z.string().min(1, 'Device code is required'), }), // Sync schemas syncRequest: z.object({ accountId: z.string().uuid('Invalid account ID'), }), // Action schemas - supports both single action (legacy) and actions array executeAction: z.object({ emailId: z.string().uuid('Invalid email ID'), action: z.enum(['delete', 'archive', 'draft', 'flag', 'read', 'star', 'none']).optional(), actions: z.array(z.enum(['delete', 'archive', 'draft', 'flag', 'read', 'star', 'none'])).optional(), draftContent: z.string().optional(), }).refine(data => data.action || (data.actions && data.actions.length > 0), { message: 'Either action or actions must be provided', }), // Migration schemas migrate: z.object({ projectRef: z.string().min(1, 'Project reference is required'), accessToken: z.string().min(1, 'Access token is required for automatic migration'), anonKey: z.string().optional(), // For knowledge base ingestion during migration }), // Rule schemas - supports both single action (legacy) and actions array // Now includes description and intent for context-aware AI matching // Actions support: 'delete', 'archive', 'draft', 'star', 'read', or 'label:*' (e.g., 'label:Financial') createRule: z.object({ name: z.string().min(1).max(100), description: z.string().max(500).optional(), intent: z.string().max(200).optional(), priority: z.number().int().min(0).max(100).optional(), condition: z.record(z.unknown()), negative_condition: z.record(z.unknown()).optional(), min_confidence: z.number().min(0).max(1).optional().default(0.7), action: z.union([ z.enum(['delete', 'archive', 'draft', 'star', 'read']), z.string().regex(/^label:.+/, 'Label actions must start with "label:"') ]).optional(), actions: z.array(z.union([ z.enum(['delete', 'archive', 'draft', 'star', 'read']), z.string().regex(/^label:.+/, 'Label actions must start with "label:"') ])).optional(), instructions: z.string().optional(), is_enabled: z.boolean().default(true), }).refine(data => data.action || (data.actions && data.actions.length > 0), { message: 'Either action or actions must be provided', }), updateRule: z.object({ name: z.string().min(1).max(100).optional(), description: z.string().max(500).optional(), intent: z.string().max(200).optional(), priority: z.number().int().min(0).max(100).optional(), condition: z.record(z.unknown()).optional(), negative_condition: z.record(z.unknown()).optional(), min_confidence: z.number().min(0).max(1).optional(), action: z.union([ z.enum(['delete', 'archive', 'draft', 'star', 'read']), z.string().regex(/^label:.+/, 'Label actions must start with "label:"') ]).optional(), actions: z.array(z.union([ z.enum(['delete', 'archive', 'draft', 'star', 'read']), z.string().regex(/^label:.+/, 'Label actions must start with "label:"') ])).optional(), instructions: z.string().optional(), is_enabled: z.boolean().optional(), }), // Settings schemas updateSettings: z.object({ llm_provider: z.string().optional(), llm_model: z.string().optional(), auto_trash_spam: z.boolean().optional(), smart_drafts: z.boolean().optional(), sync_interval_minutes: z.number().min(1).max(60).optional(), // BYOK Credentials (transient, moved to integrations) google_client_id: z.string().optional(), google_client_secret: z.string().optional(), microsoft_client_id: z.string().optional(), microsoft_client_secret: z.string().optional(), microsoft_tenant_id: z.string().optional(), }), // IMAP/SMTP Schema imapConnect: z.object({ email: z.string().email(), password: z.string().min(1, 'Password is required'), imapHost: z.string().min(1), imapPort: z.number().int().positive(), imapSecure: z.boolean().default(true), smtpHost: z.string().min(1), smtpPort: z.number().int().positive(), smtpSecure: z.boolean().default(true), }), };