@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
137 lines (136 loc) • 4.2 kB
JavaScript
/**
* 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');
}
}