/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /** * Validates that a URL is a proper base64 data string. */ export function validateDataUrl(url: string): boolean { return /^data:image\/(jpeg|png|gif|webp|svg\+xml);base64,[A-Za-z0-9+/=]+$/.test(url); } /** * Validates that a URL is safe and points to an allowed protocol. * Prevents javascript:, data:, vbscript:, and file: URLs. */ export function validateUrl(url: string): boolean { if (!url) { return true; // Empty URLs are acceptable for optional fields } // Check for dangerous protocols const dangerousProtocols = /^(javascript|data|vbscript|file):/i; if (dangerousProtocols.test(url)) { return false; } // Must be a valid URL format try { const parsedUrl = new URL(url); // Only allow http and https protocols if (!["http:", "https:"].includes(parsedUrl.protocol)) { return false; } return true; } catch { return false; } } /** * Validates string length is within acceptable bounds. */ export function validateStringLength( value: string | undefined, maxLength: number ): boolean { if (!value) { return true; } return value.length <= maxLength; } /** * Validates that a slug contains only safe characters (alphanumeric, hyphens, underscores). */ export function validateSlug(slug: string | undefined): boolean { if (!slug) { return true; } // Allow alphanumeric, hyphens, underscores only const slugPattern = /^[a-z0-9-_]+$/i; return slugPattern.test(slug) && slug.length <= 50; } /** * Validates rating is within acceptable range. */ export function validateRating(rating: number | undefined): boolean { if (rating === undefined) { return true; } return Number.isInteger(rating) && rating >= 0 && rating <= 10; } /** * Maximum string lengths for various fields. */ export const MAX_LENGTHS = { TITLE: 500, AUTHOR: 200, DESCRIPTION: 5000, BIO: 1000, SLUG: 50, URL: 2048, DISPLAY_NAME: 100, USERNAME: 100, COMMENT_CONTENT: 10000, NOTES: 5000, TAGS: 50, // per tag ISBN: 50, DATA_URL: 5 * 1024 * 1024, // 5MB in bytes (not chars) } as const;