Files
smartfeed/ts/validation.ts

168 lines
4.7 KiB
TypeScript

/**
* 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
}
/**
* 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<string, any>,
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<string, any>): Error {
const error = new Error(message);
if (context) {
Object.assign(error, { context });
}
return error;
}