From 3641cb113d8c87df1044ffb3f7a262ea8634b877 Mon Sep 17 00:00:00 2001 From: vsecoder Date: Wed, 22 Apr 2026 17:13:26 +0300 Subject: [PATCH] fix(desktop): CSP via webRequest + boot error visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two problems from the first alpha4 run reported as "blank window + CSP warning in devtools": 1. CSP was set via in index.html with a strict policy (script-src 'self'). Vite's dev server uses eval() for HMR, which the strict CSP blocked at module-load time, so the renderer never ran. The meta CSP also conflicted with Electron's own security heuristics (hence the warning even though *we* had a policy — Electron was looking for it on the HTTP response). Moved the CSP to electron/main.ts via session.webRequest .onHeadersReceived. Dev profile enables 'unsafe-eval' + ws:/wss: for HMR; production profile stays strict (no eval, no remote scripts, connect-src still wide because the user picks arbitrary node URLs). 2. When window.dchain isn't available (preload failed to load, dev misconfig, etc.), loadKeyFile() throws inside a useEffect. React swallows async-effect throws, so the app renders blank forever. Added: - requireDchain() guard in storage.ts with an explicit error. - App.tsx catches boot-effect errors and renders them inline. - ErrorBoundary.tsx for render-time throws. - window.addEventListener('error') in main.tsx as a last-resort paint for throws that escape React entirely. Also: npm script electron:dev now rebuilds main.ts before spawning Electron (was a silent concurrency bug — TypeScript errors in main.ts would produce stale dist-electron/). --- desktop/electron/main.ts | 32 +++++++++++++++++++- desktop/index.html | 7 +++-- desktop/package.json | 4 +-- desktop/src/App.tsx | 42 ++++++++++++++++++++------ desktop/src/ErrorBoundary.tsx | 55 +++++++++++++++++++++++++++++++++++ desktop/src/lib/storage.ts | 23 +++++++++++++-- desktop/src/main.tsx | 17 ++++++++++- 7 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 desktop/src/ErrorBoundary.tsx diff --git a/desktop/electron/main.ts b/desktop/electron/main.ts index 3b5d60c..8c6f00d 100644 --- a/desktop/electron/main.ts +++ b/desktop/electron/main.ts @@ -12,7 +12,7 @@ // 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 } from 'electron'; +import { app, BrowserWindow, shell, ipcMain, dialog, safeStorage, session } from 'electron'; import * as path from 'node:path'; import * as fs from 'node:fs/promises'; @@ -20,6 +20,35 @@ 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, @@ -134,6 +163,7 @@ ipcMain.handle('app:platform', async () => process.platform); // ── Lifecycle ───────────────────────────────────────────────────────── app.whenReady().then(() => { + installCSP(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); diff --git a/desktop/index.html b/desktop/index.html index 40c8802..5309a15 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -2,8 +2,11 @@ - + DChain