// Electron main process. // // Responsibilities: // * Create the BrowserWindow with a frameless + custom title bar so // the renderer owns the chrome (matches macOS traffic lights and // draws our 3-panel shell without OS padding). // * Bridge safe native APIs to the renderer through preload.ts using // contextBridge — keeps the renderer sandboxed (contextIsolation on, // nodeIntegration off). // * Deep-link handler for dchain://chat/ and similar. Stub for now. // // Everything chain-related (HTTP / WS / crypto) still runs in the // renderer — Electron main stays a thin shell + native capabilities. import { app, BrowserWindow, shell, ipcMain, dialog, safeStorage, session } from 'electron'; import * as path from 'node:path'; import * as fs from 'node:fs/promises'; const isDev = !!process.env.VITE_DEV_SERVER_URL; let mainWindow: BrowserWindow | null = null; // Content-Security-Policy is set here (not in ) so we can diverge // dev vs. production: Vite's HMR uses eval() which needs 'unsafe-eval', // but shipping that in a release build would earn us a security warning // from Electron and weaken XSS defence for no good reason. function installCSP(): void { const policy = isDev ? // Dev: permissive enough for Vite HMR (eval + WS) while still // denying random remote scripts. connect-src is wide-open because // the user picks their own node URL at runtime. "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; " + "connect-src 'self' ws: wss: http: https:; " + "img-src 'self' data: blob: http: https:;" : // Prod: no eval, no remote scripts. connect-src stays open so the // user can target any node they configure. "default-src 'self'; " + "script-src 'self'; " + "style-src 'self' 'unsafe-inline'; " + "connect-src 'self' ws: wss: http: https:; " + "img-src 'self' data: blob: http: https:;"; session.defaultSession.webRequest.onHeadersReceived((details, cb) => { cb({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': [policy], }, }); }); } function createWindow(): void { mainWindow = new BrowserWindow({ width: 1280, height: 820, minWidth: 900, minHeight: 600, backgroundColor: '#000000', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden', // Expose traffic-light buttons on macOS; Windows/Linux use a custom // title-bar painted by the renderer. titleBarOverlay: process.platform === 'win32' ? { color: '#000000', symbolColor: '#ffffff', height: 32, } : undefined, frame: process.platform === 'darwin', webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, sandbox: false, // safeStorage requires non-sandboxed preload }, show: false, }); mainWindow.once('ready-to-show', () => mainWindow?.show()); if (isDev) { mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL!); mainWindow.webContents.openDevTools({ mode: 'detach' }); } else { mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); } // Open external links (http/https in ) in the default // browser rather than a new Electron window — safer, and a desktop // user's muscle memory expects this. mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (/^https?:\/\//.test(url)) { shell.openExternal(url); return { action: 'deny' }; } return { action: 'allow' }; }); } // ── IPC — safe subset bridged into the renderer via preload ──────────── // Keys are persisted encrypted by the OS keychain via safeStorage. // Fallback to plaintext file only if the user's OS lacks an encryption // backend (surfaced as a warning in Settings → Advanced). const KEYFILE_PATH = () => path.join(app.getPath('userData'), 'keyfile.bin'); ipcMain.handle('keyfile:load', async (): Promise => { try { const raw = await fs.readFile(KEYFILE_PATH()); if (safeStorage.isEncryptionAvailable()) { return safeStorage.decryptString(raw); } // File was stored without encryption — treat as plaintext. return raw.toString('utf8'); } catch { return null; } }); ipcMain.handle('keyfile:save', async (_e, json: string): Promise => { await fs.mkdir(path.dirname(KEYFILE_PATH()), { recursive: true }); if (safeStorage.isEncryptionAvailable()) { await fs.writeFile(KEYFILE_PATH(), safeStorage.encryptString(json)); } else { // Surface the insecure path loudly in the renderer's Settings, // but don't refuse — on some Linux boxes libsecret isn't installed // and the user explicitly wants a fallback. await fs.writeFile(KEYFILE_PATH(), json, 'utf8'); } }); ipcMain.handle('keyfile:delete', async (): Promise => { await fs.rm(KEYFILE_PATH(), { force: true }); }); ipcMain.handle('keyfile:encryption-available', async (): Promise => { return safeStorage.isEncryptionAvailable(); }); ipcMain.handle('dialog:open-file', async (_e, opts: Electron.OpenDialogOptions) => { if (!mainWindow) return null; const res = await dialog.showOpenDialog(mainWindow, opts); if (res.canceled || res.filePaths.length === 0) return null; return res.filePaths[0]; }); ipcMain.handle('dialog:save-file', async (_e, opts: Electron.SaveDialogOptions) => { if (!mainWindow) return null; const res = await dialog.showSaveDialog(mainWindow, opts); if (res.canceled || !res.filePath) return null; return res.filePath; }); ipcMain.handle('fs:read-text', async (_e, filePath: string) => { return fs.readFile(filePath, 'utf8'); }); ipcMain.handle('fs:write-text', async (_e, filePath: string, contents: string) => { return fs.writeFile(filePath, contents, 'utf8'); }); ipcMain.handle('app:version', async () => app.getVersion()); ipcMain.handle('app:platform', async () => process.platform); // ── Lifecycle ───────────────────────────────────────────────────────── app.whenReady().then(() => { installCSP(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });