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; }; 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; } export class RemoteIngressEdge 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:tunnelConnected', () => { this.emit('tunnelConnected'); }); this.bridge.on('management:tunnelDisconnected', () => { this.emit('tunnelDisconnected'); }); this.bridge.on('management:publicIpDiscovered', (data: { ip: string }) => { this.emit('publicIpDiscovered', 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; } const spawned = await this.bridge.spawn(); if (!spawned) { throw new Error('Failed to spawn remoteingress-bin'); } await this.bridge.sendCommand('startEdge', { hubHost: edgeConfig.hubHost, hubPort: edgeConfig.hubPort ?? 8443, edgeId: edgeConfig.edgeId, secret: edgeConfig.secret, }); this.started = true; } /** * Stop the edge and kill the Rust process. */ public async stop(): Promise { if (this.started) { try { await this.bridge.sendCommand('stopEdge', {} as Record); } catch { // Process may already be dead } 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; } }