/** * Config Schema Validation for DashCaddy * Validates config.json structure to catch typos and invalid values early. */ const VALID_TIMEZONES_SAMPLE = [ 'UTC', 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Singapore', 'Australia/Sydney', 'Pacific/Auckland' ]; /** * Validate a config object and return errors/warnings. * @param {object} config - The config object to validate * @returns {{ valid: boolean, errors: string[], warnings: string[] }} */ function validateConfig(config) { const errors = []; const warnings = []; if (!config || typeof config !== 'object') { return { valid: false, errors: ['Config must be a non-null object'], warnings }; } // TLD validation if (config.tld !== undefined) { if (typeof config.tld !== 'string') { errors.push('tld must be a string'); } else { const tld = config.tld.startsWith('.') ? config.tld : '.' + config.tld; if (!/^\.[a-z0-9][a-z0-9-]*$/.test(tld)) { errors.push(`tld "${config.tld}" contains invalid characters (use lowercase alphanumeric)`); } if (tld.length > 20) { warnings.push(`tld "${config.tld}" is unusually long`); } } } // DNS config validation if (config.dns !== undefined) { if (typeof config.dns !== 'object' || config.dns === null) { errors.push('dns must be an object'); } else { if (config.dns.ip !== undefined && typeof config.dns.ip !== 'string') { errors.push('dns.ip must be a string'); } if (config.dns.ip && !/^[\d.]+$/.test(config.dns.ip) && !/^[a-zA-Z0-9.-]+$/.test(config.dns.ip)) { errors.push(`dns.ip "${config.dns.ip}" is not a valid IP address or hostname`); } if (config.dns.port !== undefined) { const port = parseInt(config.dns.port, 10); if (isNaN(port) || port < 1 || port > 65535) { errors.push(`dns.port "${config.dns.port}" is not a valid port number (1-65535)`); } } if (config.dns.servers !== undefined) { if (typeof config.dns.servers !== 'object' || config.dns.servers === null) { errors.push('dns.servers must be an object'); } } } } // Dashboard host validation if (config.dashboardHost !== undefined) { if (typeof config.dashboardHost !== 'string') { errors.push('dashboardHost must be a string'); } else if (config.dashboardHost && !/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(config.dashboardHost)) { errors.push(`dashboardHost "${config.dashboardHost}" contains invalid characters`); } } // Timezone validation if (config.timezone !== undefined) { if (typeof config.timezone !== 'string') { errors.push('timezone must be a string'); } else if (config.timezone) { // Basic format check — full validation would require Intl API try { Intl.DateTimeFormat(undefined, { timeZone: config.timezone }); } catch { errors.push(`timezone "${config.timezone}" is not a recognized IANA timezone`); } } } // Theme validation if (config.theme !== undefined) { const validThemes = ['dark', 'light', 'blue']; if (!validThemes.includes(config.theme)) { warnings.push(`theme "${config.theme}" is not one of: ${validThemes.join(', ')}`); } } // Warn on unknown top-level keys const knownKeys = [ 'tld', 'caName', 'dns', 'dnsServers', 'dashboardHost', 'timezone', 'theme', 'updatedAt', 'timestamp', 'logo', 'logoPosition', 'favicon', 'weather', 'setupComplete', 'setupCompleted', 'setupMode', 'onboardingCompleted', 'configurationType', 'defaults', 'customLogo', 'customFavicon', 'dashboardTitle', 'tailscale', 'license', 'skipped' ]; for (const key of Object.keys(config)) { if (!knownKeys.includes(key)) { warnings.push(`Unknown config key "${key}" — possible typo?`); } } return { valid: errors.length === 0, errors, warnings }; } module.exports = { validateConfig };