'use strict'; const { app, BrowserWindow, dialog, ipcMain, safeStorage, shell } = require('electron'); const http = require('node:http'); const path = require('node:path'); const { pathToFileURL } = require('node:url'); const { createSessionStore } = require('./src/session-store'); const { createAltaClient } = require('./src/alta-client'); const { createProxyManager } = require('./src/proxy-launch'); const { checkForUpdate } = require('./src/update-policy'); const { AppRuntime, PAIRING_ENVELOPE_FILENAME, PairingController, createBridgeHandler, } = require('./src/electron-runtime'); const BRIDGE_HOST = '127.0.0.1'; const BRIDGE_PORT = 18247; let mainWindow = null; let bridgeServer = null; let runtime = null; let pairingController = null; let firstRunPairingSecret = null; let quitCleanupInProgress = false; let allowConfirmedQuit = false; function getAppDirectory() { return app.isPackaged ? path.dirname(process.env.PORTABLE_EXECUTABLE_FILE || app.getPath('exe')) : __dirname; } function sendConnectionState() { if (mainWindow && !mainWindow.isDestroyed() && runtime) { mainWindow.webContents.send('connection-state-changed', runtime.getConnectionState()); } } function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, icon: path.join(__dirname, 'assets', 'icon.png'), title: 'Alta Video Camera Proxy', webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true, preload: path.join(__dirname, 'preload.js'), }, }); mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' })); mainWindow.webContents.on('will-navigate', (event) => event.preventDefault()); mainWindow.loadFile(path.join(__dirname, 'index.html')); if (process.argv.includes('--dev')) mainWindow.webContents.openDevTools(); } function isTrustedSender(event) { if (!mainWindow || mainWindow.isDestroyed() || !event) return false; const webContents = mainWindow.webContents; const frame = event.senderFrame; const expectedUrl = pathToFileURL(path.join(__dirname, 'index.html')).href; return event.sender === webContents && frame === webContents.mainFrame && frame.url === expectedUrl; } function registerIpc(channel, handler) { ipcMain.handle(channel, async (event, ...args) => { if (!isTrustedSender(event)) throw new Error('Forbidden IPC sender'); return handler(...args); }); } function registerIpcHandlers() { registerIpc('get-devices', () => runtime.getDevices()); registerIpc('get-device-sites', () => runtime.getDeviceSites()); registerIpc('get-device-groups', () => runtime.getDeviceGroups()); registerIpc('get-device-hierarchy', () => runtime.getDeviceHierarchy()); registerIpc('get-auth-info', () => runtime.getAuthInfo()); registerIpc('launch-proxy', (deviceId, username) => runtime.launchProxy(deviceId, username)); registerIpc('stop-proxy', async (key) => { const result = await runtime.stopProxy(key); sendConnectionState(); return result; }); registerIpc('disconnect', async () => { const state = await runtime.disconnect(); sendConnectionState(); return state; }); registerIpc('get-connection-state', () => runtime.getConnectionState()); registerIpc('check-for-updates', () => runtime.checkForUpdates()); registerIpc('open-fixed-releases-page', () => runtime.openFixedReleasesPage()); registerIpc('rotate-pairing', () => { firstRunPairingSecret = null; return pairingController.rotate(); }); registerIpc('revoke-pairing', () => { firstRunPairingSecret = null; return pairingController.revoke(); }); registerIpc('get-pairing-status', () => { const result = pairingController.getStatus(); if (firstRunPairingSecret) { result.secret = firstRunPairingSecret; firstRunPairingSecret = null; } return result; }); } function startBridgeServer() { if (!pairingController.bridgeAuth) return; const handler = createBridgeHandler({ bridgeAuth: pairingController.bridgeAuth, sessionStore: runtime.sessionStore, onConnectionStateChanged: () => { runtime.onSessionChanged(); sendConnectionState(); }, }); bridgeServer = http.createServer((request, response) => { handler(request, response).catch(() => { if (!response.writableEnded) { response.writeHead(500, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); response.end(JSON.stringify({ success: false, message: 'Bridge request failed' })); } }); }); bridgeServer.on('clientError', (_error, socket) => socket.destroy()); bridgeServer.on('error', (error) => { console.error(`Bridge server unavailable (${error.code || 'UNKNOWN'}).`); }); bridgeServer.listen(BRIDGE_PORT, BRIDGE_HOST); } app.whenReady().then(() => { const sessionStore = createSessionStore(); const altaClient = createAltaClient({ sessionStore }); const proxyManager = createProxyManager({ appDirectory: getAppDirectory() }); const secureStorageAvailable = safeStorage && safeStorage.isEncryptionAvailable(); pairingController = new PairingController({ envelopePath: path.join(app.getPath('userData'), PAIRING_ENVELOPE_FILENAME), protect: secureStorageAvailable ? (value) => safeStorage.encryptString(value) : undefined, unprotect: secureStorageAvailable ? (value) => safeStorage.decryptString(value) : undefined, }); const pairingState = pairingController.initialize(); firstRunPairingSecret = pairingState.secret || null; runtime = new AppRuntime({ sessionStore, altaClient, proxyManager, checkForUpdate, currentVersion: app.getVersion(), openExternal: (url) => shell.openExternal(url), }); registerIpcHandlers(); createWindow(); startBridgeServer(); }); function closeBridgeServer() { if (!bridgeServer) return Promise.resolve(); const server = bridgeServer; return new Promise((resolve, reject) => { const finish = (error) => { if (!error || error.code === 'ERR_SERVER_NOT_RUNNING') { if (bridgeServer === server) bridgeServer = null; resolve(); return; } reject(error); }; try { server.close(finish); } catch (error) { finish(error); } }); } async function notifyQuitBlocked(state) { sendConnectionState(); const activeCount = state && Array.isArray(state.activeProxies) ? state.activeProxies.length : 0; const activeProxiesRemain = activeCount > 0; const processDetail = activeProxiesRemain ? `${activeCount} app-owned proxy process${activeCount === 1 ? '' : 'es'} remain active.` : 'No app-owned proxy process remains active.'; const sessionDetail = state && state.connected ? ' The Alta session is still connected; resolve the process error and try quitting again.' : ' Resolve the shutdown error and try quitting again.'; await dialog.showMessageBox({ type: 'error', title: activeProxiesRemain ? 'Alta Camera Proxy is still running' : 'Alta Camera Proxy could not close', message: activeProxiesRemain ? 'The app could not confirm that every proxy process exited.' : 'The app could not complete shutdown safely.', detail: `${processDetail}${sessionDetail}`, buttons: ['OK'], noLink: true, }); } app.on('before-quit', (event) => { if (allowConfirmedQuit) return; event.preventDefault(); if (quitCleanupInProgress) return; quitCleanupInProgress = true; (async () => { try { const state = runtime ? await runtime.disconnect() : { success: true, connected: false, activeProxies: [] }; if (!state.success || state.activeProxies.length > 0) { await notifyQuitBlocked(state); return; } await closeBridgeServer(); if (runtime) runtime.sessionStore.dispose(); allowConfirmedQuit = true; app.quit(); } catch { const state = runtime ? runtime.getConnectionState() : { connected: false, activeProxies: [] }; await notifyQuitBlocked(state); } finally { quitCleanupInProgress = false; } })(); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0 && runtime) createWindow(); });