UNPKG

@the_cfdude/productboard-mcp

Version:

Model Context Protocol server for Productboard REST API with dynamic tool loading

137 lines (136 loc) 4.2 kB
/** * Input validation and sanitization utilities */ import { ValidationError } from '../errors/index.js'; // Maximum allowed string lengths const MAX_DESCRIPTION_LENGTH = 5000; const MAX_ARRAY_LENGTH = 100; const MAX_URL_LENGTH = 2048; const MAX_EMAIL_LENGTH = 254; // Regex patterns const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const URL_REGEX = /^https?:\/\/.+/; /** * Sanitize a string input */ export function sanitizeString(value, field, maxLength = MAX_DESCRIPTION_LENGTH) { if (typeof value !== 'string') { throw new ValidationError(`${field} must be a string`, field); } const trimmed = value.trim(); if (trimmed.length === 0) { throw new ValidationError(`${field} cannot be empty`, field); } if (trimmed.length > maxLength) { throw new ValidationError(`${field} exceeds maximum length of ${maxLength}`, field); } // Remove potential SQL injection characters const sanitized = trimmed .replace(/[<>]/g, '') // Remove HTML tags .replace(/\0/g, ''); // Remove null bytes return sanitized; } /** * Validate and sanitize email */ export function validateEmail(value, field = 'email') { const email = sanitizeString(value, field, MAX_EMAIL_LENGTH); if (!EMAIL_REGEX.test(email)) { throw new ValidationError(`Invalid email format`, field); } return email.toLowerCase(); } /** * Validate and sanitize URL */ export function validateUrl(value, field = 'url') { const url = sanitizeString(value, field, MAX_URL_LENGTH); if (!URL_REGEX.test(url)) { throw new ValidationError(`Invalid URL format`, field); } try { new URL(url); // Additional validation return url; } catch { throw new ValidationError(`Invalid URL format`, field); } } /** * Validate array input */ export function validateArray(value, field, validator) { if (!Array.isArray(value)) { throw new ValidationError(`${field} must be an array`, field); } if (value.length > MAX_ARRAY_LENGTH) { throw new ValidationError(`${field} exceeds maximum length of ${MAX_ARRAY_LENGTH}`, field); } if (validator) { return value.map((item, index) => validator(item, index)); } return value; } /** * Validate object has required fields */ export function validateRequired(obj, requiredFields) { for (const field of requiredFields) { if (obj[field] === undefined || obj[field] === null) { throw new ValidationError(`${String(field)} is required`, String(field)); } } } export function validatePagination(params) { const limit = params.limit !== undefined ? Number(params.limit) : 50; const offset = params.offset !== undefined ? Number(params.offset) : 0; if (isNaN(limit) || limit < 1 || limit > 100) { throw new ValidationError('Limit must be between 1 and 100', 'limit'); } if (isNaN(offset) || offset < 0) { throw new ValidationError('Offset must be non-negative', 'offset'); } return { limit, offset }; } /** * Validate date string */ export function validateDate(value, field) { const dateStr = sanitizeString(value, field, 30); const date = new Date(dateStr); if (isNaN(date.getTime())) { throw new ValidationError(`Invalid date format`, field); } return date.toISOString(); } /** * Validate enum value */ export function validateEnum(value, validValues, field) { if (!validValues.includes(value)) { throw new ValidationError(`Invalid ${field}. Must be one of: ${validValues.join(', ')}`, field); } return value; } /** * Sanitize object by removing undefined/null values */ export function sanitizeObject(obj) { const cleaned = {}; for (const [key, value] of Object.entries(obj)) { if (value !== undefined && value !== null) { cleaned[key] = value; } } return cleaned; } /** * Validate request size */ export function validateRequestSize(data) { const size = JSON.stringify(data).length; const maxSize = 1024 * 1024; // 1MB if (size > maxSize) { throw new ValidationError('Request payload too large'); } }