import type { CliPlugin, PluginConfigValue, PluginContext } from '../types.js' /** * Validation Plugin * Provides configurable request and response validation middleware */ export const validationPlugin: CliPlugin = { id: 'validation', category: 'middleware', description: 'Configurable request and response validation middleware with Zod', config: { enableRequestValidation: { type: 'boolean', description: 'Enable request body validation', default: true, }, enableResponseValidation: { type: 'boolean', description: 'Enable response validation (development only)', default: false, }, enableQueryValidation: { type: 'boolean', description: 'Enable query parameter validation', default: true, }, enableParamValidation: { type: 'boolean', description: 'Enable path parameter validation', default: true, }, strictMode: { type: 'boolean', description: 'Strict validation mode (reject unknown fields)', default: true, }, coerceTypes: { type: 'boolean', description: 'Automatically coerce types (string to number, etc.)', default: true, }, maxBodySize: { type: 'string', description: 'Maximum request body size (e.g., 1MB, 10KB)', default: '1MB', }, enableSanitization: { type: 'boolean', description: 'Enable input sanitization', default: true, }, customErrorMessages: { type: 'boolean', description: 'Use custom error messages for validation failures', default: true, }, logValidationErrors: { type: 'boolean', description: 'Log validation errors for monitoring', default: true, }, returnDetailedErrors: { type: 'boolean', description: 'Return detailed validation errors (disable in production)', default: true, }, }, prepare(ctx: PluginContext, _config: Record) { // Add Zod for validation ctx.addDependency('zod', '^3.22.4') // Add validator for additional validation utilities ctx.addDependency('@hono/zod-validator', '^0.2.1') }, async apply(ctx: PluginContext, config: Record): Promise { const enableRequestValidation = config.enableRequestValidation !== false const enableResponseValidation = config.enableResponseValidation || false const _enableQueryValidation = config.enableQueryValidation !== false const _enableParamValidation = config.enableParamValidation !== false const _strictMode = config.strictMode !== false const _coerceTypes = config.coerceTypes !== false const maxBodySize = config.maxBodySize || '1MB' const enableSanitization = config.enableSanitization !== false const customErrorMessages = config.customErrorMessages !== false const logValidationErrors = config.logValidationErrors !== false const returnDetailedErrors = config.returnDetailedErrors !== false // Generate validation middleware file const validationMiddlewareContent = `import type { Context, MiddlewareHandler } from 'hono' import { createMiddleware } from 'hono/factory' import { HTTPException } from 'hono/http-exception' import { z } from 'zod' import { zValidator } from '@hono/zod-validator' import type { AppBindings } from './types.js' /** * Validation middleware generated by Validation Plugin */ // Common validation schemas export const commonSchemas = { // ID validation id: z.string().uuid('Invalid ID format'), // Pagination pagination: z.object({ page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(100).default(10), offset: z.coerce.number().int().min(0).optional() }), // Sorting sort: z.object({ sortBy: z.string().optional(), sortOrder: z.enum(['asc', 'desc']).default('asc') }), // Search search: z.object({ q: z.string().min(1).optional(), filter: z.string().optional() }), // Common string validations email: z.string().email('Invalid email format'), url: z.string().url('Invalid URL format'), phone: z.string().regex(/^\\+?[1-9]\\d{1,14}$/, 'Invalid phone number'), // Date validations dateString: z.string().datetime('Invalid date format'), dateRange: z.object({ startDate: z.string().datetime(), endDate: z.string().datetime() }).refine(data => new Date(data.startDate) <= new Date(data.endDate), { message: 'Start date must be before end date' }) } ${ enableSanitization ? ` /** * Input sanitization utilities */ export const sanitizers = { // Remove HTML tags and scripts stripHtml: (str: string): string => { return str .replace(/]*>.*?<\\/script>/gi, '') .replace(/<[^>]*>/g, '') .replace(/javascript:/gi, '') .replace(/on\\w+\\s*=/gi, '') }, // Normalize whitespace normalizeWhitespace: (str: string): string => { return str.replace(/\\s+/g, ' ').trim() }, // Remove null bytes removeNullBytes: (str: string): string => { return str.replace(/\\0/g, '') }, // Sanitize object recursively sanitizeObject: (obj: any): any => { if (typeof obj === 'string') { return sanitizers.removeNullBytes( sanitizers.normalizeWhitespace( sanitizers.stripHtml(obj) ) ) } if (Array.isArray(obj)) { return obj.map(sanitizers.sanitizeObject) } if (obj && typeof obj === 'object') { const sanitized: any = {} for (const [key, value] of Object.entries(obj)) { const sanitizedKey = typeof key === 'string' ? sanitizers.stripHtml(key) : key sanitized[sanitizedKey] = sanitizers.sanitizeObject(value) } return sanitized } return obj } } ` : '' } /** * Custom error formatter for validation errors */ function formatValidationError(error: z.ZodError): any { ${ customErrorMessages ? ` const formattedErrors = error.errors.map(err => ({ field: err.path.join('.'), message: err.message, code: err.code, received: err.received })) return { error: 'Validation failed', details: formattedErrors, timestamp: new Date().toISOString() } ` : ` return { error: 'Validation failed', message: error.message, timestamp: new Date().toISOString() } ` } } /** * Body size validation middleware */ export function bodySizeValidator(): MiddlewareHandler { const maxBytes = parseSize('${maxBodySize}') return createMiddleware(async (c, next) => { const contentLength = c.req.header('content-length') if (contentLength && parseInt(contentLength) > maxBytes) { ${ logValidationErrors ? ` console.warn(JSON.stringify({ type: 'validation_error', reason: 'body_too_large', contentLength: parseInt(contentLength), maxSize: maxBytes, timestamp: new Date().toISOString() })) ` : '' } throw new HTTPException(413, { message: 'Request body too large' }) } await next() }) } /** * Parse size string to bytes */ function parseSize(size: string): number { const units: Record = { B: 1, KB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024 } const match = size.match(/^(\\d+(?:\\.\\d+)?)\\s*(B|KB|MB|GB)?$/i) if (!match) return 1024 * 1024 // Default 1MB const value = parseFloat(match[1]) const unit = (match[2] || 'B').toUpperCase() return Math.floor(value * (units[unit] || 1)) } ${ enableRequestValidation ? ` /** * Request validation middleware */ export function requestValidation(): MiddlewareHandler { return createMiddleware(async (c, next) => { const method = c.req.method // Only validate methods that typically have bodies if (!['POST', 'PUT', 'PATCH'].includes(method)) { await next() return } try { const body = await c.req.json().catch(() => null) if (body && typeof body === 'object') { ${ enableSanitization ? ` // Sanitize input const sanitizedBody = sanitizers.sanitizeObject(body) // Store sanitized body for route handlers c.set('sanitizedBody', sanitizedBody) ` : '' } // Basic validation - ensure it's valid JSON object if (Array.isArray(body)) { throw new HTTPException(400, { message: 'Request body must be an object, not an array' }) } } } catch (error) { if (error instanceof HTTPException) { throw error } ${ logValidationErrors ? ` console.warn(JSON.stringify({ type: 'validation_error', reason: 'invalid_json', error: (error as Error).message, timestamp: new Date().toISOString() })) ` : '' } throw new HTTPException(400, { message: 'Invalid JSON in request body' }) } await next() }) } ` : '' } ${ enableResponseValidation ? ` /** * Response validation middleware (development only) */ export function responseValidation(): MiddlewareHandler { return createMiddleware(async (c, next) => { // Only run in development if (process.env.NODE_ENV === 'production') { await next() return } await next() try { const response = c.res.clone() const contentType = response.headers.get('content-type') if (contentType?.includes('application/json')) { const body = await response.json() // Basic response structure validation if (body && typeof body === 'object') { // Ensure error responses have proper structure if (response.status >= 400) { const errorSchema = z.object({ error: z.string(), message: z.string().optional(), details: z.any().optional(), timestamp: z.string().optional() }) const result = errorSchema.safeParse(body) if (!result.success) { console.warn('Response validation warning: Error response does not match expected structure', { status: response.status, body, validation: result.error.errors }) } } } } } catch (error) { // Don't fail the request for response validation errors console.warn('Response validation error:', (error as Error).message) } }) } ` : '' } /** * Create a validator middleware for specific schema */ export function createValidator( target: 'json' | 'query' | 'param', schema: z.ZodSchema ): MiddlewareHandler { return zValidator(target, schema, (result, c) => { if (!result.success) { ${ logValidationErrors ? ` console.warn(JSON.stringify({ type: 'validation_error', target, errors: result.error.errors, timestamp: new Date().toISOString() })) ` : '' } const errorResponse = ${returnDetailedErrors ? 'formatValidationError(result.error)' : '{ error: "Validation failed" }'} return c.json(errorResponse, 400) } }) } /** * Validation helper functions */ export const validators = { // Common validators id: () => createValidator('param', z.object({ id: commonSchemas.id })), pagination: () => createValidator('query', commonSchemas.pagination), search: () => createValidator('query', commonSchemas.search), // Custom validator creator json: (schema: z.ZodSchema) => createValidator('json', schema), query: (schema: z.ZodSchema) => createValidator('query', schema), param: (schema: z.ZodSchema) => createValidator('param', schema) } // Export schemas for use in route handlers export { z, commonSchemas } ` await ctx.addFile('src/lib/validation.middleware.ts', validationMiddlewareContent) // Update main app file to include validation middleware if (await ctx.fileExists('src/index.ts')) { await ctx.modifyFile('src/index.ts', (content: string) => { // Add import if not already present if (!content.includes('validation.middleware')) { const imports = [] if (enableRequestValidation) imports.push('requestValidation', 'bodySizeValidator') if (enableResponseValidation) imports.push('responseValidation') if (imports.length > 0) { const importLine = `import { ${imports.join(', ')} } from './lib/validation.middleware.js'` // Find the last import statement const lines = content.split('\\n') let lastImportIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith('import ')) { lastImportIndex = i } } if (lastImportIndex >= 0) { lines.splice(lastImportIndex + 1, 0, importLine) } else { lines.unshift(importLine) } content = lines.join('\\n') } } // Add middleware usage if not already present const middlewareToAdd = [] if (enableRequestValidation && !content.includes('bodySizeValidator()')) { middlewareToAdd.push("app.use('*', bodySizeValidator())") } if (enableRequestValidation && !content.includes('requestValidation()')) { middlewareToAdd.push("app.use('*', requestValidation())") } if (enableResponseValidation && !content.includes('responseValidation()')) { middlewareToAdd.push("app.use('*', responseValidation())") } if (middlewareToAdd.length > 0) { // Find where to insert middleware calls if (content.includes('const app = ') || content.includes('export const app = ')) { const lines = content.split('\\n') let insertIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].includes('const app = ') || lines[i].includes('export const app = ')) { insertIndex = i + 1 // Look for existing middleware to insert after CORS while ( insertIndex < lines.length && (lines[insertIndex].includes('pinoLogger') || lines[insertIndex].includes('corsHandler') || lines[insertIndex].trim() === '' || lines[insertIndex].startsWith('//')) ) { insertIndex++ } break } } if (insertIndex >= 0) { lines.splice(insertIndex, 0, '', '// Validation middleware', ...middlewareToAdd) content = lines.join('\\n') } } } return content }) } }, }