/** * Route Validators * * This file provides utility functions for validating route configurations. * These validators help ensure that route configurations are valid and correctly structured. */ import type { IRouteConfig, IRouteMatch, IRouteAction, TPortRange } from '../models/route-types.js'; /** * Validates a port range or port number * @param port Port number or port range * @returns True if valid, false otherwise */ export function isValidPort(port: TPortRange): boolean { if (typeof port === 'number') { return port > 0 && port < 65536; // Valid port range is 1-65535 } else if (Array.isArray(port)) { return port.every(p => typeof p === 'number' && p > 0 && p < 65536); } return false; } /** * Validates a domain string * @param domain Domain string to validate * @returns True if valid, false otherwise */ export function isValidDomain(domain: string): boolean { // Basic domain validation regex - allows wildcards (*.example.com) const domainRegex = /^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/; return domainRegex.test(domain); } /** * Validates a route match configuration * @param match Route match configuration to validate * @returns { valid: boolean, errors: string[] } Validation result */ export function validateRouteMatch(match: IRouteMatch): { valid: boolean; errors: string[] } { const errors: string[] = []; // Validate ports if (match.ports !== undefined) { if (!isValidPort(match.ports)) { errors.push('Invalid port number or port range in match.ports'); } } // Validate domains if (match.domains !== undefined) { if (typeof match.domains === 'string') { if (!isValidDomain(match.domains)) { errors.push(`Invalid domain format: ${match.domains}`); } } else if (Array.isArray(match.domains)) { for (const domain of match.domains) { if (!isValidDomain(domain)) { errors.push(`Invalid domain format: ${domain}`); } } } else { errors.push('Domains must be a string or an array of strings'); } } // Validate path if (match.path !== undefined) { if (typeof match.path !== 'string' || !match.path.startsWith('/')) { errors.push('Path must be a string starting with /'); } } return { valid: errors.length === 0, errors }; } /** * Validates a route action configuration * @param action Route action configuration to validate * @returns { valid: boolean, errors: string[] } Validation result */ export function validateRouteAction(action: IRouteAction): { valid: boolean; errors: string[] } { const errors: string[] = []; // Validate action type if (!action.type) { errors.push('Action type is required'); } else if (!['forward', 'redirect', 'static', 'block'].includes(action.type)) { errors.push(`Invalid action type: ${action.type}`); } // Validate target for 'forward' action if (action.type === 'forward') { if (!action.target) { errors.push('Target is required for forward action'); } else { // Validate target host if (!action.target.host) { errors.push('Target host is required'); } // Validate target port if (!action.target.port || !isValidPort(action.target.port)) { errors.push('Valid target port is required'); } } // Validate TLS options for forward actions if (action.tls) { if (!['passthrough', 'terminate', 'terminate-and-reencrypt'].includes(action.tls.mode)) { errors.push(`Invalid TLS mode: ${action.tls.mode}`); } // For termination modes, validate certificate if (['terminate', 'terminate-and-reencrypt'].includes(action.tls.mode)) { if (action.tls.certificate !== 'auto' && (!action.tls.certificate || !action.tls.certificate.key || !action.tls.certificate.cert)) { errors.push('Certificate must be "auto" or an object with key and cert properties'); } } } } // Validate redirect for 'redirect' action if (action.type === 'redirect') { if (!action.redirect) { errors.push('Redirect configuration is required for redirect action'); } else { if (!action.redirect.to) { errors.push('Redirect target (to) is required'); } if (action.redirect.status && ![301, 302, 303, 307, 308].includes(action.redirect.status)) { errors.push('Invalid redirect status code'); } } } // Validate static file config for 'static' action if (action.type === 'static') { if (!action.static) { errors.push('Static file configuration is required for static action'); } else { if (!action.static.root) { errors.push('Static file root directory is required'); } } } return { valid: errors.length === 0, errors }; } /** * Validates a complete route configuration * @param route Route configuration to validate * @returns { valid: boolean, errors: string[] } Validation result */ export function validateRouteConfig(route: IRouteConfig): { valid: boolean; errors: string[] } { const errors: string[] = []; // Check for required properties if (!route.match) { errors.push('Route match configuration is required'); } if (!route.action) { errors.push('Route action configuration is required'); } // Validate match configuration if (route.match) { const matchValidation = validateRouteMatch(route.match); if (!matchValidation.valid) { errors.push(...matchValidation.errors.map(err => `Match: ${err}`)); } } // Validate action configuration if (route.action) { const actionValidation = validateRouteAction(route.action); if (!actionValidation.valid) { errors.push(...actionValidation.errors.map(err => `Action: ${err}`)); } } // Ensure the route has a unique identifier if (!route.id && !route.name) { errors.push('Route should have either an id or a name for identification'); } return { valid: errors.length === 0, errors }; } /** * Validate an array of route configurations * @param routes Array of route configurations to validate * @returns { valid: boolean, errors: { index: number, errors: string[] }[] } Validation result */ export function validateRoutes(routes: IRouteConfig[]): { valid: boolean; errors: { index: number; errors: string[] }[] } { const results: { index: number; errors: string[] }[] = []; routes.forEach((route, index) => { const validation = validateRouteConfig(route); if (!validation.valid) { results.push({ index, errors: validation.errors }); } }); return { valid: results.length === 0, errors: results }; } /** * Check if a route configuration has the required properties for a specific action type * @param route Route configuration to check * @param actionType Expected action type * @returns True if the route has the necessary properties, false otherwise */ export function hasRequiredPropertiesForAction(route: IRouteConfig, actionType: string): boolean { if (!route.action || route.action.type !== actionType) { return false; } switch (actionType) { case 'forward': return !!route.action.target && !!route.action.target.host && !!route.action.target.port; case 'redirect': return !!route.action.redirect && !!route.action.redirect.to; case 'static': return !!route.action.static && !!route.action.static.root; case 'block': return true; // Block action doesn't require additional properties default: return false; } } /** * Throws an error if the route config is invalid, returns the config if valid * Useful for immediate validation when creating routes * @param route Route configuration to validate * @returns The validated route configuration * @throws Error if the route configuration is invalid */ export function assertValidRoute(route: IRouteConfig): IRouteConfig { const validation = validateRouteConfig(route); if (!validation.valid) { throw new Error(`Invalid route configuration: ${validation.errors.join(', ')}`); } return route; }