import fs from 'node:fs'; import path from 'node:path'; const root = process.cwd(); const file = process.env.PORTFOLIO_CONFIG || path.join(root, 'data', 'config.json'); const example = path.join(root, 'config.example.json'); const colors = new Set(['cyan', 'yellow', 'green', 'magenta', 'blue', 'white', 'red', 'brightBlack']); function port(value, name) { if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error(`Invalid ${name}: expected an integer from 1 to 65535.`); return value; } export function validateConfig(config) { if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Configuration must be a JSON object.'); if (config.theme !== undefined) { if (!config.theme || typeof config.theme !== 'object') throw new Error('Invalid theme: expected an object.'); for (const key of ['primary', 'accent', 'muted']) if (config.theme[key] !== undefined && !colors.has(config.theme[key])) throw new Error(`Invalid theme.${key}: use one of ${[...colors].join(', ')}.`); } config.host ??= {}; config.host.port = port(config.host.port, 'host.port'); config.host.notificationPort = port(config.host.notificationPort, 'host.notificationPort'); if (typeof config.host.hostKeyPath !== 'string' || !config.host.hostKeyPath.trim()) throw new Error('Invalid host.hostKeyPath: expected a non-empty path.'); if (config.databasePath !== undefined && (typeof config.databasePath !== 'string' || !config.databasePath.trim())) throw new Error('Invalid databasePath: expected a non-empty path.'); if (config.dashboard !== undefined) { if (!config.dashboard || typeof config.dashboard !== 'object') throw new Error('Invalid dashboard: expected an object.'); if (config.dashboard.port !== undefined) config.dashboard.port = port(config.dashboard.port, 'dashboard.port'); if (config.dashboard.host !== undefined && (typeof config.dashboard.host !== 'string' || !config.dashboard.host.trim())) throw new Error('Invalid dashboard.host: expected a non-empty host.'); if (config.dashboard.tokenFile !== undefined && (typeof config.dashboard.tokenFile !== 'string' || !config.dashboard.tokenFile.trim())) throw new Error('Invalid dashboard.tokenFile: expected a non-empty path.'); } if (config.board !== undefined) { if (!config.board || typeof config.board !== 'object') throw new Error('Invalid board: expected an object.'); for (const key of ['maxCharacters', 'cooldownSeconds', 'dailyLimit']) if (config.board[key] !== undefined && (!Number.isInteger(config.board[key]) || config.board[key] < 1)) throw new Error(`Invalid board.${key}: expected a positive integer.`); if (config.board.maxCharacters > 240) throw new Error('Invalid board.maxCharacters: maximum is 240.'); } if (config.limits?.maxInputLength !== undefined && (!Number.isInteger(config.limits.maxInputLength) || config.limits.maxInputLength < 64 || config.limits.maxInputLength > 4096)) throw new Error('Invalid limits.maxInputLength: expected an integer from 64 to 4096.'); if (!Array.isArray(config.adminPublicKeys)) throw new Error('Invalid adminPublicKeys: expected an array of SSH public-key entries.'); for (const [index, entry] of config.adminPublicKeys.entries()) { const fields = typeof entry === 'string' ? entry.trim().split(/\s+/) : []; const blob = fields.length === 1 ? fields[0] : fields[1]; if (!blob || !/^[A-Za-z0-9+/]+={0,2}$/.test(blob)) throw new Error(`Invalid adminPublicKeys[${index}]: expected a base64 key blob or OpenSSH public-key line.`); } return config; } export function loadConfig() { if (!fs.existsSync(file)) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.copyFileSync(example, file); console.log(`Created ${file}; edit it to personalize your portfolio.`); } let config; try { config = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) { throw new Error(`Unable to parse ${file}: ${error.message}`); } config.host ??= {}; config.host.port = Number(process.env.PORT || config.host.port || 2222); config.host.notificationPort = Number(process.env.NOTIFICATION_PORT || config.host.notificationPort || 7777); config.dashboard ??= {}; config.dashboard.port = Number(process.env.DASHBOARD_PORT || config.dashboard.port || 3000); try { return validateConfig(config); } catch (error) { throw new Error(`Invalid configuration in ${file}: ${error.message}`); } } export const configPath = file;