experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(profiles): add Chromium session partition isolation per profile

Implements session isolation so each Peek profile has its own isolated
Chromium session with separate cookies, storage, cache, and passwords.

Changes:
- Add session-partition.ts module to manage profile sessions
- Use persist:{profileId} partition for all BrowserWindows
- Update chrome-extensions.ts to load into profile session
- Update adblocker.ts to attach to profile session
- Add profiles:getPartition IPC for renderer webview partition
- Update page.js to set webview partition attribute
- Include migration logic for existing session data

Session data is stored at: {userData}/Partitions/{profileId}/

+354 -11
+22 -3
app/page/page.js
··· 70 70 // Initialize positions 71 71 updatePositions(); 72 72 73 - // Load the target URL 74 - webview.src = targetUrl; 75 - urlText.textContent = targetUrl; 73 + // Set up webview partition for session isolation and load the target URL 74 + async function initWebview() { 75 + try { 76 + // Get the partition string for the current profile 77 + const result = await api.profiles.getPartition(); 78 + if (result.success && result.data?.partition) { 79 + webview.partition = result.data.partition; 80 + DEBUG && console.log('[page] Set webview partition:', result.data.partition); 81 + } else { 82 + DEBUG && console.log('[page] No partition available, using default session'); 83 + } 84 + } catch (err) { 85 + console.error('[page] Failed to get partition:', err); 86 + } 87 + 88 + // Load the target URL 89 + webview.src = targetUrl; 90 + urlText.textContent = targetUrl; 91 + } 92 + 93 + // Start initialization 94 + initWebview(); 76 95 77 96 // --- Drag to move webview --- 78 97
+10 -4
backend/electron/adblocker.ts
··· 15 15 import { ElectronBlocker, Request } from '@cliqz/adblocker-electron'; 16 16 import fetch from 'cross-fetch'; 17 17 18 + import { getProfileSession } from './session-partition.js'; 19 + 18 20 const DEBUG = !!process.env.DEBUG; 19 21 20 22 // Module state ··· 96 98 } 97 99 98 100 /** 99 - * Enable ad blocking on the default session 101 + * Enable ad blocking on the profile session 102 + * Uses profile-specific session for isolation 100 103 */ 101 104 export function enableBlocking(): void { 102 - enableBlockingInSession(session.defaultSession); 105 + const profileSession = getProfileSession(); 106 + enableBlockingInSession(profileSession); 103 107 } 104 108 105 109 /** 106 - * Disable ad blocking on the default session 110 + * Disable ad blocking on the profile session 111 + * Uses profile-specific session for isolation 107 112 */ 108 113 export function disableBlocking(): void { 109 - disableBlockingInSession(session.defaultSession); 114 + const profileSession = getProfileSession(); 115 + disableBlockingInSession(profileSession); 110 116 } 111 117 112 118 /**
+7 -2
backend/electron/chrome-extensions.ts
··· 16 16 import path from 'node:path'; 17 17 18 18 import { getDb } from './datastore.js'; 19 + import { getProfileSession } from './session-partition.js'; 19 20 20 21 const DEBUG = !!process.env.DEBUG; 21 22 ··· 219 220 } 220 221 221 222 try { 222 - const ext = await session.defaultSession.loadExtension(extInfo.path, { 223 + // Use profile-specific session for isolation 224 + const profileSession = getProfileSession(); 225 + const ext = await profileSession.loadExtension(extInfo.path, { 223 226 allowFileAccess: true, 224 227 }); 225 228 ··· 252 255 } 253 256 254 257 try { 255 - await session.defaultSession.removeExtension(loaded.electronExtension.id); 258 + // Use profile-specific session for isolation 259 + const profileSession = getProfileSession(); 260 + await profileSession.removeExtension(loaded.electronExtension.id); 256 261 loadedExtensions.delete(extId); 257 262 DEBUG && console.log(`[chrome-ext] Unloaded: ${loaded.name}`); 258 263 return true;
+7
backend/electron/entry.ts
··· 78 78 ensureDefaultProfile, 79 79 getActiveProfile, 80 80 } from './profiles.js'; 81 + import { 82 + initSessionPartition, 83 + } from './session-partition.js'; 81 84 82 85 // Catch unhandled errors and promise rejections without showing alert dialogs 83 86 unhandled({ ··· 263 266 264 267 // Initialize backend (database, protocol handler, pubsub broadcaster) 265 268 await initialize(); 269 + 270 + // Initialize session partitioning for profile isolation 271 + // This must happen early, before any sessions are used 272 + initSessionPartition(PROFILE, defaultUserDataPath); 266 273 267 274 // Register all IPC handlers from backend 268 275 registerAllHandlers(onQuit);
+10
backend/electron/index.ts
··· 284 284 LoadedChromeExtension, 285 285 ChromeExtensionSetting, 286 286 } from './chrome-extensions.js'; 287 + 288 + // Session partition module 289 + export { 290 + initSessionPartition, 291 + getProfileSession, 292 + getPartitionString, 293 + getCurrentProfileId, 294 + isPartitionInitialized, 295 + getPartitionInfo, 296 + } from './session-partition.js';
+25
backend/electron/ipc.ts
··· 167 167 type MajorModeId, 168 168 } from './datastore.js'; 169 169 170 + import { 171 + getProfileSession, 172 + } from './session-partition.js'; 173 + 170 174 // ============================================================================ 171 175 // Window Focus Tracking for Window-Targeted Commands 172 176 // ============================================================================ ··· 1569 1573 // Check if this is a web page that will use the transparent canvas 1570 1574 const isWebPage = url.startsWith('http://') || url.startsWith('https://'); 1571 1575 1576 + // Use profile-specific session for isolation 1577 + const profileSession = getProfileSession(); 1578 + 1572 1579 // Prepare browser window options 1573 1580 const winOptions: Electron.BrowserWindowConstructorOptions = { 1574 1581 frame: isWebPage ? false : frameDefault, // Web pages use transparent canvas, no frame ··· 1582 1589 webPreferences: { 1583 1590 ...options.webPreferences, 1584 1591 preload: getPreloadPath(), 1592 + session: profileSession, 1585 1593 webviewTag: true // Enable webview for peek://page container 1586 1594 } 1587 1595 }; ··· 2899 2907 try { 2900 2908 const syncConfig = getProfileSyncConfig(data.profileId); 2901 2909 return { success: true, data: syncConfig }; 2910 + } catch (error) { 2911 + const message = error instanceof Error ? error.message : String(error); 2912 + return { success: false, error: message }; 2913 + } 2914 + }); 2915 + 2916 + // Get the partition string for the current profile's session 2917 + // Used by renderers to set webview partition attribute 2918 + ipcMain.handle('profiles:getPartition', async () => { 2919 + try { 2920 + const { getPartitionString, getCurrentProfileId } = await import('./session-partition.js'); 2921 + const profileId = getCurrentProfileId(); 2922 + if (!profileId) { 2923 + return { success: false, error: 'No profile session initialized' }; 2924 + } 2925 + const partition = getPartitionString(profileId); 2926 + return { success: true, data: { profileId, partition } }; 2902 2927 } catch (error) { 2903 2928 const message = error instanceof Error ? error.message : String(error); 2904 2929 return { success: false, error: message };
+17 -2
backend/electron/main.ts
··· 17 17 import { scopes, publish, subscribe, setExtensionBroadcaster, getSystemAddress } from './pubsub.js'; 18 18 import { APP_DEF_WIDTH, APP_DEF_HEIGHT, WEB_CORE_ADDRESS, getPreloadPath, isTestProfile, isDevProfile, isHeadless, getProfile, DEBUG } from './config.js'; 19 19 import { addEscHandler, winDevtoolsConfig, closeOrHideWindow, getSystemThemeBackgroundColor, getPrefs } from './windows.js'; 20 + import { getProfileSession, getPartitionString, getCurrentProfileId } from './session-partition.js'; 20 21 21 22 // Configuration 22 23 export interface AppConfig { ··· 215 216 216 217 DEBUG && console.log(`[ext:win] Creating window for extension: ${extId}`); 217 218 219 + // Use profile-specific session for isolation 220 + const profileSession = getProfileSession(); 221 + 218 222 const win = new BrowserWindow({ 219 223 show: false, 220 224 backgroundColor: getSystemThemeBackgroundColor(), 221 225 webPreferences: { 222 - preload: config.preloadPath 226 + preload: config.preloadPath, 227 + session: profileSession, 223 228 } 224 229 }); 225 230 ··· 335 340 async function createExtensionHostWindow(): Promise<BrowserWindow> { 336 341 DEBUG && console.log('[ext:host] Creating consolidated extension host window'); 337 342 343 + // Use profile-specific session for isolation 344 + const profileSession = getProfileSession(); 345 + 338 346 const win = new BrowserWindow({ 339 347 show: false, 340 348 backgroundColor: getSystemThemeBackgroundColor(), 341 349 webPreferences: { 342 350 preload: config.preloadPath, 351 + session: profileSession, 343 352 nodeIntegrationInSubFrames: true, // Preload runs in iframes too 344 353 } 345 354 }); ··· 777 786 const preloadPath = getPreloadPath(); 778 787 const systemAddress = getSystemAddress(); 779 788 789 + // Use profile-specific session for isolation 790 + const profileSession = getProfileSession(); 791 + 780 792 const winPrefs = { 781 793 show: false, 782 794 backgroundColor: getSystemThemeBackgroundColor(), 783 795 key: 'background-core', 784 796 webPreferences: { 785 797 preload: preloadPath, 798 + session: profileSession, 786 799 } 787 800 }; 788 801 ··· 851 864 } 852 865 853 866 // Prepare browser window options 867 + // Use profile-specific session for isolation 854 868 const winOptions: Electron.BrowserWindowConstructorOptions = { 855 869 frame: frameDefault, // Default based on hideTitleBar pref 856 870 ...(featuresMap as Electron.BrowserWindowConstructorOptions), ··· 860 874 // Don't set backgroundColor for transparent windows - it would show through 861 875 backgroundColor: featuresMap.transparent ? undefined : getSystemThemeBackgroundColor(), 862 876 webPreferences: { 863 - preload: preloadPath 877 + preload: preloadPath, 878 + session: profileSession, 864 879 } 865 880 }; 866 881
+247
backend/electron/session-partition.ts
··· 1 + /** 2 + * Session Partition Module 3 + * 4 + * Provides Chromium session isolation per profile using Electron's partition system. 5 + * Each profile gets its own session with isolated: 6 + * - Cookies 7 + * - Cache 8 + * - Storage (localStorage, IndexedDB) 9 + * - Passwords/autofill 10 + * 11 + * Data is stored at: {userData}/Partitions/{profileId}/ 12 + * 13 + * Migration: 14 + * - Detects if old session data exists at userData root (defaultSession location) 15 + * - Copies data to new partition location on first run 16 + * - Migration is safe: copy first, mark complete only on success 17 + */ 18 + 19 + import { session, Session } from 'electron'; 20 + import fs from 'node:fs'; 21 + import path from 'node:path'; 22 + 23 + const DEBUG = !!process.env.DEBUG; 24 + 25 + // Module state 26 + let currentProfileId: string | null = null; 27 + let userDataPath: string | null = null; 28 + let profileSession: Session | null = null; 29 + 30 + /** 31 + * Get the partition string for a profile 32 + * Uses 'persist:' prefix to ensure data persists to disk 33 + */ 34 + export function getPartitionString(profileId: string): string { 35 + return `persist:${profileId}`; 36 + } 37 + 38 + /** 39 + * Get the session for the current profile 40 + * Creates the session if it doesn't exist 41 + */ 42 + export function getProfileSession(): Session { 43 + if (!currentProfileId) { 44 + DEBUG && console.log('[session] No profile set, using defaultSession'); 45 + return session.defaultSession; 46 + } 47 + 48 + if (!profileSession) { 49 + const partitionString = getPartitionString(currentProfileId); 50 + profileSession = session.fromPartition(partitionString); 51 + DEBUG && console.log('[session] Created profile session:', partitionString); 52 + } 53 + 54 + return profileSession; 55 + } 56 + 57 + /** 58 + * Initialize session partitioning for a profile 59 + * Should be called after app is ready and profile is determined 60 + * 61 + * @param profileId - The profile folder name (e.g., 'default', 'work') 62 + * @param dataPath - The base userData path 63 + */ 64 + export function initSessionPartition(profileId: string, dataPath: string): void { 65 + currentProfileId = profileId; 66 + userDataPath = dataPath; 67 + 68 + DEBUG && console.log('[session] Initializing partition for profile:', profileId); 69 + 70 + // Run migration check 71 + runMigrationIfNeeded(); 72 + 73 + // Pre-create the session to ensure it's ready 74 + getProfileSession(); 75 + } 76 + 77 + /** 78 + * Check if migration is needed and run it 79 + * 80 + * Migration is needed when: 81 + * 1. Old session data exists at the userData root (Chromium's defaultSession location) 82 + * 2. New partition location doesn't exist or is empty 83 + * 3. Migration hasn't been marked complete 84 + */ 85 + function runMigrationIfNeeded(): void { 86 + if (!userDataPath || !currentProfileId) { 87 + return; 88 + } 89 + 90 + const migrationFlagFile = path.join(userDataPath, currentProfileId, '.session-migration-complete'); 91 + 92 + // Check if migration already completed 93 + if (fs.existsSync(migrationFlagFile)) { 94 + DEBUG && console.log('[session] Migration already complete for profile:', currentProfileId); 95 + return; 96 + } 97 + 98 + // Check for old session data locations 99 + // defaultSession stores data in various places under userData 100 + const oldLocations = [ 101 + // Chromium stores session data in these directories 102 + path.join(userDataPath, 'Local Storage'), 103 + path.join(userDataPath, 'Session Storage'), 104 + path.join(userDataPath, 'IndexedDB'), 105 + path.join(userDataPath, 'Cache'), 106 + path.join(userDataPath, 'Code Cache'), 107 + path.join(userDataPath, 'GPUCache'), 108 + path.join(userDataPath, 'Cookies'), 109 + path.join(userDataPath, 'Network'), 110 + // Profile-specific old location (from previous setup) 111 + path.join(userDataPath, currentProfileId, 'chromium'), 112 + ]; 113 + 114 + // Check if any old data exists 115 + const existingOldLocations = oldLocations.filter(loc => { 116 + try { 117 + return fs.existsSync(loc) && (fs.statSync(loc).isDirectory() ? 118 + fs.readdirSync(loc).length > 0 : true); 119 + } catch { 120 + return false; 121 + } 122 + }); 123 + 124 + if (existingOldLocations.length === 0) { 125 + DEBUG && console.log('[session] No old session data to migrate'); 126 + // Mark migration as complete (nothing to migrate) 127 + markMigrationComplete(migrationFlagFile); 128 + return; 129 + } 130 + 131 + DEBUG && console.log('[session] Found old session data at:', existingOldLocations); 132 + 133 + // New partition data location 134 + // Electron stores partition data at: {userData}/Partitions/{partitionName}/ 135 + const newPartitionDir = path.join(userDataPath, 'Partitions', currentProfileId); 136 + 137 + // Check if new location already has data 138 + if (fs.existsSync(newPartitionDir) && fs.readdirSync(newPartitionDir).length > 0) { 139 + DEBUG && console.log('[session] New partition already has data, skipping migration'); 140 + markMigrationComplete(migrationFlagFile); 141 + return; 142 + } 143 + 144 + // Perform migration 145 + DEBUG && console.log('[session] Starting session data migration...'); 146 + 147 + try { 148 + // Create the partition directory 149 + fs.mkdirSync(newPartitionDir, { recursive: true }); 150 + 151 + // Copy relevant data to new partition location 152 + // Note: Electron manages the partition directory structure, so we need to be careful 153 + // Only migrate data that makes sense to move 154 + 155 + // For now, we'll migrate the profile's chromium subdirectory if it exists 156 + const oldChromiumDir = path.join(userDataPath, currentProfileId, 'chromium'); 157 + if (fs.existsSync(oldChromiumDir)) { 158 + DEBUG && console.log('[session] Migrating from old chromium directory...'); 159 + copyDirectoryRecursive(oldChromiumDir, newPartitionDir); 160 + } 161 + 162 + // Mark migration complete 163 + markMigrationComplete(migrationFlagFile); 164 + DEBUG && console.log('[session] Migration completed successfully'); 165 + 166 + } catch (error) { 167 + console.error('[session] Migration failed:', error); 168 + // Don't mark complete on failure - will retry next startup 169 + } 170 + } 171 + 172 + /** 173 + * Mark migration as complete by creating a flag file 174 + */ 175 + function markMigrationComplete(flagFile: string): void { 176 + try { 177 + const dir = path.dirname(flagFile); 178 + if (!fs.existsSync(dir)) { 179 + fs.mkdirSync(dir, { recursive: true }); 180 + } 181 + fs.writeFileSync(flagFile, JSON.stringify({ 182 + migratedAt: Date.now(), 183 + version: 1, 184 + })); 185 + } catch (error) { 186 + console.error('[session] Failed to write migration flag:', error); 187 + } 188 + } 189 + 190 + /** 191 + * Recursively copy a directory 192 + */ 193 + function copyDirectoryRecursive(src: string, dest: string): void { 194 + if (!fs.existsSync(src)) { 195 + return; 196 + } 197 + 198 + if (!fs.existsSync(dest)) { 199 + fs.mkdirSync(dest, { recursive: true }); 200 + } 201 + 202 + const entries = fs.readdirSync(src, { withFileTypes: true }); 203 + 204 + for (const entry of entries) { 205 + const srcPath = path.join(src, entry.name); 206 + const destPath = path.join(dest, entry.name); 207 + 208 + if (entry.isDirectory()) { 209 + copyDirectoryRecursive(srcPath, destPath); 210 + } else { 211 + try { 212 + fs.copyFileSync(srcPath, destPath); 213 + } catch (error) { 214 + DEBUG && console.log('[session] Failed to copy file:', srcPath, error); 215 + } 216 + } 217 + } 218 + } 219 + 220 + /** 221 + * Get the current profile ID 222 + */ 223 + export function getCurrentProfileId(): string | null { 224 + return currentProfileId; 225 + } 226 + 227 + /** 228 + * Check if session partitioning is initialized 229 + */ 230 + export function isPartitionInitialized(): boolean { 231 + return currentProfileId !== null && profileSession !== null; 232 + } 233 + 234 + /** 235 + * Get partition info for debugging/status 236 + */ 237 + export function getPartitionInfo(): { 238 + profileId: string | null; 239 + partitionString: string | null; 240 + initialized: boolean; 241 + } { 242 + return { 243 + profileId: currentProfileId, 244 + partitionString: currentProfileId ? getPartitionString(currentProfileId) : null, 245 + initialized: isPartitionInitialized(), 246 + }; 247 + }
+9
preload.js
··· 772 772 */ 773 773 getSyncConfig: (profileId) => { 774 774 return ipcRenderer.invoke('profiles:getSyncConfig', { profileId }); 775 + }, 776 + 777 + /** 778 + * Get the partition string for the current profile's session 779 + * Used for setting webview partition attribute for session isolation 780 + * @returns {Promise<{success: boolean, data?: {profileId: string, partition: string}, error?: string}>} 781 + */ 782 + getPartition: () => { 783 + return ipcRenderer.invoke('profiles:getPartition'); 775 784 } 776 785 }; 777 786