/** * Holesail manager — assembles all sub-modules and exposes the same API * as the original monolithic holesail-manager.js. */ const stateModule = require('./state.js'); const settingsModule = require('./settings.js'); const connectionsModule = require('./connections.js'); const serversModule = require('./servers.js'); const vhostsModule = require('./virtual-hosts.js'); const svcModule = require('./service-tunnels.js'); // ── Event emitter ───────────────────────────────────────────────────────────── let eventEmit = null; /** * Register the callback used to push tunnel lifecycle events to the extension. * Must be called before any tunnels are started so events are not lost. * @param {Function} emit - Called as `emit(eventName, payloadObject)`. */ function setEventEmitter(emit) { eventEmit = emit; } function emit(event, payload) { if (eventEmit) eventEmit(event, payload); } // ── Shared saveState snapshot ───────────────────────────────────────────────── // Collects current state from all sub-modules and persists it. let onStateSaved = null; let stateSaveSuppressed = false; /** * When true, saveState() is a no-op. Used during sync apply so only the final * state is written once by the sync-manager. * @param {boolean} v */ function setStateSaveSuppressed(v) { stateSaveSuppressed = !!v; } /** * Register a callback invoked after each saveState() with the snapshot object. * Used by sync-manager to push state to autopass. * @param {Function} fn - (snapshot: object) => void */ function setOnStateSaved(fn) { onStateSaved = typeof fn === 'function' ? fn : null; } function saveState() { if (stateSaveSuppressed) return; const serversList = serversModule.getServers().map(s => ({ id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || '' })); const virtualHostsList = vhostsModule.getVirtualHosts() .map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl, useTls: v.useTls === true })); const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({ id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort })); const snapshot = { version: 2, settings: settingsModule.getSettings(), nextServerId: serversModule.getNextServerId(), nextServiceTunnelId: svcModule.getNextServiceTunnelId(), servers: serversList, virtualHosts: virtualHostsList, serviceTunnels: serviceTunnelsList, sshConnections: connectionsModule.getSshConnections(), rdpConnections: connectionsModule.getRdpConnections() }; stateModule.saveStateSync(snapshot); if (onStateSaved) onStateSaved(snapshot); } /** * Return current in-memory state in the same shape as saveState() snapshot. * Used by sync-manager to diff and apply only changes. * @returns {{ version: number, settings: object, nextServerId: number, nextServiceTunnelId: number, servers: Array, virtualHosts: Array, serviceTunnels: Array, sshConnections: Array, rdpConnections: Array }} */ function getStateSnapshot() { const serversList = serversModule.getServers().map(s => ({ id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || '' })); const virtualHostsList = vhostsModule.getVirtualHosts() .map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl, useTls: v.useTls === true })); const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({ id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort })); return { version: 2, settings: settingsModule.getSettings(), nextServerId: serversModule.getNextServerId(), nextServiceTunnelId: svcModule.getNextServiceTunnelId(), servers: serversList, virtualHosts: virtualHostsList, serviceTunnels: serviceTunnelsList, sshConnections: connectionsModule.getSshConnections(), rdpConnections: connectionsModule.getRdpConnections() }; } /** * Apply non-tunnel state from a snapshot (settings, connection lists, ID counters). * Does not start/stop tunnels; used by sync-manager before applying tunnel diffs. * @param {object} snapshot - Snapshot with settings, sshConnections, rdpConnections, nextServerId, nextServiceTunnelId */ function applySnapshotData(snapshot) { if (!snapshot || typeof snapshot !== 'object') return; if (snapshot.settings != null) settingsModule.applyLoaded(snapshot.settings); const ssh = Array.isArray(snapshot.sshConnections) ? snapshot.sshConnections : []; const rdp = Array.isArray(snapshot.rdpConnections) ? snapshot.rdpConnections : []; connectionsModule.applyLoaded(ssh, rdp); if (typeof snapshot.nextServerId === 'number') serversModule.applyLoaded(snapshot.nextServerId); if (typeof snapshot.nextServiceTunnelId === 'number') svcModule.applyLoaded(snapshot.nextServiceTunnelId); } // Inject the shared saveState and emit callbacks into each sub-module settingsModule.init(saveState); connectionsModule.init(saveState); serversModule.init(saveState, settingsModule.getReadyTimeoutMs); vhostsModule.init(saveState, emit, settingsModule.getReadyTimeoutMs, settingsModule.getTunnelAutoReconnect); svcModule.init(saveState, emit, settingsModule.getReadyTimeoutMs, settingsModule.getTunnelAutoReconnect); // ── Storage path ────────────────────────────────────────────────────────────── /** * Set the base directory used for state.json persistence. * Must be called before `restorePersistedState`. * @param {string} baseDir - Absolute path to the storage directory. */ function setStoragePath(baseDir) { stateModule.setStoragePath(baseDir); } function ensureStorageDir() { stateModule.ensureStorageDir(); } // ── Restore persisted state ─────────────────────────────────────────────────── /** * Load state.json and apply the persisted settings, connection lists, and ID counters * to all sub-modules. Returns the raw persisted data so the caller can re-start tunnels. * @returns {{settings: object, servers: Array, virtualHosts: Array, serviceTunnels: Array, sshConnections: Array, rdpConnections: Array}} */ function restorePersistedState() { const loaded = stateModule.loadState(); settingsModule.applyLoaded(loaded.settings); connectionsModule.applyLoaded(loaded.sshConnections, loaded.rdpConnections); serversModule.applyLoaded(loaded.nextServerId); svcModule.applyLoaded(loaded.nextServiceTunnelId); return { settings: settingsModule.getSettings(), servers: loaded.servers, virtualHosts: loaded.virtualHosts, serviceTunnels: loaded.serviceTunnels || [], sshConnections: connectionsModule.getSshConnections(), rdpConnections: connectionsModule.getRdpConnections() }; } // ── Cleanup ─────────────────────────────────────────────────────────────────── /** * Close all active tunnels across all sub-modules. * Called during native host shutdown to release resources cleanly. * @returns {Promise} */ async function cleanup() { await serversModule.cleanupServers(); await vhostsModule.cleanupVirtualHosts(); await svcModule.cleanupServiceTunnels(); } // ── Re-export full original API ─────────────────────────────────────────────── module.exports = { // Event emitter setEventEmitter, // Storage setStoragePath, ensureStorageDir, setOnStateSaved, setStateSaveSuppressed, getStateSnapshot, applySnapshotData, // Settings getSettings: settingsModule.getSettings, updateSettings: settingsModule.updateSettings, getProxyPort: settingsModule.getProxyPort, setProxyPort: settingsModule.setProxyPort, // SSH/RDP connection lists getSshConnections: connectionsModule.getSshConnections, setSshConnections: connectionsModule.setSshConnections, getRdpConnections: connectionsModule.getRdpConnections, setRdpConnections: connectionsModule.setRdpConnections, // Servers startServer: serversModule.startServer, stopServer: serversModule.stopServer, getServers: serversModule.getServers, // Virtual hosts setVirtualHost: vhostsModule.setVirtualHost, removeVirtualHost: vhostsModule.removeVirtualHost, getVirtualHosts: vhostsModule.getVirtualHosts, getLocalPortForHostname: vhostsModule.getLocalPortForHostname, getLocalBackend: vhostsModule.getLocalBackend, getVirtualHostMap: vhostsModule.getVirtualHostMap, // Service tunnels startServiceTunnel: svcModule.startServiceTunnel, stopServiceTunnel: svcModule.stopServiceTunnel, getServiceTunnels: svcModule.getServiceTunnels, // Lookup lookup, // Lifecycle restorePersistedState, cleanup }; // ── Lookup (standalone, only needs Holesail) ────────────────────────────────── let Holesail = null; try { Holesail = require('holesail'); } catch (_) {} /** * Resolve an hs:// key via the DHT and return connection metadata without * establishing a full tunnel. * @param {object} payload * @param {string} payload.hsUrl - The hs:// key to look up. * @returns {Promise<{ok: boolean, host?: string, port?: number, protocol?: string, secure?: boolean, error?: string}>} */ async function lookup(payload) { if (!Holesail) return { ok: false, error: 'Holesail module not installed' }; const url = payload.url || payload.hsUrl; if (!url) return { ok: false, error: 'url required' }; try { const result = await Holesail.lookup(url); return { ok: true, ...result }; } catch (e) { return { ok: false, error: e.message }; } }