let vault = null; let isInitialized = false; let currentSession = null; let keepAliveInterval = null; let sessionTimeoutDuration = 15; // Default 15 seconds let sessionTimeoutId = null; // Background timer let popupPort = null; // Track popup connection // SigSocket service instance let sigSocketService = null; // Utility function to convert Uint8Array to hex function toHex(uint8Array) { return Array.from(uint8Array) .map(b => b.toString(16).padStart(2, '0')) .join(''); } // Background session timeout management async function loadTimeoutSetting() { const result = await chrome.storage.local.get(['sessionTimeout']); sessionTimeoutDuration = result.sessionTimeout || 15; } function startSessionTimeout() { clearSessionTimeout(); if (currentSession && sessionTimeoutDuration > 0) { sessionTimeoutId = setTimeout(async () => { if (vault && currentSession) { // Lock the session vault.lock_session(); // Keep the session info for SigSocket connection but mark it as timed out const keyspace = currentSession.keyspace; await sessionManager.clear(); // Maintain SigSocket connection for the locked keyspace to receive pending requests if (sigSocketService && keyspace) { try { // Keep SigSocket connected to receive requests even when locked console.log(`🔒 Session timed out but maintaining SigSocket connection for: ${keyspace}`); } catch (error) { console.warn('Failed to maintain SigSocket connection after timeout:', error); } } // Notify popup if it's open if (popupPort) { popupPort.postMessage({ type: 'sessionTimeout', message: 'Session timed out due to inactivity' }); } } }, sessionTimeoutDuration * 1000); } } function clearSessionTimeout() { if (sessionTimeoutId) { clearTimeout(sessionTimeoutId); sessionTimeoutId = null; } } function resetSessionTimeout() { if (currentSession) { startSessionTimeout(); } } // Session persistence functions async function saveSession(keyspace) { currentSession = { keyspace, timestamp: Date.now() }; // Save to both session and local storage for better persistence await chrome.storage.session.set({ cryptoVaultSession: currentSession }); await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession }); } async function loadSession() { // Try session storage first let result = await chrome.storage.session.get(['cryptoVaultSession']); if (result.cryptoVaultSession) { currentSession = result.cryptoVaultSession; return currentSession; } // Fallback to local storage result = await chrome.storage.local.get(['cryptoVaultSessionBackup']); if (result.cryptoVaultSessionBackup) { currentSession = result.cryptoVaultSessionBackup; // Restore to session storage await chrome.storage.session.set({ cryptoVaultSession: currentSession }); return currentSession; } return null; } async function clearSession() { currentSession = null; await chrome.storage.session.remove(['cryptoVaultSession']); await chrome.storage.local.remove(['cryptoVaultSessionBackup']); } // Keep service worker alive function startKeepAlive() { if (keepAliveInterval) { clearInterval(keepAliveInterval); } keepAliveInterval = setInterval(() => { chrome.storage.session.get(['keepAlive']).catch(() => {}); }, 20000); } function stopKeepAlive() { if (keepAliveInterval) { clearInterval(keepAliveInterval); keepAliveInterval = null; } } // Consolidated session management const sessionManager = { async save(keyspace) { await saveSession(keyspace); startKeepAlive(); await loadTimeoutSetting(); startSessionTimeout(); }, async clear() { await clearSession(); stopKeepAlive(); clearSessionTimeout(); } }; async function restoreSession() { const session = await loadSession(); if (session && vault) { // Check if the session is still valid by testing if vault is unlocked const isUnlocked = vault.is_unlocked(); if (isUnlocked) { // Restart keep-alive for restored session startKeepAlive(); // Connect to SigSocket for the restored session if (sigSocketService) { try { const connected = await sigSocketService.connectToServer(session.keyspace); if (connected) { console.log(`🔗 SigSocket reconnected for restored workspace: ${session.keyspace}`); } } catch (error) { // Don't show as warning if it's just "no workspace" - this is expected on fresh start if (error.message && error.message.includes('Workspace not found')) { console.log(`â„šī¸ SigSocket connection skipped for restored session: No workspace available yet`); } else { console.warn('Failed to reconnect SigSocket for restored session:', error); } } } return session; } else { // Session exists but is locked - still try to connect SigSocket to receive pending requests if (sigSocketService && session.keyspace) { try { const connected = await sigSocketService.connectToServer(session.keyspace); if (connected) { console.log(`🔗 SigSocket connected for locked workspace: ${session.keyspace} (will queue requests)`); } } catch (error) { // Don't show as warning if it's just "no workspace" - this is expected on fresh start if (error.message && error.message.includes('Workspace not found')) { console.log(`â„šī¸ SigSocket connection skipped for locked session: No workspace available yet`); } else { console.warn('Failed to connect SigSocket for locked session:', error); } } } // Don't clear the session - keep it for SigSocket connection // await sessionManager.clear(); } } return session; // Return session even if locked, so we know which keyspace to use } // Import WASM module functions and SigSocket service import init, * as wasmFunctions from './wasm/wasm_app.js'; import SigSocketService from './background/sigsocket.js'; // Initialize WASM module async function initVault() { try { if (vault && isInitialized) return vault; // Initialize with the WASM file const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm'); await init(wasmUrl); // Use imported functions directly vault = wasmFunctions; isInitialized = true; // Initialize SigSocket service if (!sigSocketService) { sigSocketService = new SigSocketService(); await sigSocketService.initialize(vault); console.log('🔌 SigSocket service initialized'); } // Try to restore previous session await restoreSession(); return vault; } catch (error) { console.error('Failed to initialize CryptoVault:', error); throw error; } } // Consolidated message handlers const messageHandlers = { createKeyspace: async (request) => { await vault.create_keyspace(request.keyspace, request.password); return { success: true }; }, initSession: async (request) => { await vault.init_session(request.keyspace, request.password); await sessionManager.save(request.keyspace); // Smart auto-connect to SigSocket when session is initialized if (sigSocketService) { try { console.log(`🔗 Initializing SigSocket connection for workspace: ${request.keyspace}`); // This will reuse existing connection if same workspace, or switch if different const connected = await sigSocketService.connectToServer(request.keyspace); if (connected) { console.log(`✅ SigSocket ready for workspace: ${request.keyspace}`); } else { console.warn(`âš ī¸ SigSocket connection failed for workspace: ${request.keyspace}`); } } catch (error) { console.warn('Failed to auto-connect to SigSocket:', error); // If connection fails, try once more after a short delay setTimeout(async () => { try { console.log(`🔄 Retrying SigSocket connection for workspace: ${request.keyspace}`); await sigSocketService.connectToServer(request.keyspace); } catch (retryError) { console.warn('SigSocket retry connection also failed:', retryError); } }, 2000); } // Notify SigSocket service that keyspace is now unlocked await sigSocketService.onKeypaceUnlocked(); } return { success: true }; }, isUnlocked: () => ({ success: true, unlocked: vault.is_unlocked() }), addKeypair: async (request) => { const result = await vault.add_keypair(request.keyType, request.metadata); return { success: true, result }; }, listKeypairs: async () => { if (!vault.is_unlocked()) { return { success: false, error: 'Session is not unlocked' }; } const keypairsRaw = await vault.list_keypairs(); const keypairs = typeof keypairsRaw === 'string' ? JSON.parse(keypairsRaw) : keypairsRaw; return { success: true, keypairs }; }, selectKeypair: (request) => { vault.select_keypair(request.keyId); return { success: true }; }, getCurrentKeypairMetadata: () => ({ success: true, metadata: vault.current_keypair_metadata() }), getCurrentKeypairPublicKey: () => ({ success: true, publicKey: toHex(vault.current_keypair_public_key()) }), sign: async (request) => { const signature = await vault.sign(new Uint8Array(request.message)); return { success: true, signature }; }, encrypt: async (request) => { if (!vault.is_unlocked()) { return { success: false, error: 'Session is not unlocked' }; } const messageBytes = new TextEncoder().encode(request.message); const encryptedData = await vault.encrypt_data(messageBytes); const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData))); return { success: true, encryptedMessage }; }, decrypt: async (request) => { if (!vault.is_unlocked()) { return { success: false, error: 'Session is not unlocked' }; } const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0))); const decryptedData = await vault.decrypt_data(encryptedBytes); const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData)); return { success: true, decryptedMessage }; }, verify: async (request) => { const metadata = vault.current_keypair_metadata(); if (!metadata) { return { success: false, error: 'No keypair selected' }; } const isValid = await vault.verify(new Uint8Array(request.message), request.signature); return { success: true, isValid }; }, lockSession: async () => { vault.lock_session(); await sessionManager.clear(); return { success: true }; }, getStatus: async () => { const status = vault ? vault.is_unlocked() : false; const session = await loadSession(); return { success: true, status, session: session ? { keyspace: session.keyspace } : null }; }, // Timeout management handlers resetTimeout: async () => { resetSessionTimeout(); return { success: true }; }, updateTimeout: async (request) => { sessionTimeoutDuration = request.timeout; await chrome.storage.local.set({ sessionTimeout: request.timeout }); resetSessionTimeout(); // Restart with new duration return { success: true }; }, updateSigSocketUrl: async (request) => { if (sigSocketService) { // Update the server URL in the SigSocket service sigSocketService.defaultServerUrl = request.serverUrl; // Save to storage (already done in popup, but ensure consistency) await chrome.storage.local.set({ sigSocketUrl: request.serverUrl }); console.log(`🔗 SigSocket server URL updated to: ${request.serverUrl}`); } return { success: true }; }, // SigSocket handlers connectSigSocket: async (request) => { if (!sigSocketService) { return { success: false, error: 'SigSocket service not initialized' }; } const connected = await sigSocketService.connectToServer(request.workspace); return { success: connected }; }, disconnectSigSocket: async () => { if (!sigSocketService) { return { success: false, error: 'SigSocket service not initialized' }; } await sigSocketService.disconnect(); return { success: true }; }, getSigSocketStatus: async () => { if (!sigSocketService) { return { success: false, error: 'SigSocket service not initialized' }; } const status = await sigSocketService.getStatus(); return { success: true, status }; }, getSigSocketStatusWithTest: async () => { if (!sigSocketService) { return { success: false, error: 'SigSocket service not initialized' }; } // Use the enhanced connection testing method const status = await sigSocketService.getStatusWithConnectionTest(); return { success: true, status }; }, getPendingSignRequests: async () => { if (!sigSocketService) { return { success: false, error: 'SigSocket service not initialized' }; } try { // Use WASM filtered requests which handles workspace filtering const requests = await sigSocketService.getFilteredRequests(); return { success: true, requests }; } catch (error) { console.error('Failed to get pending requests:', error); return { success: false, error: error.message }; } }, approveSignRequest: async (request) => { if (!sigSocketService) { return { success: false, error: 'SigSocket service not initialized' }; } const approved = await sigSocketService.approveSignRequest(request.requestId); return { success: approved }; }, rejectSignRequest: async (request) => { if (!sigSocketService) { return { success: false, error: 'SigSocket service not initialized' }; } const rejected = await sigSocketService.rejectSignRequest(request.requestId, request.reason); return { success: rejected }; } }; // Handle messages from popup and content scripts chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { const handleRequest = async () => { try { if (!vault) { await initVault(); } const handler = messageHandlers[request.action]; if (handler) { return await handler(request); } else { throw new Error('Unknown action: ' + request.action); } } catch (error) { return { success: false, error: error.message }; } }; handleRequest().then(sendResponse); return true; // Keep the message channel open for async response }); // Initialize vault when extension starts chrome.runtime.onStartup.addListener(() => { initVault(); }); chrome.runtime.onInstalled.addListener(() => { initVault(); }); // Handle popup connection for keep-alive and timeout notifications chrome.runtime.onConnect.addListener((port) => { if (port.name === 'popup') { // Track popup connection popupPort = port; // Connect SigSocket service to popup if (sigSocketService) { sigSocketService.setPopupPort(port); } // If we have an active session, ensure keep-alive is running if (currentSession) { startKeepAlive(); } // Handle messages from popup port.onMessage.addListener(async (message) => { if (message.type === 'REQUEST_IMMEDIATE_STATUS') { // Immediately send current SigSocket status to popup if (sigSocketService) { try { const status = await sigSocketService.getStatus(); port.postMessage({ type: 'CONNECTION_STATUS_CHANGED', status: status }); console.log('📡 Sent immediate status to popup:', status.isConnected ? 'Connected' : 'Disconnected'); } catch (error) { console.warn('Failed to send immediate status:', error); } } } }); port.onDisconnect.addListener(() => { // Popup closed, clear reference and stop keep-alive popupPort = null; stopKeepAlive(); // Disconnect SigSocket service from popup if (sigSocketService) { sigSocketService.setPopupPort(null); } }); } }); // Handle notification clicks to open extension (notifications are now clickable without buttons) chrome.notifications.onClicked.addListener(async (notificationId) => { console.log(`🔔 Notification clicked: ${notificationId}`); // Check if this is a SigSocket notification if (notificationId.startsWith('sigsocket-request-')) { console.log('🔔 SigSocket notification clicked, opening extension...'); try { await openExtensionPopup(); // Clear the notification after successfully opening chrome.notifications.clear(notificationId); console.log('✅ Notification cleared after opening extension'); } catch (error) { console.error('❌ Failed to handle notification click:', error); } } else { console.log('🔔 Non-SigSocket notification clicked, ignoring'); } }); // Note: Notification button handler removed - notifications are now clickable without buttons // Function to open extension popup with best UX async function openExtensionPopup() { try { console.log('🔔 Opening extension popup from notification...'); // First, check if there's already a popup window open const windows = await chrome.windows.getAll({ populate: true }); const existingPopup = windows.find(window => window.type === 'popup' && window.tabs?.some(tab => tab.url?.includes('popup.html')) ); if (existingPopup) { // Focus existing popup and send focus message await chrome.windows.update(existingPopup.id, { focused: true }); console.log('✅ Focused existing popup window'); // Send message to focus on SigSocket section if (popupPort) { popupPort.postMessage({ type: 'FOCUS_SIGSOCKET', fromNotification: true }); } return; } // Best UX: Try to use the normal popup experience // The action API gives the same popup as clicking the extension icon try { if (chrome.action && chrome.action.openPopup) { await chrome.action.openPopup(); console.log('✅ Extension popup opened via action API (best UX - normal popup)'); // Send focus message after popup opens setTimeout(() => { if (popupPort) { popupPort.postMessage({ type: 'FOCUS_SIGSOCKET', fromNotification: true }); } }, 200); return; } } catch (actionError) { // The action API fails when there's no active browser window // This is common when all browser windows are closed but extension is still running console.log('âš ī¸ Action API failed (likely no active window):', actionError.message); // Check if we have any normal browser windows const allWindows = await chrome.windows.getAll(); const normalWindows = allWindows.filter(w => w.type === 'normal'); if (normalWindows.length > 0) { // We have browser windows, try to focus one and retry action API try { const targetWindow = normalWindows.find(w => w.focused) || normalWindows[0]; await chrome.windows.update(targetWindow.id, { focused: true }); // Small delay and retry await new Promise(resolve => setTimeout(resolve, 100)); await chrome.action.openPopup(); console.log('✅ Extension popup opened via action API after focusing window'); setTimeout(() => { if (popupPort) { popupPort.postMessage({ type: 'FOCUS_SIGSOCKET', fromNotification: true }); } }, 200); return; } catch (retryError) { console.log('âš ī¸ Action API retry also failed:', retryError.message); } } } // If action API fails completely, we need to create a window // But let's make it as close to the normal popup experience as possible console.log('âš ī¸ Creating popup window as fallback (action API unavailable)'); const popupUrl = chrome.runtime.getURL('popup.html?from=notification'); // Position the popup where the extension icon would normally show its popup // Try to position it in the top-right area like a normal extension popup let left = screen.width - 420; // 400px width + 20px margin let top = 80; // Below browser toolbar area try { // If we have a browser window, position relative to it const allWindows = await chrome.windows.getAll(); const normalWindows = allWindows.filter(w => w.type === 'normal'); if (normalWindows.length > 0) { const referenceWindow = normalWindows[0]; left = (referenceWindow.left || 0) + (referenceWindow.width || 800) - 420; top = (referenceWindow.top || 0) + 80; } } catch (positionError) { console.log('âš ī¸ Could not get window position, using screen-based positioning'); } const newWindow = await chrome.windows.create({ url: popupUrl, type: 'popup', width: 400, height: 600, left: Math.max(0, left), top: Math.max(0, top), focused: true }); console.log(`✅ Extension popup window created: ${newWindow.id}`); } catch (error) { console.error('❌ Failed to open extension popup:', error); // Final fallback: open in new tab (least ideal but still functional) try { const popupUrl = chrome.runtime.getURL('popup.html?from=notification'); await chrome.tabs.create({ url: popupUrl, active: true }); console.log('✅ Opened extension in new tab as final fallback'); } catch (tabError) { console.error('❌ All popup opening methods failed:', tabError); } } }