import * as plugins from './plugins.js'; import { EventEmitter } from 'events'; import { decodeConnectionToken } from './classes.token.js'; // Command map for the edge side of remoteingress-bin type TEdgeCommands = { ping: { params: Record; result: { pong: boolean }; }; startEdge: { params: { hubHost: string; hubPort: number; edgeId: string; secret: string; bindAddress?: string; transportMode?: 'tcpTls' | 'quic' | 'quicWithFallback'; }; result: { started: boolean }; }; stopEdge: { params: Record; result: { stopped: boolean; wasRunning?: boolean }; }; getEdgeStatus: { params: Record; result: { running: boolean; connected: boolean; publicIp: string | null; activeStreams: number; listenPorts: number[]; }; }; }; export interface IEdgeConfig { hubHost: string; hubPort?: number; edgeId: string; secret: string; bindAddress?: string; transportMode?: 'tcpTls' | 'quic' | 'quicWithFallback'; } const MAX_RESTART_ATTEMPTS = 10; const MAX_RESTART_BACKOFF_MS = 30_000; export class RemoteIngressEdge extends EventEmitter { private bridge: InstanceType>; private started = false; private stopping = false; private savedConfig: IEdgeConfig | null = null; private restartBackoffMs = 1000; private restartAttempts = 0; private statusInterval: ReturnType | undefined; constructor() { super(); const packageDir = plugins.path.resolve( plugins.path.dirname(new URL(import.meta.url).pathname), '..', ); this.bridge = new plugins.smartrust.RustBridge({ binaryName: 'remoteingress-bin', cliArgs: ['--management'], requestTimeoutMs: 30_000, readyTimeoutMs: 10_000, localPaths: [ // Platform-suffixed binary in dist_rust (production) plugins.path.join(packageDir, 'dist_rust', `remoteingress-bin_${process.platform === 'win32' ? 'windows' : 'linux'}_${process.arch === 'x64' ? 'amd64' : process.arch}`), // Exact binaryName fallback in dist_rust plugins.path.join(packageDir, 'dist_rust', 'remoteingress-bin'), // Development build paths (cargo output uses exact name) plugins.path.join(packageDir, 'rust', 'target', 'release', 'remoteingress-bin'), plugins.path.join(packageDir, 'rust', 'target', 'debug', 'remoteingress-bin'), ], searchSystemPath: false, }); // Forward events from Rust binary this.bridge.on('management:tunnelConnected', () => { this.emit('tunnelConnected'); }); this.bridge.on('management:tunnelDisconnected', (data: { reason?: string }) => { const reason = data?.reason ?? 'unknown'; console.log(`[RemoteIngressEdge] Tunnel disconnected: ${reason}`); this.emit('tunnelDisconnected', data); }); this.bridge.on('management:publicIpDiscovered', (data: { ip: string }) => { this.emit('publicIpDiscovered', data); }); this.bridge.on('management:portsAssigned', (data: { listenPorts: number[] }) => { console.log(`[RemoteIngressEdge] Ports assigned by hub: ${data.listenPorts.join(', ')}`); this.emit('portsAssigned', data); }); this.bridge.on('management:portsUpdated', (data: { listenPorts: number[] }) => { console.log(`[RemoteIngressEdge] Ports updated by hub: ${data.listenPorts.join(', ')}`); this.emit('portsUpdated', data); }); } /** * Start the edge — spawns the Rust binary and connects to the hub. * Accepts either a connection token or an explicit IEdgeConfig. */ public async start(config: { token: string } | IEdgeConfig): Promise { let edgeConfig: IEdgeConfig; if ('token' in config) { const decoded = decodeConnectionToken(config.token); edgeConfig = { hubHost: decoded.hubHost, hubPort: decoded.hubPort, edgeId: decoded.edgeId, secret: decoded.secret, }; } else { edgeConfig = config; } this.savedConfig = edgeConfig; this.stopping = false; const spawned = await this.bridge.spawn(); if (!spawned) { throw new Error('Failed to spawn remoteingress-bin'); } // Register crash recovery handler this.bridge.on('exit', this.handleCrashRecovery); await this.bridge.sendCommand('startEdge', { hubHost: edgeConfig.hubHost, hubPort: edgeConfig.hubPort ?? 8443, edgeId: edgeConfig.edgeId, secret: edgeConfig.secret, ...(edgeConfig.bindAddress ? { bindAddress: edgeConfig.bindAddress } : {}), ...(edgeConfig.transportMode ? { transportMode: edgeConfig.transportMode } : {}), }); this.started = true; this.restartAttempts = 0; this.restartBackoffMs = 1000; // Start periodic status logging this.statusInterval = setInterval(async () => { try { const status = await this.getStatus(); console.log( `[RemoteIngressEdge] Status: connected=${status.connected}, ` + `streams=${status.activeStreams}, ports=[${status.listenPorts.join(',')}], ` + `publicIp=${status.publicIp ?? 'unknown'}` ); } catch { // Bridge may be shutting down } }, 60_000); } /** * Stop the edge and kill the Rust process. */ public async stop(): Promise { this.stopping = true; if (this.statusInterval) { clearInterval(this.statusInterval); this.statusInterval = undefined; } if (this.started) { try { await this.bridge.sendCommand('stopEdge', {} as Record); } catch { // Process may already be dead } this.bridge.removeListener('exit', this.handleCrashRecovery); this.bridge.kill(); this.started = false; } } /** * Get the current edge status. */ public async getStatus() { return this.bridge.sendCommand('getEdgeStatus', {} as Record); } /** * Check if the bridge is running. */ public get running(): boolean { return this.bridge.running; } /** * Handle unexpected Rust binary crash — auto-restart with backoff. */ private handleCrashRecovery = async (code: number | null, signal: string | null) => { if (this.stopping || !this.started || !this.savedConfig) { return; } console.error( `[RemoteIngressEdge] Rust binary crashed (code=${code}, signal=${signal}), ` + `attempt ${this.restartAttempts + 1}/${MAX_RESTART_ATTEMPTS}` ); this.started = false; if (this.restartAttempts >= MAX_RESTART_ATTEMPTS) { console.error('[RemoteIngressEdge] Max restart attempts reached, giving up'); this.emit('crashRecoveryFailed'); return; } await new Promise(resolve => setTimeout(resolve, this.restartBackoffMs)); this.restartBackoffMs = Math.min(this.restartBackoffMs * 2, MAX_RESTART_BACKOFF_MS); this.restartAttempts++; try { const spawned = await this.bridge.spawn(); if (!spawned) { console.error('[RemoteIngressEdge] Failed to respawn binary'); return; } this.bridge.on('exit', this.handleCrashRecovery); await this.bridge.sendCommand('startEdge', { hubHost: this.savedConfig.hubHost, hubPort: this.savedConfig.hubPort ?? 8443, edgeId: this.savedConfig.edgeId, secret: this.savedConfig.secret, ...(this.savedConfig.bindAddress ? { bindAddress: this.savedConfig.bindAddress } : {}), ...(this.savedConfig.transportMode ? { transportMode: this.savedConfig.transportMode } : {}), }); this.started = true; this.restartAttempts = 0; this.restartBackoffMs = 1000; console.log('[RemoteIngressEdge] Successfully recovered from crash'); this.emit('crashRecovered'); } catch (err) { console.error(`[RemoteIngressEdge] Crash recovery failed: ${err}`); } }; }