[grade=A] P2-6: refactor config-schema.js validateConfig (complexity 44→8 sub-validators)

Extracted 8 field-level validators from the monolithic validateConfig function:
validateTld, validateDns, validateDashboardHost, validateTimezone,
validateTheme, validateRoutingMode, validateDomain, validateKnownKeys.

ESLint complexity dropped from 44 (Error) to <10 per function.
Removed unused VALID_TIMEZONES_SAMPLE constant.
Extracted VALID_THEMES, VALID_ROUTING_MODES, VALID_DNS_PROVIDERS, KNOWN_KEYS
as module-level constants.

Behavior-preserving: same validation rules, same error/warning messages,
same return shape. 1539/1539 tests pass. ESLint: 0 problems (was 2).
This commit is contained in:
Hermes
2026-08-10 21:18:09 -07:00
parent 1bc41bb2bc
commit f5fc688185
+162 -116
View File
@@ -3,12 +3,161 @@
* Validates config.json structure to catch typos and invalid values early. * Validates config.json structure to catch typos and invalid values early.
*/ */
const VALID_TIMEZONES_SAMPLE = [ const VALID_THEMES = ['dark', 'light', 'blue'];
'UTC', 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', const VALID_ROUTING_MODES = ['subdomain', 'subdirectory'];
'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Asia/Tokyo', 'Asia/Shanghai', const VALID_DNS_PROVIDERS = ['technitium', 'cloudflare', 'rfc2136', 'manual'];
'Asia/Singapore', 'Australia/Sydney', 'Pacific/Auckland'
const KNOWN_KEYS = [
'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',
'routingMode', 'domain', 'email', 'defaultIP', 'pylon',
'customLogoDark', 'customLogoLight'
]; ];
/**
* @param {string[]} arr
* @param {string} val
* @returns {boolean}
*/
function isInArray(arr, val) {
return arr.includes(val);
}
/**
* @param {{errors:string[], warnings:string[]}} ctx
* @param {object} config
*/
function validateTld(ctx, config) {
if (config.tld === undefined) return;
if (typeof config.tld !== 'string') {
ctx.errors.push('tld must be a string');
return;
}
const tld = config.tld.startsWith('.') ? config.tld : '.' + config.tld;
if (!/^\.[a-z0-9][a-z0-9-]*$/.test(tld)) {
ctx.errors.push(`tld "${config.tld}" contains invalid characters (use lowercase alphanumeric)`);
}
if (tld.length > 20) {
ctx.warnings.push(`tld "${config.tld}" is unusually long`);
}
}
/**
* @param {{errors:string[], warnings:string[]}} ctx
* @param {object} config
*/
function validateDns(ctx, config) {
if (config.dns === undefined) return;
if (typeof config.dns !== 'object' || config.dns === null) {
ctx.errors.push('dns must be an object');
return;
}
if (config.dns.ip !== undefined && typeof config.dns.ip !== 'string') {
ctx.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)) {
ctx.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) {
ctx.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) {
ctx.errors.push('dns.servers must be an object');
}
}
if (config.dns.provider !== undefined) {
if (typeof config.dns.provider !== 'string') {
ctx.errors.push('dns.provider must be a string');
} else if (!isInArray(VALID_DNS_PROVIDERS, config.dns.provider)) {
ctx.warnings.push(`dns.provider "${config.dns.provider}" is not one of: ${VALID_DNS_PROVIDERS.join(', ')}. It may still work if a custom adapter is installed.`);
}
}
}
/**
* @param {{errors:string[], warnings:string[]}} ctx
* @param {object} config
*/
function validateDashboardHost(ctx, config) {
if (config.dashboardHost === undefined) return;
if (typeof config.dashboardHost !== 'string') {
ctx.errors.push('dashboardHost must be a string');
} else if (config.dashboardHost && !/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(config.dashboardHost)) {
ctx.errors.push(`dashboardHost "${config.dashboardHost}" contains invalid characters`);
}
}
/**
* @param {{errors:string[], warnings:string[]}} ctx
* @param {object} config
*/
function validateTimezone(ctx, config) {
if (config.timezone === undefined) return;
if (typeof config.timezone !== 'string') {
ctx.errors.push('timezone must be a string');
} else if (config.timezone) {
try {
Intl.DateTimeFormat(undefined, { timeZone: config.timezone });
} catch {
ctx.errors.push(`timezone "${config.timezone}" is not a recognized IANA timezone`);
}
}
}
/**
* @param {{errors:string[], warnings:string[]}} ctx
* @param {object} config
*/
function validateTheme(ctx, config) {
if (config.theme === undefined) return;
if (!isInArray(VALID_THEMES, config.theme)) {
ctx.warnings.push(`theme "${config.theme}" is not one of: ${VALID_THEMES.join(', ')}`);
}
}
/**
* @param {{errors:string[], warnings:string[]}} ctx
* @param {object} config
*/
function validateRoutingMode(ctx, config) {
if (config.routingMode === undefined) return;
if (!isInArray(VALID_ROUTING_MODES, config.routingMode)) {
ctx.errors.push(`routingMode "${config.routingMode}" is not one of: ${VALID_ROUTING_MODES.join(', ')}`);
}
}
/**
* @param {{errors:string[], warnings:string[]}} ctx
* @param {object} config
*/
function validateDomain(ctx, config) {
if (config.domain === undefined) return;
if (typeof config.domain !== 'string') {
ctx.errors.push('domain must be a string');
} else if (config.domain && !/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(config.domain)) {
ctx.warnings.push(`domain "${config.domain}" may not be a valid domain name`);
}
}
/**
* @param {{warnings:string[]}} ctx
* @param {object} config
*/
function validateKnownKeys(ctx, config) {
for (const key of Object.keys(config)) {
if (!isInArray(KNOWN_KEYS, key)) {
ctx.warnings.push(`Unknown config key "${key}" — possible typo?`);
}
}
}
/** /**
* Validate a config object and return errors/warnings. * Validate a config object and return errors/warnings.
* @param {object} config - The config object to validate * @param {object} config - The config object to validate
@@ -17,123 +166,20 @@ const VALID_TIMEZONES_SAMPLE = [
function validateConfig(config) { function validateConfig(config) {
const errors = []; const errors = [];
const warnings = []; const warnings = [];
const ctx = { errors, warnings };
if (!config || typeof config !== 'object') { if (!config || typeof config !== 'object') {
return { valid: false, errors: ['Config must be a non-null object'], warnings }; return { valid: false, errors: ['Config must be a non-null object'], warnings };
} }
// TLD validation validateTld(ctx, config);
if (config.tld !== undefined) { validateDns(ctx, config);
if (typeof config.tld !== 'string') { validateDashboardHost(ctx, config);
errors.push('tld must be a string'); validateTimezone(ctx, config);
} else { validateTheme(ctx, config);
const tld = config.tld.startsWith('.') ? config.tld : '.' + config.tld; validateRoutingMode(ctx, config);
if (!/^\.[a-z0-9][a-z0-9-]*$/.test(tld)) { validateDomain(ctx, config);
errors.push(`tld "${config.tld}" contains invalid characters (use lowercase alphanumeric)`); validateKnownKeys(ctx, config);
}
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');
}
}
// DNS provider validation
if (config.dns.provider !== undefined) {
const validProviders = ['technitium', 'cloudflare', 'rfc2136', 'manual'];
if (typeof config.dns.provider !== 'string') {
errors.push('dns.provider must be a string');
} else if (!validProviders.includes(config.dns.provider)) {
warnings.push(`dns.provider "${config.dns.provider}" is not one of: ${validProviders.join(', ')}. It may still work if a custom adapter is installed.`);
}
}
}
}
// 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(', ')}`);
}
}
// Routing mode validation
if (config.routingMode !== undefined) {
const validModes = ['subdomain', 'subdirectory'];
if (!validModes.includes(config.routingMode)) {
errors.push(`routingMode "${config.routingMode}" is not one of: ${validModes.join(', ')}`);
}
}
// Domain validation
if (config.domain !== undefined) {
if (typeof config.domain !== 'string') {
errors.push('domain must be a string');
} else if (config.domain && !/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(config.domain)) {
warnings.push(`domain "${config.domain}" may not be a valid domain name`);
}
}
// 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',
'routingMode', 'domain', 'email', 'defaultIP', 'pylon',
'customLogoDark', 'customLogoLight'
];
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 }; return { valid: errors.length === 0, errors, warnings };
} }