/** * Application configuration — loaded from .nogit/config.json. * * All network addresses, credentials, provider settings, device definitions, * and routing rules come from this single config file. No hardcoded values * in source. */ import fs from 'node:fs'; import path from 'node:path'; import type { IEndpoint } from './sip/index.ts'; // --------------------------------------------------------------------------- // Config interfaces // --------------------------------------------------------------------------- export interface IQuirks { earlyMediaSilence: boolean; silencePayloadType?: number; silenceMaxPackets?: number; } export interface IProviderConfig { id: string; displayName: string; domain: string; outboundProxy: IEndpoint; username: string; password: string; registerIntervalSec: number; codecs: number[]; quirks: IQuirks; } export interface IDeviceConfig { id: string; displayName: string; expectedAddress: string; extension: string; } export interface IRoutingConfig { outbound: { default: string }; inbound: Record; ringBrowsers?: Record; } export interface IProxyConfig { lanIp: string; lanPort: number; publicIpSeed: string | null; rtpPortRange: { min: number; max: number }; webUiPort: number; } export interface IContact { id: string; name: string; number: string; company?: string; notes?: string; starred?: boolean; } export interface IAppConfig { proxy: IProxyConfig; providers: IProviderConfig[]; devices: IDeviceConfig[]; routing: IRoutingConfig; contacts: IContact[]; } // --------------------------------------------------------------------------- // Loader // --------------------------------------------------------------------------- const CONFIG_PATH = path.join(process.cwd(), '.nogit', 'config.json'); export function loadConfig(): IAppConfig { let raw: string; try { raw = fs.readFileSync(CONFIG_PATH, 'utf8'); } catch { throw new Error(`config not found at ${CONFIG_PATH} — create .nogit/config.json`); } const cfg = JSON.parse(raw) as IAppConfig; // Basic validation. if (!cfg.proxy) throw new Error('config: missing "proxy" section'); if (!cfg.proxy.lanIp) throw new Error('config: missing proxy.lanIp'); if (!cfg.proxy.lanPort) throw new Error('config: missing proxy.lanPort'); if (!cfg.proxy.rtpPortRange?.min || !cfg.proxy.rtpPortRange?.max) { throw new Error('config: missing proxy.rtpPortRange.min/max'); } cfg.proxy.webUiPort ??= 3060; cfg.proxy.publicIpSeed ??= null; cfg.providers ??= []; for (const p of cfg.providers) { if (!p.id || !p.domain || !p.outboundProxy || !p.username || !p.password) { throw new Error(`config: provider "${p.id || '?'}" missing required fields`); } p.displayName ??= p.id; p.registerIntervalSec ??= 300; p.codecs ??= [9, 0, 8, 101]; p.quirks ??= { earlyMediaSilence: false }; } if (!Array.isArray(cfg.devices) || !cfg.devices.length) { throw new Error('config: need at least one device'); } for (const d of cfg.devices) { if (!d.id || !d.expectedAddress) { throw new Error(`config: device "${d.id || '?'}" missing required fields`); } d.displayName ??= d.id; d.extension ??= '100'; } cfg.routing ??= { outbound: { default: cfg.providers[0].id }, inbound: {} }; cfg.routing.outbound ??= { default: cfg.providers[0].id }; cfg.contacts ??= []; for (const c of cfg.contacts) { c.starred ??= false; } return cfg; } // --------------------------------------------------------------------------- // Lookup helpers // --------------------------------------------------------------------------- export function getProvider(cfg: IAppConfig, id: string): IProviderConfig | null { return cfg.providers.find((p) => p.id === id) ?? null; } export function getDevice(cfg: IAppConfig, id: string): IDeviceConfig | null { return cfg.devices.find((d) => d.id === id) ?? null; } export function getProviderForOutbound(cfg: IAppConfig): IProviderConfig | null { const id = cfg.routing?.outbound?.default; if (!id) return cfg.providers[0] || null; return getProvider(cfg, id) || cfg.providers[0] || null; } export function getDevicesForInbound(cfg: IAppConfig, providerId: string): IDeviceConfig[] { const ids = cfg.routing.inbound[providerId]; if (!ids?.length) return cfg.devices; // fallback: ring all devices return ids.map((id) => getDevice(cfg, id)).filter(Boolean) as IDeviceConfig[]; }