Files
ssh-portfolio/src/config.js
T

61 lines
4.4 KiB
JavaScript

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;