/** * Validation Middleware * * Provides validation middleware for route parameters and request bodies. */ // Session ID validation pattern // 10-64 characters, alphanumeric, underscore, hyphen // Updated to support timestamp-based session IDs like: session-1769029589191-gjqeg0i0i const SESSION_ID_PATTERN = /^[a-zA-Z0-9_-]{10,64}$/; // Terminal ID validation pattern const TERMINAL_ID_PATTERN = /^term-[0-9]{13,}-[a-zA-Z0-9]{4,}$/; /** * Validate session ID format * @param {string} sessionId - Session ID to validate * @returns {boolean} True if valid */ function validateSessionIdFormat(sessionId) { if (!sessionId) { return false; } return SESSION_ID_PATTERN.test(sessionId); } /** * Validate terminal ID format * @param {string} terminalId - Terminal ID to validate * @returns {boolean} True if valid */ function validateTerminalIdFormat(terminalId) { if (!terminalId) { return false; } return TERMINAL_ID_PATTERN.test(terminalId); } /** * Middleware to validate session ID parameter */ function validateSessionId(req, res, next) { const { sessionId } = req.params; if (!sessionId) { return res.status(400).json({ error: 'Session ID is required', statusCode: 400 }); } if (!validateSessionIdFormat(sessionId)) { return res.status(400).json({ error: 'Invalid session ID format', sessionId, expected: '10-64 characters, alphanumeric, underscore, hyphen', statusCode: 400 }); } // Check session exists // Note: We access the global claudeService instance from app.locals const claudeService = req.app.locals.claudeService; if (!claudeService) { console.error('[ValidationError] claudeService not found in app.locals'); return res.status(500).json({ error: 'Service configuration error', statusCode: 500 }); } const session = claudeService.getSession(sessionId); if (!session) { return res.status(404).json({ error: 'Session not found', sessionId, hint: 'The session may have been deleted or never existed', statusCode: 404 }); } // Attach session context to request req.sessionContext = session; next(); } /** * Middleware to validate terminal ID parameter */ function validateTerminalId(req, res, next) { const { terminalId } = req.params; if (!terminalId) { return res.status(400).json({ error: 'Terminal ID is required', statusCode: 400 }); } if (!validateTerminalIdFormat(terminalId)) { return res.status(400).json({ error: 'Invalid terminal ID format', terminalId, expected: 'Format: term-{timestamp}-{random}', statusCode: 400 }); } // Check terminal exists // Note: We access the global terminalService instance from app.locals const terminalService = req.app.locals.terminalService; if (!terminalService) { console.error('[ValidationError] terminalService not found in app.locals'); return res.status(500).json({ error: 'Service configuration error', statusCode: 500 }); } const result = terminalService.getTerminal(terminalId); if (!result.success) { return res.status(404).json({ error: 'Terminal not found', terminalId, hint: 'The terminal may have been closed', statusCode: 404 }); } // Attach terminal context to request req.terminalContext = result.terminal; next(); } /** * Middleware to validate command/prompt body */ function validateCommand(req, res, next) { const { command } = req.body; if (!command || typeof command !== 'string') { return res.status(400).json({ error: 'Command is required and must be a string', statusCode: 400 }); } if (command.trim().length === 0) { return res.status(400).json({ error: 'Command cannot be empty', statusCode: 400 }); } if (command.length > 100000) { return res.status(400).json({ error: 'Command too large (max 100KB)', statusCode: 400 }); } next(); } /** * Middleware to validate operations array */ function validateOperations(req, res, next) { const { operations } = req.body; if (!operations || !Array.isArray(operations)) { return res.status(400).json({ error: 'Operations array is required', statusCode: 400 }); } if (operations.length === 0) { return res.status(400).json({ error: 'Operations array cannot be empty', statusCode: 400 }); } if (operations.length > 1000) { return res.status(400).json({ error: 'Too many operations (max 1000)', statusCode: 400 }); } // Validate each operation for (let i = 0; i < operations.length; i++) { const op = operations[i]; if (!op.type) { return res.status(400).json({ error: `Operation at index ${i} missing 'type' field`, operation: op, statusCode: 400 }); } const validTypes = ['read', 'write', 'delete', 'list', 'search']; if (!validTypes.includes(op.type)) { return res.status(400).json({ error: `Invalid operation type at index ${i}: ${op.type}`, validTypes, statusCode: 400 }); } } next(); } /** * Middleware to validate response for operations preview */ function validateResponse(req, res, next) { const { response } = req.body; if (!response || typeof response !== 'string') { return res.status(400).json({ error: 'Response is required and must be a string', statusCode: 400 }); } if (response.trim().length === 0) { return res.status(400).json({ error: 'Response cannot be empty', statusCode: 400 }); } if (response.length > 10000000) { return res.status(400).json({ error: 'Response too large (max 10MB)', statusCode: 400 }); } next(); } /** * Error handler middleware */ function errorHandler(err, req, res, next) { console.error('[ErrorHandler]', err); // Handle validation errors if (err.name === 'ValidationError') { return res.status(400).json({ error: 'Validation error', details: err.message, statusCode: 400 }); } // Handle not found errors if (err.name === 'NotFoundError') { return res.status(404).json({ error: 'Resource not found', details: err.message, statusCode: 404 }); } // Default error response res.status(500).json({ error: 'Internal server error', message: err.message, statusCode: 500 }); } module.exports = { validateSessionIdFormat, validateTerminalIdFormat, validateSessionId, validateTerminalId, validateCommand, validateOperations, validateResponse, errorHandler };