Files
smartvpn/ts/smartvpn.classes.vpnbridge.ts

153 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-02-27 10:18:23 +00:00
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<string, string> = { x64: 'amd64', arm64: 'arm64' };
const osMap: Record<string, string> = { 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<TCommands extends TCommandMap> extends plugins.events.EventEmitter {
private bridge: plugins.smartrust.RustBridge<TCommands>;
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<TCommands>({
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:<eventName>' 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<boolean> {
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<K extends string & keyof TCommands>(
method: K,
params: TCommands[K]['params'],
): Promise<TCommands[K]['result']> {
return this.bridge.sendCommand(method, params);
}
/**
* Whether the bridge is currently running/connected.
*/
public get running(): boolean {
return this.bridge.running;
}
}