168 lines
4.7 KiB
TypeScript
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, '<')
|
|
.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<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;
|
|
}
|