diff options
Diffstat (limited to 'gateway/src/config.js')
| -rw-r--r-- | gateway/src/config.js | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/gateway/src/config.js b/gateway/src/config.js new file mode 100644 index 0000000..33cad84 --- /dev/null +++ b/gateway/src/config.js @@ -0,0 +1,97 @@ +// Loads the same config.toml + .env the Flask dashboard uses (see config.py). +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parse as parseToml } from "smol-toml"; + +export const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +// [section, key, ENV_VAR, type]. Keep in sync with ENV_OVERRIDES in config.py. +const ENV_OVERRIDES = [ + ["web", "host", "WEB_HOST", "string"], + ["web", "port", "WEB_PORT", "int"], + ["web", "dashboard_port", "DASHBOARD_PORT", "int"], + ["server", "host", "TS_HOST", "string"], + ["server", "webquery_port", "TS_WEBQUERY_PORT", "int"], + ["server", "server_id", "TS_SERVER_ID", "string"], + ["server", "voice_port", "TS_VOICE_PORT", "int"], + ["server", "voice_port", "TS_PORT", "int"], // legacy name from the old .env.template + ["server", "ts_domain", "TS_DOMAIN", "string"], + ["voice", "enabled", "VOICE_ENABLED", "bool"], + ["voice", "trust_proxy", "VOICE_TRUST_PROXY", "bool"], +]; + +function envValue(raw, kind) { + const value = raw.trim(); + if (kind === "bool") return ["1", "true", "yes", "on"].includes(value.toLowerCase()); + if (kind === "int") { + if (!/^\d+$/.test(value)) throw new Error("not an integer"); + return Number(value); + } + return value; +} + +function toInt(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) { + const n = Number(value); + return Number.isInteger(n) && n >= min && n <= max ? n : fallback; +} + +export function loadConfig(env = process.env) { + const envFile = resolve(ROOT_DIR, ".env"); + // Like python-dotenv's load_dotenv(): existing variables are not overridden. + if (env === process.env && existsSync(envFile)) process.loadEnvFile(envFile); + + const configFile = env.TS6_CONFIG_FILE || resolve(ROOT_DIR, "config.toml"); + let raw = {}; + if (existsSync(configFile)) raw = parseToml(readFileSync(configFile, "utf8")); + else console.warn(`[config] ${configFile} not found, using defaults`); + + const section = (name) => ({ ...(raw[name] ?? {}) }); + const web = section("web"); + const server = section("server"); + const voice = section("voice"); + + const sections = { web, server, voice }; + const applied = new Set(); + for (const [name, key, envName, kind] of ENV_OVERRIDES) { + const value = env[envName]; + const id = `${name}.${key}`; + if (value === undefined || value.trim() === "" || applied.has(id)) continue; + try { + sections[name][key] = envValue(value, kind); + applied.add(id); + } catch { + console.warn(`[config] ignoring invalid ${envName}=${JSON.stringify(value)}`); + } + } + + const allowedOrigins = Array.isArray(voice.allowed_origins) + ? voice.allowed_origins.filter((o) => typeof o === "string" && o).map((o) => o.replace(/\/+$/, "").toLowerCase()) + : []; + + return { + web: { + host: String(web.host ?? "0.0.0.0"), + port: toInt(web.port, 5000, 1, 65535), + dashboardPort: toInt(web.dashboard_port, 5001, 1, 65535), + }, + server: { + host: String(server.host ?? "").trim(), + voicePort: toInt(server.voice_port, 9987, 1, 65535), + // Secret: only ever read from the environment / .env. + password: env.TS_SERVER_PASSWORD ?? "", + }, + voice: { + enabled: voice.enabled !== false, + maxSessions: toInt(voice.max_sessions, 8, 1, 1000), + maxSessionsPerIp: toInt(voice.max_sessions_per_ip, 2, 1, 1000), + maxFailedLogins: toInt(voice.max_failed_logins, 5, 1, 1000), + lockoutMs: toInt(voice.lockout_minutes, 10, 1, 24 * 60) * 60_000, + nicknamePrefix: String(voice.nickname_prefix ?? ""), + defaultChannel: String(voice.default_channel ?? ""), + identitySecurityLevel: toInt(voice.identity_security_level, 8, 0, 40), + trustProxy: voice.trust_proxy === true, + allowedOrigins, + }, + }; +} |
