Skip to main content
Glama
validation.ts12.2 kB
import { Request, Response, NextFunction } from 'express'; import { z, ZodSchema, ZodError } from 'zod'; import { InputValidator } from '../utils/input-sanitizer'; /** * Validation result interface */ interface ValidationResult { success: boolean; data?: any; errors?: ValidationError[]; } /** * Validation error structure */ interface ValidationError { field: string; message: string; code: string; value?: any; } /** * Validation middleware options */ export interface ValidationOptions { /** Validate request body */ body?: ZodSchema; /** Validate query parameters */ query?: ZodSchema; /** Validate URL parameters */ params?: ZodSchema; /** Validate request headers */ headers?: ZodSchema; /** Strip unknown fields from the validated data */ stripUnknown?: boolean; /** Allow partial validation (useful for PATCH requests) */ partial?: boolean; /** Custom error message */ errorMessage?: string; /** Additional security validation */ securityChecks?: boolean; /** Sanitize after validation */ sanitize?: boolean; } /** * Common Zod schemas for API validation */ export const CommonSchemas = { /** Standard pagination parameters */ pagination: z.object({ page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(100).default(20), offset: z.coerce.number().int().min(0).optional(), }), /** Form ID parameter validation */ formId: z.object({ formId: z.string().min(1, 'Form ID is required').refine( (val) => InputValidator.isValidId(val), 'Form ID contains invalid characters' ), }), /** Workspace ID parameter validation */ workspaceId: z.object({ workspaceId: z.string().min(1, 'Workspace ID is required').refine( (val) => InputValidator.isValidId(val), 'Workspace ID contains invalid characters' ), }), /** User ID parameter validation */ userId: z.object({ userId: z.string().min(1, 'User ID is required').refine( (val) => InputValidator.isValidId(val), 'User ID contains invalid characters' ), }), /** Submission ID parameter validation */ submissionId: z.object({ submissionId: z.string().min(1, 'Submission ID is required').refine( (val) => InputValidator.isValidId(val), 'Submission ID contains invalid characters' ), }), /** Date range query validation */ dateRange: z.object({ startDate: z.string().datetime().optional(), endDate: z.string().datetime().optional(), timezone: z.string().optional(), }).refine( (data) => { if (data.startDate && data.endDate) { return new Date(data.startDate) <= new Date(data.endDate); } return true; }, 'Start date must be before or equal to end date' ), /** File upload validation */ fileUpload: z.object({ filename: z.string().min(1).max(255), mimetype: z.string().refine( (val) => /^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_.]*$/.test(val), 'Invalid MIME type format' ), size: z.number().int().min(1).max(50 * 1024 * 1024), // 50MB max }), /** API key validation */ apiKey: z.object({ apiKey: z.string().min(20).max(200).refine( (val) => /^[A-Za-z0-9_\-]+$/.test(val), 'API key contains invalid characters' ), }), /** Email validation */ email: z.object({ email: z.string().email('Invalid email format').refine( (val) => InputValidator.isValidEmail(val), 'Email contains potentially unsafe characters' ), }), /** URL validation */ url: z.object({ url: z.string().url('Invalid URL format').refine( (val) => InputValidator.isValidUrl(val), 'URL contains unsafe protocol or characters' ), }), /** Search query validation */ search: z.object({ q: z.string().min(1).max(200).refine( (val) => !InputValidator.containsSqlInjectionPatterns(val), 'Search query contains potentially dangerous patterns' ).refine( (val) => !InputValidator.containsXssPatterns(val), 'Search query contains potentially malicious content' ), fields: z.array(z.string()).optional(), sort: z.enum(['asc', 'desc']).optional(), }), }; /** * Creates a validation middleware with the specified schemas */ export function createValidationMiddleware(options: ValidationOptions) { return (req: Request, res: Response, next: NextFunction): void => { const errors: ValidationError[] = []; try { // Validate request body if (options.body) { const bodyResult = validateData(req.body || {}, options.body, 'body', options); if (!bodyResult.success) { errors.push(...bodyResult.errors!); } else { req.body = bodyResult.data; } } // Validate query parameters if (options.query) { const queryResult = validateData(req.query || {}, options.query, 'query', options); if (!queryResult.success) { errors.push(...queryResult.errors!); } else { req.query = queryResult.data; } } // Validate URL parameters if (options.params) { const paramsResult = validateData(req.params || {}, options.params, 'params', options); if (!paramsResult.success) { errors.push(...paramsResult.errors!); } else { req.params = paramsResult.data; } } // Validate headers if (options.headers) { const headersResult = validateData(req.headers || {}, options.headers, 'headers', options); if (!headersResult.success) { errors.push(...headersResult.errors!); } } // Additional security checks if (options.securityChecks) { const securityErrors = performSecurityChecks(req); errors.push(...securityErrors); } // If there are validation errors, respond with error if (errors.length > 0) { res.status(400).json({ error: 'Validation failed', message: options.errorMessage || 'Request data is invalid', details: errors, }); return; } next(); } catch (error) { res.status(500).json({ error: 'Validation error', message: 'An error occurred during request validation', }); return; } }; } /** * Validates data against a Zod schema */ function validateData( data: any, schema: ZodSchema, field: string, options: ValidationOptions ): ValidationResult { try { let processedSchema = schema; // Apply partial validation if requested if (options.partial && 'partial' in schema) { processedSchema = (schema as any).partial(); } // Strip unknown fields if requested if (options.stripUnknown && 'strip' in processedSchema) { processedSchema = (processedSchema as any).strip(); } const result = processedSchema.parse(data); return { success: true, data: result, }; } catch (error) { if (error instanceof ZodError) { const errors: ValidationError[] = error.errors.map(err => ({ field: `${field}.${err.path.join('.')}`, message: err.message, code: err.code, value: err.path.reduce((obj, key) => obj?.[key], data), })); return { success: false, errors, }; } return { success: false, errors: [{ field, message: 'Validation failed', code: 'validation_error', }], }; } } /** * Performs additional security checks on the request */ function performSecurityChecks(req: Request): ValidationError[] { const errors: ValidationError[] = []; // Check for suspicious patterns in all string values const checkObject = (obj: any, prefix: string) => { if (typeof obj === 'string') { if (InputValidator.containsSqlInjectionPatterns(obj)) { errors.push({ field: prefix, message: 'Contains potential SQL injection patterns', code: 'security_violation', value: obj, }); } if (InputValidator.containsXssPatterns(obj)) { errors.push({ field: prefix, message: 'Contains potential XSS patterns', code: 'security_violation', value: obj, }); } } else if (typeof obj === 'object' && obj !== null) { Object.keys(obj).forEach(key => { checkObject(obj[key], `${prefix}.${key}`); }); } }; // Check body, query, and params if (req.body) checkObject(req.body, 'body'); if (req.query) checkObject(req.query, 'query'); if (req.params) checkObject(req.params, 'params'); // Check for excessively long values that might be attack attempts const checkLength = (obj: any, prefix: string, maxLength: number = 10000) => { if (typeof obj === 'string' && obj.length > maxLength) { errors.push({ field: prefix, message: `Value exceeds maximum length of ${maxLength} characters`, code: 'length_violation', value: `${obj.substring(0, 100)}...`, }); } else if (typeof obj === 'object' && obj !== null) { Object.keys(obj).forEach(key => { checkLength(obj[key], `${prefix}.${key}`, maxLength); }); } }; if (req.body) checkLength(req.body, 'body'); if (req.query) checkLength(req.query, 'query', 1000); // Shorter limit for query params return errors; } /** * Pre-built validation middleware for common scenarios */ export const ValidationMiddleware = { /** Validate pagination parameters */ pagination: createValidationMiddleware({ query: CommonSchemas.pagination, }), /** Validate form ID in URL parameters */ formParams: createValidationMiddleware({ params: CommonSchemas.formId, securityChecks: true, }), /** Validate workspace ID in URL parameters */ workspaceParams: createValidationMiddleware({ params: CommonSchemas.workspaceId, securityChecks: true, }), /** Validate user ID in URL parameters */ userParams: createValidationMiddleware({ params: CommonSchemas.userId, securityChecks: true, }), /** Validate submission ID in URL parameters */ submissionParams: createValidationMiddleware({ params: CommonSchemas.submissionId, securityChecks: true, }), /** Validate search queries */ search: createValidationMiddleware({ query: CommonSchemas.search, securityChecks: true, }), /** Validate file upload metadata */ fileUpload: createValidationMiddleware({ body: CommonSchemas.fileUpload, securityChecks: true, }), /** Validate API key authentication */ apiKey: createValidationMiddleware({ headers: z.object({ 'x-api-key': z.string().min(20).max(200), }), securityChecks: true, }), /** Comprehensive validation for MCP tool requests */ mcpRequest: createValidationMiddleware({ body: z.object({ method: z.string().min(1).max(100), params: z.record(z.any()).optional(), }), stripUnknown: true, securityChecks: true, }), }; /** * Helper function to validate data outside of middleware context */ export function validateWithSchema<T>( data: unknown, schema: ZodSchema<T>, options: { stripUnknown?: boolean; partial?: boolean } = {} ): { success: true; data: T } | { success: false; errors: ValidationError[] } { const result = validateData(data, schema, 'data', options); if (result.success) { return { success: true, data: result.data }; } return { success: false, errors: result.errors! }; } /** * Creates a typed validator function for a specific schema */ export function createTypedValidator<T>(schema: ZodSchema<T>) { return (data: unknown): data is T => { try { schema.parse(data); return true; } catch { return false; } }; } /** * Error response formatter for validation failures */ export function formatValidationError(errors: ValidationError[]) { return { error: 'Validation failed', message: 'The provided data is invalid', details: errors.map(err => ({ field: err.field, message: err.message, code: err.code, })), }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server