Full-featured SIP router with multi-provider trunking, browser softphone via WebRTC, real-time Opus/G.722/PCM transcoding in Rust, RNNoise ML noise suppression, Kokoro neural TTS announcements, and a Lit-based web dashboard with live call monitoring and REST API.
154 lines
4.4 KiB
TypeScript
154 lines
4.4 KiB
TypeScript
/**
|
|
* 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<string, string[]>;
|
|
ringBrowsers?: Record<string, boolean>;
|
|
}
|
|
|
|
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[];
|
|
}
|