'use strict'; const { app, BrowserWindow, clipboard, dialog, ipcMain, net, protocol, shell } = require('electron'); const fs = require('node:fs/promises'); const path = require('node:path'); const { pathToFileURL } = require('node:url'); const { buildCsv } = require('./static/export-utils'); const { APP_RENDERER_URL, resolveAppAssetPath } = require('./src/app-protocol'); const { KEY_PATTERN } = require('./src/discovery'); const { createScanSession } = require('./src/scan-session'); protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true }, }]); let mainWindow = null; const scanSession = createScanSession(); function isTrustedSender(event) { if (!mainWindow || mainWindow.isDestroyed() || !event || !event.senderFrame) return false; return event.sender === mainWindow.webContents && event.senderFrame === mainWindow.webContents.mainFrame && event.senderFrame.url === APP_RENDERER_URL; } function registerIpc(channel, handler) { ipcMain.handle(channel, async (event, ...args) => { if (!isTrustedSender(event)) throw new Error('Forbidden IPC sender'); return handler(...args); }); } async function openCameraUrl(value) { const url = scanSession.allowedCameraUrl(value); if (!url) return; await shell.openExternal(url); } function registerIpcHandlers() { registerIpc('scan-cameras', scanSession.scanOnce); registerIpc('copy-text', (value) => { const text = String(value || ''); if (!KEY_PATTERN.test(text)) throw new Error('Invalid Alta Key.'); clipboard.writeText(text); return { ok: true }; }); registerIpc('export-csv', async () => { const csv = buildCsv(scanSession.getLatestCameras()); if (Buffer.byteLength(csv, 'utf8') > 1024 * 1024) throw new Error('CSV export is too large.'); const result = await dialog.showSaveDialog(mainWindow, { title: 'Export discovered Alta cameras', defaultPath: 'alta-camera-keys.csv', filters: [{ name: 'CSV file', extensions: ['csv'] }], }); if (result.canceled || !result.filePath) return { ok: false, canceled: true }; await fs.writeFile(result.filePath, csv, 'utf8'); return { ok: true, canceled: false }; }); } function registerAppProtocol() { const staticDir = path.join(__dirname, 'static'); protocol.handle('app', (request) => { const assetPath = resolveAppAssetPath(request.url, staticDir); if (!assetPath) return new Response('Not found', { status: 404 }); return net.fetch(pathToFileURL(assetPath).href); }); } function denyRendererPermissions(targetSession) { targetSession.setPermissionCheckHandler(() => false); targetSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false)); } function createWindow() { mainWindow = new BrowserWindow({ width: 1180, height: 760, minWidth: 820, minHeight: 620, backgroundColor: '#f5f6f8', icon: path.join(__dirname, 'assets', 'icon.png'), title: 'Alta Local Camera Discovery', autoHideMenuBar: true, webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true, preload: path.join(__dirname, 'preload.js'), }, }); denyRendererPermissions(mainWindow.webContents.session); mainWindow.webContents.setWindowOpenHandler(({ url }) => { openCameraUrl(url).catch(() => {}); return { action: 'deny' }; }); mainWindow.webContents.on('will-navigate', (event) => event.preventDefault()); mainWindow.loadURL(APP_RENDERER_URL); if (!app.isPackaged && process.argv.includes('--dev')) mainWindow.webContents.openDevTools(); } app.whenReady().then(() => { registerAppProtocol(); registerIpcHandlers(); createWindow(); }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });