import * as plugins from './smartvpn.plugins.js'; import * as paths from './smartvpn.paths.js'; import type { TVpnTransportOptions, IVpnTransportSocket, } from './smartvpn.interfaces.js'; import type { TCommandMap } from '@push.rocks/smartrust'; /** * Get the package root directory. */ function getPackageRoot(): string { const thisDir = plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url)); return plugins.path.resolve(thisDir, '..'); } /** * Map Node.js platform/arch to tsrust's platform suffix. */ function getTsrustPlatformSuffix(): string | null { const archMap: Record = { x64: 'amd64', arm64: 'arm64' }; const osMap: Record = { linux: 'linux', darwin: 'macos' }; const os = osMap[process.platform]; const arch = archMap[process.arch]; if (os && arch) { return `${os}_${arch}`; } return null; } /** * Build local search paths for the smartvpn daemon binary. */ function buildLocalPaths(binaryName: string): string[] { const packageRoot = getPackageRoot(); const suffix = getTsrustPlatformSuffix(); const paths: string[] = []; // dist_rust/ (tsrust cross-compiled output) if (suffix) { paths.push(plugins.path.join(packageRoot, 'dist_rust', `${binaryName}_${suffix}`)); } paths.push(plugins.path.join(packageRoot, 'dist_rust', binaryName)); // Local dev build paths paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'release', binaryName)); paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'debug', binaryName)); return paths; } /** * Shared bridge wrapper around smartrust RustBridge. * Supports stdio mode (dev: spawn child process) and socket mode (production: connect to running daemon). */ export class VpnBridge extends plugins.events.EventEmitter { private bridge: plugins.smartrust.RustBridge; private transportOptions: TVpnTransportOptions; private mode: 'client' | 'server'; constructor(options: { transport: TVpnTransportOptions; mode: 'client' | 'server'; binaryName?: string; }) { super(); const binaryName = options.binaryName || 'smartvpn_daemon'; this.transportOptions = options.transport; this.mode = options.mode; this.bridge = new plugins.smartrust.RustBridge({ binaryName, envVarName: 'SMARTVPN_RUST_BINARY', platformPackagePrefix: '@push.rocks/smartvpn', localPaths: buildLocalPaths(binaryName), cliArgs: ['--management', '--mode', this.mode], maxPayloadSize: 10 * 1024 * 1024, // 10 MB }); // Forward events from inner bridge this.bridge.on('exit', (code: number | null, signal: string | null) => { this.emit('exit', code, signal); }); this.bridge.on('reconnected', () => { this.emit('reconnected'); }); // Forward management events from the daemon // smartrust emits 'management:' for unsolicited events this.bridge.on('management:status', (data: any) => { this.emit('status', data); }); this.bridge.on('management:error', (data: any) => { this.emit('error', data); }); this.bridge.on('management:client-connected', (data: any) => { this.emit('client-connected', data); }); this.bridge.on('management:client-disconnected', (data: any) => { this.emit('client-disconnected', data); }); this.bridge.on('management:started', (data: any) => { this.emit('started', data); }); this.bridge.on('management:stopped', (data: any) => { this.emit('stopped', data); }); } /** * Start the bridge: spawn in stdio mode or connect in socket mode. */ public async start(): Promise { if (this.transportOptions.transport === 'socket') { const socketOpts = this.transportOptions as IVpnTransportSocket; return this.bridge.connect(socketOpts.socketPath, { autoReconnect: socketOpts.autoReconnect, reconnectBaseDelayMs: socketOpts.reconnectBaseDelayMs, reconnectMaxDelayMs: socketOpts.reconnectMaxDelayMs, maxReconnectAttempts: socketOpts.maxReconnectAttempts, }); } else { return this.bridge.spawn(); } } /** * Stop the bridge. In socket mode, closes the socket (daemon stays alive). * In stdio mode, kills the child process. */ public stop(): void { this.bridge.kill(); } /** * Send a typed command to the daemon. */ public async sendCommand( method: K, params: TCommands[K]['params'], ): Promise { return this.bridge.sendCommand(method, params); } /** * Whether the bridge is currently running/connected. */ public get running(): boolean { return this.bridge.running; } }