/** * Proxy engine bridge — manages the Rust proxy-engine subprocess. * * The proxy-engine handles ALL SIP protocol mechanics. TypeScript only: * - Sends configuration * - Receives high-level events (incoming_call, call_ended, etc.) * - Sends high-level commands (hangup, make_call, play_audio) * * No raw SIP ever touches TypeScript. */ import path from 'node:path'; import { RustBridge } from '@push.rocks/smartrust'; // --------------------------------------------------------------------------- // Command type map for smartrust // --------------------------------------------------------------------------- type TProxyCommands = { configure: { params: Record; result: { bound: string }; }; hangup: { params: { call_id: string }; result: Record; }; make_call: { params: { number: string; device_id?: string; provider_id?: string }; result: { call_id: string }; }; play_audio: { params: { call_id: string; leg_id?: string; file_path: string; codec?: number }; result: Record; }; start_recording: { params: { call_id: string; file_path: string; max_duration_ms?: number }; result: Record; }; stop_recording: { params: { call_id: string }; result: { file_path: string; duration_ms: number }; }; }; // --------------------------------------------------------------------------- // Event types from Rust // --------------------------------------------------------------------------- export interface IIncomingCallEvent { call_id: string; from_uri: string; to_number: string; provider_id: string; } export interface IOutboundCallEvent { call_id: string; from_device: string | null; to_number: string; } export interface ICallEndedEvent { call_id: string; reason: string; duration: number; from_side?: string; } export interface IProviderRegisteredEvent { provider_id: string; registered: boolean; public_ip: string | null; } export interface IDeviceRegisteredEvent { device_id: string; display_name: string; address: string; port: number; aor: string; expires: number; } // --------------------------------------------------------------------------- // Bridge singleton // --------------------------------------------------------------------------- let bridge: RustBridge | null = null; let initialized = false; let logFn: ((msg: string) => void) | undefined; function buildLocalPaths(): string[] { const root = process.cwd(); return [ path.join(root, 'dist_rust', 'proxy-engine'), path.join(root, 'rust', 'target', 'release', 'proxy-engine'), path.join(root, 'rust', 'target', 'debug', 'proxy-engine'), ]; } /** * Initialize the proxy engine — spawn the Rust binary. * Call configure() separately to push config and start SIP. */ export async function initProxyEngine(log?: (msg: string) => void): Promise { if (initialized && bridge) return true; logFn = log; try { bridge = new RustBridge({ binaryName: 'proxy-engine', localPaths: buildLocalPaths(), }); const spawned = await bridge.spawn(); if (!spawned) { log?.('[proxy-engine] failed to spawn binary'); bridge = null; return false; } bridge.on('exit', () => { logFn?.('[proxy-engine] process exited — will need re-init'); bridge = null; initialized = false; }); // Forward stderr for debugging. bridge.on('stderr', (line: string) => { logFn?.(`[proxy-engine:stderr] ${line}`); }); initialized = true; log?.('[proxy-engine] spawned and ready'); return true; } catch (e: any) { log?.(`[proxy-engine] init error: ${e.message}`); bridge = null; return false; } } /** * Send the full app config to the proxy engine. * This binds the SIP socket, starts provider registrations, etc. */ export async function configureProxyEngine(config: Record): Promise { if (!bridge || !initialized) return false; try { const result = await bridge.sendCommand('configure', config as any); logFn?.(`[proxy-engine] configured, SIP bound on ${(result as any)?.bound || '?'}`); return true; } catch (e: any) { logFn?.(`[proxy-engine] configure error: ${e.message}`); return false; } } /** * Initiate an outbound call via Rust. Returns the call ID or null on failure. */ export async function makeCall(number: string, deviceId?: string, providerId?: string): Promise { if (!bridge || !initialized) return null; try { const result = await bridge.sendCommand('make_call', { number, device_id: deviceId, provider_id: providerId, } as any); return (result as any)?.call_id || null; } catch (e: any) { logFn?.(`[proxy-engine] make_call error: ${e?.message || e}`); return null; } } /** * Send a hangup command. */ export async function hangupCall(callId: string): Promise { if (!bridge || !initialized) return false; try { await bridge.sendCommand('hangup', { call_id: callId } as any); return true; } catch { return false; } } /** * Send a WebRTC offer to the proxy engine. Returns the SDP answer. */ export async function webrtcOffer(sessionId: string, sdp: string): Promise<{ sdp: string } | null> { if (!bridge || !initialized) return null; try { const result = await bridge.sendCommand('webrtc_offer', { session_id: sessionId, sdp } as any); return result as any; } catch (e: any) { logFn?.(`[proxy-engine] webrtc_offer error: ${e?.message || e}`); return null; } } /** * Forward an ICE candidate to the proxy engine. */ export async function webrtcIce(sessionId: string, candidate: any): Promise { if (!bridge || !initialized) return; try { await bridge.sendCommand('webrtc_ice', { session_id: sessionId, candidate: candidate?.candidate || candidate, sdp_mid: candidate?.sdpMid, sdp_mline_index: candidate?.sdpMLineIndex, } as any); } catch { /* ignore */ } } /** * Link a WebRTC session to a SIP call — enables audio bridging. * The browser's Opus audio will be transcoded and sent to the provider. */ export async function webrtcLink(sessionId: string, callId: string, providerMediaAddr: string, providerMediaPort: number, sipPt: number = 9): Promise { if (!bridge || !initialized) return false; try { await bridge.sendCommand('webrtc_link', { session_id: sessionId, call_id: callId, provider_media_addr: providerMediaAddr, provider_media_port: providerMediaPort, sip_pt: sipPt, } as any); return true; } catch (e: any) { logFn?.(`[proxy-engine] webrtc_link error: ${e?.message || e}`); return false; } } /** * Close a WebRTC session. */ export async function webrtcClose(sessionId: string): Promise { if (!bridge || !initialized) return; try { await bridge.sendCommand('webrtc_close', { session_id: sessionId } as any); } catch { /* ignore */ } } /** * Subscribe to an event from the proxy engine. * Event names: incoming_call, outbound_device_call, call_ringing, * call_answered, call_ended, provider_registered, device_registered, * dtmf_digit, recording_done, sip_unhandled */ export function onProxyEvent(event: string, handler: (data: any) => void): void { if (!bridge) throw new Error('proxy engine not initialized'); bridge.on(`management:${event}`, handler); } /** Check if the proxy engine is ready. */ export function isProxyReady(): boolean { return initialized && bridge !== null; } /** Shut down the proxy engine. */ export function shutdownProxyEngine(): void { if (bridge) { try { bridge.kill(); } catch { /* ignore */ } bridge = null; initialized = false; } }