/** * Validation utilities for smartfeed * Provides security, validation, and sanitization functions */ /** * Validates that a URL is absolute and optionally prefers HTTPS * @param url - The URL to validate * @param preferHttps - Whether to warn if not HTTPS * @returns Validated URL * @throws Error if URL is invalid or not absolute */ export function validateUrl(url: string, preferHttps: boolean = true): string { if (!url || typeof url !== 'string') { throw new Error('URL must be a non-empty string'); } // Check if URL is absolute try { const parsedUrl = new URL(url); // Validate protocol if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { throw new Error(`Invalid URL protocol: ${parsedUrl.protocol}. Only http and https are supported.`); } // Prefer HTTPS for security if (preferHttps && parsedUrl.protocol === 'http:') { console.warn(`Warning: URL '${url}' uses HTTP instead of HTTPS. HTTPS is recommended for security and privacy.`); } return url; } catch (error) { if (error instanceof TypeError) { throw new Error(`Invalid or relative URL: ${url}. All URLs must be absolute (e.g., https://example.com/path)`); } throw error; } } /** * Sanitizes HTML content to prevent XSS attacks * This is a basic implementation - for production use, consider a dedicated library * @param content - The content to sanitize * @returns Sanitized content */ export function sanitizeContent(content: string): string { if (!content || typeof content !== 'string') { return ''; } // Basic HTML entity encoding to prevent XSS return content .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/\//g, '/'); } /** * Validates that required fields are present and non-empty * @param obj - Object to validate * @param requiredFields - Array of required field names * @param objectName - Name of object for error messages * @throws Error if validation fails */ export function validateRequiredFields( obj: Record, requiredFields: string[], objectName: string = 'Object' ): void { const missingFields: string[] = []; for (const field of requiredFields) { if (!obj[field] || (typeof obj[field] === 'string' && obj[field].trim() === '')) { missingFields.push(field); } } if (missingFields.length > 0) { throw new Error( `${objectName} validation failed: Missing or empty required fields: ${missingFields.join(', ')}` ); } } /** * Validates an email address format * @param email - Email to validate * @returns Validated email * @throws Error if email is invalid */ export function validateEmail(email: string): string { if (!email || typeof email !== 'string') { throw new Error('Email must be a non-empty string'); } // Basic email validation regex const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new Error(`Invalid email address: ${email}`); } return email; } /** * Validates a timestamp * @param timestamp - Timestamp to validate (milliseconds since epoch) * @returns Validated timestamp * @throws Error if timestamp is invalid */ export function validateTimestamp(timestamp: number): number { if (typeof timestamp !== 'number' || isNaN(timestamp)) { throw new Error('Timestamp must be a valid number'); } if (timestamp < 0) { throw new Error('Timestamp cannot be negative'); } // Check if timestamp is reasonable (not in far future) const now = Date.now(); const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000); if (timestamp > tenYearsFromNow) { console.warn(`Warning: Timestamp ${timestamp} is more than 10 years in the future`); } return timestamp; } /** * Validates that a domain is properly formatted * @param domain - Domain to validate * @returns Validated domain * @throws Error if domain is invalid */ export function validateDomain(domain: string): string { if (!domain || typeof domain !== 'string') { throw new Error('Domain must be a non-empty string'); } // Basic domain validation const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i; if (!domainRegex.test(domain)) { throw new Error(`Invalid domain format: ${domain}`); } return domain; } /** * Creates a validation error with context * @param message - Error message * @param context - Additional context information * @returns Error object */ export function createValidationError(message: string, context?: Record): Error { const error = new Error(message); if (context) { Object.assign(error, { context }); } return error; }