import * as plugins from './plugins.js'; import { EventEmitter } from 'events'; // Command map for the hub side of remoteingress-bin type THubCommands = { ping: { params: Record; result: { pong: boolean }; }; startHub: { params: { tunnelPort: number; targetHost?: string; }; result: { started: boolean }; }; stopHub: { params: Record; result: { stopped: boolean; wasRunning?: boolean }; }; updateAllowedEdges: { params: { edges: Array<{ id: string; secret: string; listenPorts?: number[]; stunIntervalSecs?: number }>; }; result: { updated: boolean }; }; getHubStatus: { params: Record; result: { running: boolean; tunnelPort: number; connectedEdges: Array<{ edgeId: string; connectedAt: number; activeStreams: number; }>; }; }; }; export interface IHubConfig { tunnelPort?: number; targetHost?: string; } export class RemoteIngressHub extends EventEmitter { private bridge: InstanceType>; private started = false; 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:edgeConnected', (data: { edgeId: string }) => { this.emit('edgeConnected', data); }); this.bridge.on('management:edgeDisconnected', (data: { edgeId: string }) => { this.emit('edgeDisconnected', data); }); this.bridge.on('management:streamOpened', (data: { edgeId: string; streamId: number }) => { this.emit('streamOpened', data); }); this.bridge.on('management:streamClosed', (data: { edgeId: string; streamId: number }) => { this.emit('streamClosed', data); }); } /** * Start the hub — spawns the Rust binary and starts the tunnel server. */ public async start(config: IHubConfig = {}): Promise { const spawned = await this.bridge.spawn(); if (!spawned) { throw new Error('Failed to spawn remoteingress-bin'); } await this.bridge.sendCommand('startHub', { tunnelPort: config.tunnelPort ?? 8443, targetHost: config.targetHost ?? '127.0.0.1', }); this.started = true; } /** * Stop the hub and kill the Rust process. */ public async stop(): Promise { if (this.started) { try { await this.bridge.sendCommand('stopHub', {} as Record); } catch { // Process may already be dead } this.bridge.kill(); this.started = false; } } /** * Update the list of allowed edges that can connect to this hub. */ public async updateAllowedEdges(edges: Array<{ id: string; secret: string; listenPorts?: number[]; stunIntervalSecs?: number }>): Promise { await this.bridge.sendCommand('updateAllowedEdges', { edges }); } /** * Get the current hub status. */ public async getStatus() { return this.bridge.sendCommand('getHubStatus', {} as Record); } /** * Check if the bridge is running. */ public get running(): boolean { return this.bridge.running; } }