2026-04-09 23:03:55 +00:00
|
|
|
/**
|
2026-04-14 10:45:59 +00:00
|
|
|
* SIP proxy bootstrap.
|
2026-04-09 23:03:55 +00:00
|
|
|
*
|
2026-04-14 10:45:59 +00:00
|
|
|
* Spawns the Rust proxy-engine, wires runtime state/event handling,
|
|
|
|
|
* and starts the web dashboard plus browser signaling layer.
|
2026-04-09 23:03:55 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import fs from 'node:fs';
|
|
|
|
|
import path from 'node:path';
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
import { loadConfig, type IAppConfig } from './config.ts';
|
2026-04-09 23:03:55 +00:00
|
|
|
import { broadcastWs, initWebUi } from './frontend.ts';
|
2026-04-14 10:45:59 +00:00
|
|
|
import { initWebRtcSignaling, getAllBrowserDeviceIds, sendToBrowserDevice } from './webrtcbridge.ts';
|
2026-04-10 08:54:46 +00:00
|
|
|
import { VoiceboxManager } from './voicebox.ts';
|
2026-04-10 09:57:27 +00:00
|
|
|
import {
|
|
|
|
|
initProxyEngine,
|
|
|
|
|
configureProxyEngine,
|
|
|
|
|
hangupCall,
|
2026-04-10 11:36:18 +00:00
|
|
|
makeCall,
|
2026-04-10 09:57:27 +00:00
|
|
|
shutdownProxyEngine,
|
2026-04-10 11:36:18 +00:00
|
|
|
webrtcOffer,
|
|
|
|
|
webrtcIce,
|
2026-04-10 12:19:20 +00:00
|
|
|
webrtcLink,
|
2026-04-10 11:36:18 +00:00
|
|
|
webrtcClose,
|
2026-04-10 09:57:27 +00:00
|
|
|
} from './proxybridge.ts';
|
2026-04-14 10:45:59 +00:00
|
|
|
import { registerProxyEventHandlers } from './runtime/proxy-events.ts';
|
|
|
|
|
import { StatusStore } from './runtime/status-store.ts';
|
|
|
|
|
import { WebRtcLinkManager, type IProviderMediaInfo } from './runtime/webrtc-linking.ts';
|
2026-04-09 23:03:55 +00:00
|
|
|
|
|
|
|
|
let appConfig: IAppConfig = loadConfig();
|
|
|
|
|
|
|
|
|
|
const LOG_PATH = path.join(process.cwd(), 'sip_trace.log');
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
const instanceId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
const statusStore = new StatusStore(appConfig);
|
|
|
|
|
const webRtcLinks = new WebRtcLinkManager();
|
|
|
|
|
const voiceboxManager = new VoiceboxManager(log);
|
|
|
|
|
|
|
|
|
|
voiceboxManager.init(appConfig.voiceboxes ?? []);
|
|
|
|
|
initWebRtcSignaling({ log });
|
|
|
|
|
|
2026-04-09 23:03:55 +00:00
|
|
|
function now(): string {
|
|
|
|
|
return new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
function log(message: string): void {
|
|
|
|
|
const line = `${now()} ${message}\n`;
|
2026-04-09 23:03:55 +00:00
|
|
|
fs.appendFileSync(LOG_PATH, line);
|
|
|
|
|
process.stdout.write(line);
|
2026-04-14 10:45:59 +00:00
|
|
|
broadcastWs('log', { message });
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
function errorMessage(error: unknown): string {
|
|
|
|
|
return error instanceof Error ? error.message : String(error);
|
2026-04-10 14:54:21 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
function buildProxyConfig(config: IAppConfig): Record<string, unknown> {
|
|
|
|
|
return {
|
|
|
|
|
proxy: config.proxy,
|
|
|
|
|
providers: config.providers,
|
|
|
|
|
devices: config.devices,
|
|
|
|
|
routing: config.routing,
|
|
|
|
|
voiceboxes: config.voiceboxes ?? [],
|
|
|
|
|
ivr: config.ivr,
|
|
|
|
|
};
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
function getStatus() {
|
|
|
|
|
return statusStore.buildStatusSnapshot(
|
|
|
|
|
instanceId,
|
|
|
|
|
startTime,
|
|
|
|
|
getAllBrowserDeviceIds(),
|
|
|
|
|
voiceboxManager.getAllUnheardCounts(),
|
|
|
|
|
);
|
2026-04-10 09:57:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
function requestWebRtcLink(callId: string, sessionId: string, media: IProviderMediaInfo): void {
|
|
|
|
|
log(`[webrtc] linking session=${sessionId.slice(0, 8)} to call=${callId} media=${media.addr}:${media.port} pt=${media.sipPt}`);
|
|
|
|
|
void webrtcLink(sessionId, callId, media.addr, media.port, media.sipPt).then((ok) => {
|
|
|
|
|
log(`[webrtc] link result: ${ok}`);
|
2026-04-10 09:57:27 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
async function configureRuntime(config: IAppConfig): Promise<boolean> {
|
|
|
|
|
return configureProxyEngine(buildProxyConfig(config));
|
2026-04-09 23:03:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
async function reloadConfig(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const previousConfig = appConfig;
|
|
|
|
|
const nextConfig = loadConfig();
|
2026-04-10 08:54:46 +00:00
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
appConfig = nextConfig;
|
|
|
|
|
statusStore.updateConfig(nextConfig);
|
|
|
|
|
voiceboxManager.init(nextConfig.voiceboxes ?? []);
|
2026-04-09 23:03:55 +00:00
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
if (nextConfig.proxy.lanPort !== previousConfig.proxy.lanPort) {
|
|
|
|
|
log('[config] proxy.lanPort changed; restart required for SIP socket rebinding');
|
|
|
|
|
}
|
|
|
|
|
if (nextConfig.proxy.webUiPort !== previousConfig.proxy.webUiPort) {
|
|
|
|
|
log('[config] proxy.webUiPort changed; restart required for web UI rebinding');
|
|
|
|
|
}
|
2026-04-09 23:03:55 +00:00
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
const configured = await configureRuntime(nextConfig);
|
|
|
|
|
if (configured) {
|
|
|
|
|
log('[config] reloaded - proxy engine reconfigured');
|
|
|
|
|
} else {
|
|
|
|
|
log('[config] reload failed - proxy engine rejected config');
|
|
|
|
|
}
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
log(`[config] reload failed: ${errorMessage(error)}`);
|
2026-04-10 11:36:18 +00:00
|
|
|
}
|
2026-04-09 23:03:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 09:57:27 +00:00
|
|
|
async function startProxyEngine(): Promise<void> {
|
2026-04-14 10:45:59 +00:00
|
|
|
const started = await initProxyEngine(log);
|
|
|
|
|
if (!started) {
|
2026-04-10 09:57:27 +00:00
|
|
|
log('[FATAL] failed to start proxy engine');
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
2026-04-09 23:03:55 +00:00
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
registerProxyEventHandlers({
|
|
|
|
|
log,
|
|
|
|
|
statusStore,
|
|
|
|
|
voiceboxManager,
|
|
|
|
|
webRtcLinks,
|
|
|
|
|
getBrowserDeviceIds: getAllBrowserDeviceIds,
|
|
|
|
|
sendToBrowserDevice,
|
|
|
|
|
broadcast: broadcastWs,
|
|
|
|
|
onLinkWebRtcSession: requestWebRtcLink,
|
|
|
|
|
onCloseWebRtcSession: (sessionId) => {
|
|
|
|
|
void webrtcClose(sessionId);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const configured = await configureRuntime(appConfig);
|
2026-04-10 09:57:27 +00:00
|
|
|
if (!configured) {
|
|
|
|
|
log('[FATAL] failed to configure proxy engine');
|
|
|
|
|
process.exit(1);
|
2026-04-09 23:03:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
const providerList = appConfig.providers.map((provider) => provider.displayName).join(', ');
|
|
|
|
|
const deviceList = appConfig.devices.map((device) => device.displayName).join(', ');
|
2026-04-10 09:57:27 +00:00
|
|
|
log(`proxy engine started | LAN ${appConfig.proxy.lanIp}:${appConfig.proxy.lanPort} | providers: ${providerList} | devices: ${deviceList}`);
|
|
|
|
|
}
|
2026-04-09 23:03:55 +00:00
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
initWebUi({
|
|
|
|
|
port: appConfig.proxy.webUiPort,
|
2026-04-09 23:03:55 +00:00
|
|
|
getStatus,
|
|
|
|
|
log,
|
2026-04-14 10:45:59 +00:00
|
|
|
onStartCall: (number, deviceId, providerId) => {
|
2026-04-10 11:36:18 +00:00
|
|
|
log(`[dashboard] start call: ${number} device=${deviceId || 'any'} provider=${providerId || 'auto'}`);
|
2026-04-14 10:45:59 +00:00
|
|
|
void makeCall(number, deviceId, providerId).then((callId) => {
|
2026-04-10 11:36:18 +00:00
|
|
|
if (callId) {
|
|
|
|
|
log(`[dashboard] call started: ${callId}`);
|
2026-04-14 10:45:59 +00:00
|
|
|
statusStore.noteDashboardCallStarted(callId, number, providerId);
|
2026-04-10 11:36:18 +00:00
|
|
|
} else {
|
|
|
|
|
log(`[dashboard] call failed for ${number}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-14 10:45:59 +00:00
|
|
|
|
2026-04-10 11:36:18 +00:00
|
|
|
return { id: `pending-${Date.now()}` };
|
2026-04-10 09:57:27 +00:00
|
|
|
},
|
2026-04-14 10:45:59 +00:00
|
|
|
onHangupCall: (callId) => {
|
|
|
|
|
void hangupCall(callId);
|
2026-04-10 09:57:27 +00:00
|
|
|
return true;
|
2026-04-09 23:03:55 +00:00
|
|
|
},
|
2026-04-14 10:45:59 +00:00
|
|
|
onConfigSaved: reloadConfig,
|
|
|
|
|
voiceboxManager,
|
|
|
|
|
onWebRtcOffer: async (sessionId, sdp, ws) => {
|
2026-04-10 12:19:20 +00:00
|
|
|
log(`[webrtc] offer from browser session=${sessionId.slice(0, 8)} sdp_type=${typeof sdp} sdp_len=${sdp?.length || 0}`);
|
|
|
|
|
if (!sdp || typeof sdp !== 'string' || sdp.length < 10) {
|
|
|
|
|
log(`[webrtc] WARNING: invalid SDP (type=${typeof sdp}), skipping offer`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-14 10:45:59 +00:00
|
|
|
|
2026-04-10 12:19:20 +00:00
|
|
|
log(`[webrtc] sending offer to Rust (${sdp.length}b)...`);
|
2026-04-10 11:36:18 +00:00
|
|
|
const result = await webrtcOffer(sessionId, sdp);
|
2026-04-10 12:19:20 +00:00
|
|
|
log(`[webrtc] Rust result: ${JSON.stringify(result)?.slice(0, 200)}`);
|
2026-04-10 11:36:18 +00:00
|
|
|
if (result?.sdp) {
|
|
|
|
|
ws.send(JSON.stringify({ type: 'webrtc-answer', sessionId, sdp: result.sdp }));
|
|
|
|
|
log(`[webrtc] answer sent to browser session=${sessionId.slice(0, 8)}`);
|
2026-04-14 10:45:59 +00:00
|
|
|
return;
|
2026-04-10 11:36:18 +00:00
|
|
|
}
|
2026-04-14 10:45:59 +00:00
|
|
|
|
|
|
|
|
log('[webrtc] ERROR: no answer SDP from Rust');
|
2026-04-10 11:36:18 +00:00
|
|
|
},
|
2026-04-14 10:45:59 +00:00
|
|
|
onWebRtcIce: async (sessionId, candidate) => {
|
2026-04-10 11:36:18 +00:00
|
|
|
await webrtcIce(sessionId, candidate);
|
|
|
|
|
},
|
2026-04-14 10:45:59 +00:00
|
|
|
onWebRtcClose: async (sessionId) => {
|
|
|
|
|
webRtcLinks.removeSession(sessionId);
|
2026-04-10 11:36:18 +00:00
|
|
|
await webrtcClose(sessionId);
|
|
|
|
|
},
|
2026-04-14 10:45:59 +00:00
|
|
|
onWebRtcAccept: (callId, sessionId) => {
|
2026-04-10 12:19:20 +00:00
|
|
|
log(`[webrtc] accept: callId=${callId} sessionId=${sessionId.slice(0, 8)}`);
|
|
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
const pendingMedia = webRtcLinks.acceptCall(callId, sessionId);
|
|
|
|
|
if (pendingMedia) {
|
|
|
|
|
requestWebRtcLink(callId, sessionId, pendingMedia);
|
|
|
|
|
return;
|
2026-04-10 12:19:20 +00:00
|
|
|
}
|
2026-04-14 10:45:59 +00:00
|
|
|
|
|
|
|
|
log(`[webrtc] session ${sessionId.slice(0, 8)} accepted, waiting for call_answered media info`);
|
2026-04-10 12:19:20 +00:00
|
|
|
},
|
2026-04-14 10:45:59 +00:00
|
|
|
});
|
2026-04-09 23:03:55 +00:00
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
void startProxyEngine();
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
process.on('SIGINT', () => {
|
|
|
|
|
log('SIGINT, exiting');
|
|
|
|
|
shutdownProxyEngine();
|
|
|
|
|
process.exit(0);
|
|
|
|
|
});
|
2026-04-10 09:57:27 +00:00
|
|
|
|
2026-04-14 10:45:59 +00:00
|
|
|
process.on('SIGTERM', () => {
|
|
|
|
|
log('SIGTERM, exiting');
|
|
|
|
|
shutdownProxyEngine();
|
|
|
|
|
process.exit(0);
|
|
|
|
|
});
|