diff --git a/electron/main.js b/electron/main.js index ac4470a7..f5242128 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,5 +1,8 @@ const { app, components, BrowserWindow, ipcMain, shell, session, dialog, crashReporter, powerMonitor } = require('electron'); +// Browser cards live in their own persistent partition so cookies/localStorage/IndexedDB survive reload + quit (Discord etc. stay logged in) and site data stays isolated from the app's defaultSession. The "clear browsing data" wipe nukes only this partition. MUST match BROWSER_PARTITION in frontend BrowserCard.tsx. +const BROWSER_PARTITION = 'persist:openswarm-browser'; + // E2E flag: when OPENSWARM_E2E=1, append a Chromium command-line switch the // renderer reads at startup to set window.__OPENSWARM_E2E__ = true BEFORE any // page script parses, so the production-build store-on-window gate fires @@ -1719,45 +1722,50 @@ app.whenReady().then(async () => { try { app.dock.setIcon(iconPath); } catch (_) {} } - session.defaultSession.setPermissionRequestHandler((_wc, permission, callback) => { - const allowed = [ - 'media', 'mediaKeySystem', 'protected-media-identifier', - 'geolocation', 'notifications', 'midi', 'midiSysex', - 'clipboard-read', 'clipboard-sanitized-write', - 'pointerLock', 'fullscreen', 'idle-detection', - ]; - console.log('Permission request:', permission, '->', allowed.includes(permission) ? 'granted' : 'denied'); - callback(allowed.includes(permission)); - }); - session.defaultSession.setPermissionCheckHandler((_wc, permission) => { - const allowed = [ - 'media', 'mediaKeySystem', 'protected-media-identifier', - 'clipboard-read', 'clipboard-sanitized-write', - 'pointerLock', 'fullscreen', 'idle-detection', - ]; - return allowed.includes(permission); - }); + // Same permission grants + iframe header-strip on BOTH the app's defaultSession and the browser-card partition. A named partition is a separate session, so without re-applying these, browser cards lose camera/mic prompts and the ability to embed sites that send X-Frame-Options. + const configureBrowsingSession = (ses) => { + ses.setPermissionRequestHandler((_wc, permission, callback) => { + const allowed = [ + 'media', 'mediaKeySystem', 'protected-media-identifier', + 'geolocation', 'notifications', 'midi', 'midiSysex', + 'clipboard-read', 'clipboard-sanitized-write', + 'pointerLock', 'fullscreen', 'idle-detection', + ]; + console.log('Permission request:', permission, '->', allowed.includes(permission) ? 'granted' : 'denied'); + callback(allowed.includes(permission)); + }); + ses.setPermissionCheckHandler((_wc, permission) => { + const allowed = [ + 'media', 'mediaKeySystem', 'protected-media-identifier', + 'clipboard-read', 'clipboard-sanitized-write', + 'pointerLock', 'fullscreen', 'idle-detection', + ]; + return allowed.includes(permission); + }); - // Strip X-Frame-Options and CSP frame-ancestors directives on iframe subframe loads so the Windows BrowserCard iframe fallback (used because tag commit segfaults on Chromium 144 + this Electron 40 CastLabs build) can render sites that normally refuse to be embedded. Scoped to types:['sub_frame'] so OAuth popups, the main app frame, deep-link redirects, and DRM license fetches keep their security headers intact. urls filter limits to http/https so file:// loads of the bundled frontend are untouched. - session.defaultSession.webRequest.onHeadersReceived( - // Electron's webRequest type name for iframes is 'subFrame' (camelCase), not the Chrome-extension 'sub_frame' — passing the wrong name throws "Invalid type sub_frame" synchronously which becomes an unhandledRejection and prevents the app from booting. - { urls: ['http://*/*', 'https://*/*'], types: ['subFrame'] }, - (details, callback) => { - const headers = { ...(details.responseHeaders || {}) }; - for (const k of Object.keys(headers)) { - const lk = k.toLowerCase(); - if (lk === 'x-frame-options') { - delete headers[k]; - } else if (lk === 'content-security-policy' || lk === 'content-security-policy-report-only') { - const cleaned = (headers[k] || []) - .map((v) => v.split(';').filter((d) => !/^\s*frame-ancestors\b/i.test(d)).join(';').trim()) - .filter(Boolean); - if (cleaned.length) headers[k] = cleaned; else delete headers[k]; + // Strip X-Frame-Options and CSP frame-ancestors directives on iframe subframe loads so the Windows BrowserCard iframe fallback (used because tag commit segfaults on Chromium 144 + this Electron 40 CastLabs build) can render sites that normally refuse to be embedded. Scoped to types:['sub_frame'] so OAuth popups, the main app frame, deep-link redirects, and DRM license fetches keep their security headers intact. urls filter limits to http/https so file:// loads of the bundled frontend are untouched. + ses.webRequest.onHeadersReceived( + // Electron's webRequest type name for iframes is 'subFrame' (camelCase), not the Chrome-extension 'sub_frame' — passing the wrong name throws "Invalid type sub_frame" synchronously which becomes an unhandledRejection and prevents the app from booting. + { urls: ['http://*/*', 'https://*/*'], types: ['subFrame'] }, + (details, callback) => { + const headers = { ...(details.responseHeaders || {}) }; + for (const k of Object.keys(headers)) { + const lk = k.toLowerCase(); + if (lk === 'x-frame-options') { + delete headers[k]; + } else if (lk === 'content-security-policy' || lk === 'content-security-policy-report-only') { + const cleaned = (headers[k] || []) + .map((v) => v.split(';').filter((d) => !/^\s*frame-ancestors\b/i.test(d)).join(';').trim()) + .filter(Boolean); + if (cleaned.length) headers[k] = cleaned; else delete headers[k]; + } } - } - callback({ responseHeaders: headers }); - }, - ); + callback({ responseHeaders: headers }); + }, + ); + }; + configureBrowsingSession(session.defaultSession); + configureBrowsingSession(session.fromPartition(BROWSER_PARTITION)); // Read-only logging for DRM license requests — no modifying interceptors // so the network stack can set Content-Type and other headers normally. diff --git a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx index e6e56722..1556a5f3 100644 --- a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx @@ -123,6 +123,9 @@ const chromeUserAgent = navigator.userAgent .replace(/\s*Electron\/\S+/, '') .replace(/\s*OpenSwarm\/\S+/, ''); +// Persistent partition so browser-card logins/cookies/localStorage outlive a reload or quit. MUST match BROWSER_PARTITION in electron/main.js, which configures permissions + iframe header-strip on this exact partition. +const BROWSER_PARTITION = 'persist:openswarm-browser'; + // Sync exposure set at preload boot; async API fallback for older builds. const webviewPreloadPath: string | undefined = isElectron ? ((window as any).__OPENSWARM_WEBVIEW_PRELOAD__ @@ -1136,6 +1139,7 @@ const BrowserCard: React.FC = ({ else webviewMap.current.delete(tab.id); }} data-tab-id={tab.id} + partition={BROWSER_PARTITION} src="about:blank" {...({ allowpopups: 'true' } as any) /* React drops boolean-valued unknown attrs, so string it stays; @types/react wrongly says boolean */} useragent={chromeUserAgent}