// Consolidated toast system function showToast(message, type = 'info') { document.querySelector('.toast-notification')?.remove(); const icons = { success: '', error: '', info: '' }; const toast = Object.assign(document.createElement('div'), { className: `toast-notification toast-${type}`, innerHTML: `
${icons[type] || icons.info}
${message}
` }); document.body.appendChild(toast); setTimeout(() => toast.classList.add('toast-show'), 10); setTimeout(() => { if (toast.parentElement) { toast.classList.add('toast-hide'); setTimeout(() => toast.remove(), 300); } }, 4000); } // Enhanced loading states for buttons function setButtonLoading(button, loading = true) { // Handle null/undefined button gracefully if (!button) { return; } if (loading) { button.dataset.originalText = button.textContent; button.classList.add('loading'); button.disabled = true; } else { button.classList.remove('loading'); button.disabled = false; if (button.dataset.originalText) { button.textContent = button.dataset.originalText; } } } // Enhanced response error handling function getResponseError(response, operation = 'operation') { if (!response) { return `Failed to ${operation}: No response received`; } if (response.success === false || response.error) { const errorMsg = getErrorMessage(response.error, `${operation} failed`); // Handle specific error types if (errorMsg.includes('decryption error') || errorMsg.includes('aead::Error')) { return 'Invalid password or corrupted keyspace data'; } if (errorMsg.includes('Crypto error')) { return 'Keyspace not found or corrupted. Try creating a new one.'; } if (errorMsg.includes('not unlocked') || errorMsg.includes('session')) { return 'Session expired. Please login again.'; } return errorMsg; } return `Failed to ${operation}: Unknown error`; } function showSection(sectionId) { document.querySelectorAll('.section').forEach(s => s.classList.add('hidden')); document.getElementById(sectionId).classList.remove('hidden'); } function setStatus(text, isConnected = false) { const statusText = document.getElementById('statusText'); const statusSection = document.getElementById('vaultStatus'); const lockBtn = document.getElementById('lockBtn'); if (isConnected && text) { // Show keyspace name and status section statusText.textContent = text; statusSection.classList.remove('hidden'); if (lockBtn) { lockBtn.classList.remove('hidden'); } } } // Message handling async function sendMessage(action, data = {}) { return new Promise((resolve) => { chrome.runtime.sendMessage({ action, ...data }, resolve); }); } // Copy to clipboard async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); showToast('Copied to clipboard!', 'success'); } catch (err) { showToast('Failed to copy', 'error'); } } // Convert string to Uint8Array function stringToUint8Array(str) { return Array.from(new TextEncoder().encode(str)); } // DOM Elements const elements = { // Authentication elements keyspaceInput: document.getElementById('keyspaceInput'), passwordInput: document.getElementById('passwordInput'), createKeyspaceBtn: document.getElementById('createKeyspaceBtn'), loginBtn: document.getElementById('loginBtn'), // Header elements lockBtn: document.getElementById('lockBtn'), themeToggle: document.getElementById('themeToggle'), settingsBtn: document.getElementById('settingsBtn'), headerTitle: document.getElementById('headerTitle'), // Section elements authSection: document.getElementById('authSection'), vaultSection: document.getElementById('vaultSection'), settingsSection: document.getElementById('settingsSection'), // Settings page elements timeoutInput: document.getElementById('timeoutInput'), serverUrlInput: document.getElementById('serverUrlInput'), saveServerUrlBtn: document.getElementById('saveServerUrlBtn'), // Keypair management elements toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'), addKeypairCard: document.getElementById('addKeypairCard'), keyTypeSelect: document.getElementById('keyTypeSelect'), keyNameInput: document.getElementById('keyNameInput'), addKeypairBtn: document.getElementById('addKeypairBtn'), cancelAddKeypairBtn: document.getElementById('cancelAddKeypairBtn'), keypairsList: document.getElementById('keypairsList'), // Crypto operation elements - Sign tab messageInput: document.getElementById('messageInput'), signBtn: document.getElementById('signBtn'), signatureResult: document.getElementById('signatureResult'), copySignatureBtn: document.getElementById('copySignatureBtn'), // Crypto operation elements - Encrypt tab encryptMessageInput: document.getElementById('encryptMessageInput'), encryptBtn: document.getElementById('encryptBtn'), encryptResult: document.getElementById('encryptResult'), // Crypto operation elements - Decrypt tab encryptedMessageInput: document.getElementById('encryptedMessageInput'), decryptBtn: document.getElementById('decryptBtn'), decryptResult: document.getElementById('decryptResult'), // Crypto operation elements - Verify tab verifyMessageInput: document.getElementById('verifyMessageInput'), signatureToVerifyInput: document.getElementById('signatureToVerifyInput'), verifyBtn: document.getElementById('verifyBtn'), verifyResult: document.getElementById('verifyResult'), }; // Global state variables let currentKeyspace = null; let selectedKeypairId = null; let backgroundPort = null; let sessionTimeoutDuration = 15; // Default 15 seconds // Session timeout management function handleError(error, context, shouldShowToast = true) { const errorMessage = error?.message || 'An unexpected error occurred'; if (shouldShowToast) { showToast(`${context}: ${errorMessage}`, 'error'); } } function validateInput(value, fieldName, options = {}) { const { minLength = 1, maxLength = 1000, required = true } = options; if (required && (!value || !value.trim())) { showToast(`${fieldName} is required`, 'error'); return false; } if (value && value.length < minLength) { showToast(`${fieldName} must be at least ${minLength} characters`, 'error'); return false; } if (value && value.length > maxLength) { showToast(`${fieldName} must be less than ${maxLength} characters`, 'error'); return false; } return true; } async function loadTimeoutSetting() { const result = await chrome.storage.local.get(['sessionTimeout']); sessionTimeoutDuration = result.sessionTimeout || 15; if (elements.timeoutInput) { elements.timeoutInput.value = sessionTimeoutDuration; } } async function checkSessionTimeout() { const result = await chrome.storage.local.get(['sessionTimedOut']); if (result.sessionTimedOut) { // Clear the flag await chrome.storage.local.remove(['sessionTimedOut']); // Show timeout notification showToast('Session timed out due to inactivity', 'info'); } } async function saveTimeoutSetting(timeout) { sessionTimeoutDuration = timeout; await sendMessage('updateTimeout', { timeout }); } // Server URL settings async function loadServerUrlSetting() { try { const result = await chrome.storage.local.get(['sigSocketUrl']); const serverUrl = result.sigSocketUrl || 'ws://localhost:8080/ws'; if (elements.serverUrlInput) { elements.serverUrlInput.value = serverUrl; } } catch (error) { console.warn('Failed to load server URL setting:', error); } } async function saveServerUrlSetting() { try { const serverUrl = elements.serverUrlInput?.value?.trim(); if (!serverUrl) { showToast('Please enter a valid server URL', 'error'); return; } // Basic URL validation if (!serverUrl.startsWith('ws://') && !serverUrl.startsWith('wss://')) { showToast('Server URL must start with ws:// or wss://', 'error'); return; } // Save to storage await chrome.storage.local.set({ sigSocketUrl: serverUrl }); // Notify background script to update server URL const response = await sendMessage('updateSigSocketUrl', { serverUrl }); if (response?.success) { showToast('Server URL saved successfully', 'success'); // Refresh connection status await loadSigSocketState(); } else { showToast('Failed to update server URL', 'error'); } } catch (error) { console.error('Failed to save server URL:', error); showToast('Failed to save server URL', 'error'); } } async function resetSessionTimeout() { if (currentKeyspace) { await sendMessage('resetTimeout'); } } // Theme management function initializeTheme() { const savedTheme = localStorage.getItem('cryptovault-theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); updateThemeIcon(savedTheme); } function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('cryptovault-theme', newTheme); updateThemeIcon(newTheme); } // Settings page navigation async function showSettingsPage() { // Hide all sections document.querySelectorAll('.section').forEach(section => { section.classList.add('hidden'); }); // Show settings section elements.settingsSection?.classList.remove('hidden'); // Ensure we have current status before updating settings display await loadSigSocketState(); } async function hideSettingsPage() { // Hide settings section elements.settingsSection?.classList.add('hidden'); // Check current session state to determine what to show try { const response = await sendMessage('getStatus'); if (response && response.success && response.status && response.session) { // Active session exists - show vault section currentKeyspace = response.session.keyspace; if (elements.keyspaceInput) { elements.keyspaceInput.value = currentKeyspace; } setStatus(currentKeyspace, true); elements.vaultSection?.classList.remove('hidden'); updateSettingsVisibility(); // Update settings visibility // Load vault content await loadKeypairs(); // Use retry mechanism for existing sessions that might have stale connections await loadSigSocketStateWithRetry(); } else { // No active session - show auth section currentKeyspace = null; setStatus('', false); elements.authSection?.classList.remove('hidden'); updateSettingsVisibility(); // Update settings visibility // For no session, use regular loading await loadSigSocketState(); } } catch (error) { console.warn('Failed to check session state:', error); // Fallback to auth section on error currentKeyspace = null; setStatus('', false); elements.authSection?.classList.remove('hidden'); updateSettingsVisibility(); // Update settings visibility // Still try to load SigSocket state await loadSigSocketState(); } } function closeSettingsDropdown() { const dropdown = elements.settingsDropdown; if (dropdown) { dropdown.classList.add('hidden'); } } function updateThemeIcon(theme) { const themeToggle = elements.themeToggle; if (!themeToggle) return; if (theme === 'dark') { // Bright sun SVG for dark theme themeToggle.innerHTML = ` `; themeToggle.title = 'Switch to light mode'; } else { // Dark crescent moon SVG for light theme themeToggle.innerHTML = ` `; themeToggle.title = 'Switch to dark mode'; } } // Update settings button visibility based on keyspace state function updateSettingsVisibility() { if (elements.settingsBtn) { if (currentKeyspace) { // Show settings when keyspace is unlocked elements.settingsBtn.style.display = ''; } else { // Hide settings when keyspace is locked elements.settingsBtn.style.display = 'none'; } } } // Establish connection to background script for keep-alive function connectToBackground() { backgroundPort = chrome.runtime.connect({ name: 'popup' }); // Listen for messages from background script backgroundPort.onMessage.addListener((message) => { if (message.type === 'sessionTimeout') { // Update UI state to reflect locked session currentKeyspace = null; selectedKeypairId = null; setStatus('', false); showSection('authSection'); updateSettingsVisibility(); // Update settings visibility clearVaultState(); // Clear form inputs if (elements.keyspaceInput) elements.keyspaceInput.value = ''; if (elements.passwordInput) elements.passwordInput.value = ''; // Show timeout notification showToast(message.message, 'info'); } }); backgroundPort.onDisconnect.addListener(() => { backgroundPort = null; }); // Immediately request status update when popup connects setTimeout(() => { if (backgroundPort) { backgroundPort.postMessage({ type: 'REQUEST_IMMEDIATE_STATUS' }); } }, 50); // Small delay to ensure connection is established } // Initialize document.addEventListener('DOMContentLoaded', async function() { // Initialize theme first initializeTheme(); // Load timeout setting await loadTimeoutSetting(); // Load server URL setting await loadServerUrlSetting(); // Ensure lock button starts hidden const lockBtn = document.getElementById('lockBtn'); if (lockBtn) { lockBtn.classList.add('hidden'); } // Connect to background script for keep-alive connectToBackground(); // Consolidated event listeners const eventMap = { createKeyspaceBtn: createKeyspace, loginBtn: login, lockBtn: lockSession, themeToggle: toggleTheme, settingsBtn: showSettingsPage, headerTitle: hideSettingsPage, saveServerUrlBtn: saveServerUrlSetting, toggleAddKeypairBtn: toggleAddKeypairForm, addKeypairBtn: addKeypair, cancelAddKeypairBtn: hideAddKeypairForm, signBtn: signMessage, encryptBtn: encryptMessage, decryptBtn: decryptMessage, verifyBtn: verifySignature }; Object.entries(eventMap).forEach(([elementKey, handler]) => { const element = elements[elementKey]; if (element) { element.addEventListener('click', handler); } }); // Tab functionality initializeTabs(); // Additional event listeners elements.copySignatureBtn?.addEventListener('click', () => { copyToClipboard(document.getElementById('signatureValue')?.textContent); }); elements.messageInput?.addEventListener('input', () => { if (elements.signBtn) { elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId; } }); // Timeout setting event listener elements.timeoutInput?.addEventListener('change', async (e) => { const timeout = parseInt(e.target.value); if (timeout >= 3 && timeout <= 300) { await saveTimeoutSetting(timeout); } else { e.target.value = sessionTimeoutDuration; // Reset to current value if invalid } }); // Activity detection - reset timeout on any interaction document.addEventListener('click', (e) => { resetSessionTimeout(); // Close settings dropdown if clicking outside if (!elements.settingsToggle?.contains(e.target) && !elements.settingsDropdown?.contains(e.target)) { closeSettingsDropdown(); } }); document.addEventListener('keydown', resetSessionTimeout); document.addEventListener('input', resetSessionTimeout); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !elements.addKeypairCard?.classList.contains('hidden')) { hideAddKeypairForm(); } if (e.key === 'Enter' && e.target === elements.keyNameInput && elements.keyNameInput.value.trim()) { e.preventDefault(); addKeypair(); } }); // Initialize SigSocket UI elements after DOM is ready sigSocketElements = { connectionStatus: document.getElementById('connectionStatus'), connectionDot: document.getElementById('connectionDot'), connectionText: document.getElementById('connectionText'), requestsContainer: document.getElementById('requestsContainer'), loadingRequestsMessage: document.getElementById('loadingRequestsMessage'), noRequestsMessage: document.getElementById('noRequestsMessage'), requestsList: document.getElementById('requestsList'), refreshRequestsBtn: document.getElementById('refreshRequestsBtn') }; // Add SigSocket button listeners sigSocketElements.refreshRequestsBtn?.addEventListener('click', refreshSigSocketRequests); // Check if opened via notification (focus on SigSocket section) const urlParams = new URLSearchParams(window.location.search); const fromNotification = urlParams.get('from') === 'notification'; // Check for existing session await checkExistingSession(); // If opened from notification, focus on SigSocket section and show requests if (fromNotification) { console.log('🔔 Opened from notification, focusing on SigSocket section'); focusOnSigSocketSection(); } // Try to load any cached SigSocket state immediately for better UX await loadCachedSigSocketState(); }); // Focus on SigSocket section when opened from notification function focusOnSigSocketSection() { try { // Switch to SigSocket tab if not already active const sigSocketTab = document.querySelector('[data-tab="sigsocket"]'); if (sigSocketTab && !sigSocketTab.classList.contains('active')) { sigSocketTab.click(); } // Scroll to requests container if (sigSocketElements.requestsContainer) { sigSocketElements.requestsContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // Show a helpful toast showToast('New signature request received! Review pending requests below.', 'info'); // Refresh requests to ensure latest state setTimeout(() => refreshSigSocketRequests(), 500); } catch (error) { console.error('Failed to focus on SigSocket section:', error); } } async function checkExistingSession() { try { const response = await sendMessage('getStatus'); if (response && response.success && response.status && response.session) { // Session is active currentKeyspace = response.session.keyspace; elements.keyspaceInput.value = currentKeyspace; setStatus(currentKeyspace, true); showSection('vaultSection'); updateSettingsVisibility(); // Update settings visibility await loadKeypairs(); // Use retry mechanism for existing sessions to handle stale connections await loadSigSocketStateWithRetry(); } else { // No active session currentKeyspace = null; setStatus('', false); showSection('authSection'); updateSettingsVisibility(); // Update settings visibility // For no session, use regular loading (no retry needed) await loadSigSocketState(); } } catch (error) { setStatus('', false); showSection('authSection'); // Still try to load SigSocket state even on error try { await loadSigSocketState(); } catch (sigSocketError) { console.warn('Failed to load SigSocket state:', sigSocketError); } } } // Toggle add keypair form function toggleAddKeypairForm() { const isHidden = elements.addKeypairCard.classList.contains('hidden'); if (isHidden) { showAddKeypairForm(); } else { hideAddKeypairForm(); } } function showAddKeypairForm() { elements.addKeypairCard.classList.remove('hidden'); elements.keyNameInput.focus(); } function hideAddKeypairForm() { elements.addKeypairCard.classList.add('hidden'); // Clear the form elements.keyNameInput.value = ''; elements.keyTypeSelect.selectedIndex = 0; } // Tab functionality function initializeTabs() { const tabContainer = document.querySelector('.operation-tabs'); if (tabContainer) { // Use event delegation for better performance tabContainer.addEventListener('click', (e) => { if (e.target.classList.contains('tab-btn')) { handleTabSwitch(e.target); } }); } // Initialize input validation initializeInputValidation(); } function handleTabSwitch(clickedTab) { const targetTab = clickedTab.getAttribute('data-tab'); const tabButtons = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); // Remove active class from all tabs and contents tabButtons.forEach(btn => btn.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active')); // Add active class to clicked tab and corresponding content clickedTab.classList.add('active'); const targetContent = document.getElementById(`${targetTab}-tab`); if (targetContent) { targetContent.classList.add('active'); } // Clear results when switching tabs clearTabResults(); // Update button states updateButtonStates(); } function clearTabResults() { // Hide all result sections (with null checks) if (elements.signatureResult) { elements.signatureResult.classList.add('hidden'); elements.signatureResult.innerHTML = ''; } if (elements.encryptResult) { elements.encryptResult.classList.add('hidden'); elements.encryptResult.innerHTML = ''; } if (elements.decryptResult) { elements.decryptResult.classList.add('hidden'); elements.decryptResult.innerHTML = ''; } if (elements.verifyResult) { elements.verifyResult.classList.add('hidden'); elements.verifyResult.innerHTML = ''; } } function initializeInputValidation() { // Sign tab validation (with null checks) if (elements.messageInput) { elements.messageInput.addEventListener('input', updateButtonStates); } // Encrypt tab validation (with null checks) if (elements.encryptMessageInput) { elements.encryptMessageInput.addEventListener('input', updateButtonStates); } // Decrypt tab validation (with null checks) if (elements.encryptedMessageInput) { elements.encryptedMessageInput.addEventListener('input', updateButtonStates); } // Verify tab validation (with null checks) if (elements.verifyMessageInput) { elements.verifyMessageInput.addEventListener('input', updateButtonStates); } if (elements.signatureToVerifyInput) { elements.signatureToVerifyInput.addEventListener('input', updateButtonStates); } } function updateButtonStates() { // Sign button (with null checks) if (elements.signBtn && elements.messageInput) { elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId; } // Encrypt button (with null checks) - only needs message and keyspace session if (elements.encryptBtn && elements.encryptMessageInput) { elements.encryptBtn.disabled = !elements.encryptMessageInput.value.trim() || !currentKeyspace; } // Decrypt button (with null checks) - only needs encrypted message and keyspace session if (elements.decryptBtn && elements.encryptedMessageInput) { elements.decryptBtn.disabled = !elements.encryptedMessageInput.value.trim() || !currentKeyspace; } // Verify button (with null checks) - only needs message and signature if (elements.verifyBtn && elements.verifyMessageInput && elements.signatureToVerifyInput) { elements.verifyBtn.disabled = !elements.verifyMessageInput.value.trim() || !elements.signatureToVerifyInput.value.trim() || !selectedKeypairId; } } // Clear all vault-related state and UI function clearVaultState() { // Clear all crypto operation inputs (with null checks) if (elements.messageInput) elements.messageInput.value = ''; if (elements.encryptMessageInput) elements.encryptMessageInput.value = ''; if (elements.encryptedMessageInput) elements.encryptedMessageInput.value = ''; if (elements.verifyMessageInput) elements.verifyMessageInput.value = ''; if (elements.signatureToVerifyInput) elements.signatureToVerifyInput.value = ''; // Clear all result sections clearTabResults(); // Clear signature value with null check const signatureValue = document.getElementById('signatureValue'); if (signatureValue) signatureValue.textContent = ''; // Clear selected keypair state selectedKeypairId = null; updateButtonStates(); // Hide add keypair form if open hideAddKeypairForm(); // Clear keypairs list if (elements.keypairsList) { elements.keypairsList.innerHTML = '
Loading keypairs...
'; } } // Validation utilities const validateAuth = () => { const keyspace = elements.keyspaceInput.value.trim(); const password = elements.passwordInput.value.trim(); if (!validateInput(keyspace, 'Keyspace name', { minLength: 1, maxLength: 100 })) { return null; } if (!validateInput(password, 'Password', { minLength: 1, maxLength: 1000 })) { return null; } return { keyspace, password }; }; async function createKeyspace() { const auth = validateAuth(); if (!auth) return; try { await executeOperation( async () => { const response = await sendMessage('createKeyspace', auth); if (response?.success) { clearVaultState(); await login(); // Auto-login after creation return response; } else { throw new Error(getResponseError(response, 'create keyspace')); } }, { loadingElement: elements.createKeyspaceBtn, successMessage: 'Keyspace created successfully!', maxRetries: 1 } ); } catch (error) { handleError(error, 'Create keyspace'); } } async function login() { const auth = validateAuth(); if (!auth) return; try { await executeOperation( async () => { const response = await sendMessage('initSession', auth); if (response?.success) { currentKeyspace = auth.keyspace; setStatus(auth.keyspace, true); showSection('vaultSection'); updateSettingsVisibility(); // Update settings visibility clearVaultState(); await loadKeypairs(); // Clean flow: Login -> Connect -> Restore -> Display console.log('🔓 Login successful, applying clean flow...'); // 1. Wait for SigSocket to connect and restore requests await loadSigSocketStateWithRetry(); // 2. Show loading state while fetching showRequestsLoading(); // 3. Refresh requests to get the clean, restored state await refreshSigSocketRequests(); console.log('✅ Login clean flow completed'); return response; } else { throw new Error(getResponseError(response, 'login')); } }, { loadingElement: elements.loginBtn, successMessage: 'Logged in successfully!', maxRetries: 2 } ); } catch (error) { handleError(error, 'Create keyspace'); } } async function lockSession() { try { await sendMessage('lockSession'); currentKeyspace = null; selectedKeypairId = null; setStatus('', false); showSection('authSection'); updateSettingsVisibility(); // Update settings visibility // Clear all form inputs elements.keyspaceInput.value = ''; elements.passwordInput.value = ''; clearVaultState(); showToast('Session locked', 'info'); } catch (error) { showToast('Error: ' + error.message, 'error'); } } async function addKeypair() { const keyType = elements.keyTypeSelect.value; const keyName = elements.keyNameInput.value.trim(); if (!keyName) { showToast('Please enter a name for the keypair', 'error'); return; } await executeOperation( async () => { const metadata = JSON.stringify({ name: keyName }); const response = await sendMessage('addKeypair', { keyType, metadata }); if (response?.success) { hideAddKeypairForm(); await loadKeypairs(); return response; } else { throw new Error(getResponseError(response, 'add keypair')); } }, { loadingElement: elements.addKeypairBtn, successMessage: 'Keypair added successfully!' } ); } async function loadKeypairs() { const response = await sendMessage('listKeypairs'); if (response && response.success) { renderKeypairs(response.keypairs); } else { const errorMsg = getResponseError(response, 'load keypairs'); const container = elements.keypairsList; container.innerHTML = '
Failed to load keypairs. Try refreshing.
'; showToast(errorMsg, 'error'); } } function renderKeypairs(keypairs) { const container = elements.keypairsList; // Simple array handling const keypairArray = Array.isArray(keypairs) ? keypairs : []; if (keypairArray.length === 0) { container.innerHTML = '
No keypairs found. Add one above.
'; return; } container.innerHTML = keypairArray.map((keypair) => { const metadata = typeof keypair.metadata === 'string' ? JSON.parse(keypair.metadata) : keypair.metadata; return `
${metadata.name || 'Unnamed'}
${keypair.key_type}
`; }).join(''); // Add event listeners to all keypair cards container.querySelectorAll('.keypair-item').forEach(card => { card.addEventListener('click', (e) => { const keypairId = e.currentTarget.getAttribute('data-keypair-id'); selectKeypair(keypairId); }); // Add keyboard support card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const keypairId = e.currentTarget.getAttribute('data-keypair-id'); selectKeypair(keypairId); } }); }); // Restore selection state if there was a previously selected keypair if (selectedKeypairId) { updateKeypairSelection(selectedKeypairId); } } async function selectKeypair(keyId) { // Don't show loading overlay for selection - it's too disruptive try { // Update visual state immediately for better UX updateKeypairSelection(keyId); await sendMessage('selectKeypair', { keyId }); selectedKeypairId = keyId; // Get keypair details for internal use (but don't show the card) const metadataResponse = await sendMessage('getCurrentKeypairMetadata'); const publicKeyResponse = await sendMessage('getCurrentKeypairPublicKey'); if (metadataResponse && metadataResponse.success && publicKeyResponse && publicKeyResponse.success) { // Enable sign button if message is entered updateButtonStates(); } else { // Handle metadata or public key fetch failure const metadataError = getResponseError(metadataResponse, 'get keypair metadata'); const publicKeyError = getResponseError(publicKeyResponse, 'get public key'); const errorMsg = metadataResponse && !metadataResponse.success ? metadataError : publicKeyError; updateKeypairSelection(null); showToast(errorMsg, 'error'); } } catch (error) { const errorMsg = getErrorMessage(error, 'Failed to select keypair'); // Revert visual state if there was an error updateKeypairSelection(null); showToast(errorMsg, 'error'); } } function updateKeypairSelection(selectedId) { // Remove previous selection styling from all keypair items const allKeypairs = document.querySelectorAll('.keypair-item'); allKeypairs.forEach(item => { item.classList.remove('selected'); }); // Add selection styling to the selected keypair (if any) if (selectedId) { const selectedKeypair = document.querySelector(`[data-keypair-id="${selectedId}"]`); if (selectedKeypair) { selectedKeypair.classList.add('selected'); } } } // Shared templates const copyIcon = ` `; const createResultContainer = (label, value, btnId) => `
${value}
`; // Unified crypto operation handler async function performCryptoOperation(config) { const { input, validation, action, resultElement, button, successMsg, resultProcessor } = config; if (!validation()) { showToast(config.errorMsg, 'error'); return; } await executeOperation( async () => { const response = await sendMessage(action, input()); if (response?.success) { resultElement.classList.remove('hidden'); resultElement.innerHTML = resultProcessor(response); // Add copy button listener if result has copy button const copyBtn = resultElement.querySelector('.btn-copy'); if (copyBtn && config.copyValue) { copyBtn.addEventListener('click', () => copyToClipboard(config.copyValue(response))); } return response; } else { throw new Error(getResponseError(response, action)); } }, { loadingElement: button, successMessage: successMsg } ); } // Crypto operation functions using shared templates const signMessage = () => performCryptoOperation({ validation: () => elements.messageInput.value.trim() && selectedKeypairId, errorMsg: 'Please enter a message and select a keypair', action: 'sign', input: () => ({ message: stringToUint8Array(elements.messageInput.value.trim()) }), resultElement: elements.signatureResult, button: elements.signBtn, successMsg: 'Message signed successfully!', copyValue: (response) => response.signature, resultProcessor: (response) => createResultContainer('Signature', response.signature, 'copySignatureBtn') }); const encryptMessage = () => performCryptoOperation({ validation: () => elements.encryptMessageInput.value.trim() && currentKeyspace, errorMsg: 'Please enter a message and ensure you are connected to a keyspace', action: 'encrypt', input: () => ({ message: elements.encryptMessageInput.value.trim() }), resultElement: elements.encryptResult, button: elements.encryptBtn, successMsg: 'Message encrypted successfully!', copyValue: (response) => response.encryptedMessage, resultProcessor: (response) => createResultContainer('Encrypted Message', response.encryptedMessage, 'copyEncryptedBtn') }); const decryptMessage = () => performCryptoOperation({ validation: () => elements.encryptedMessageInput.value.trim() && currentKeyspace, errorMsg: 'Please enter encrypted message and ensure you are connected to a keyspace', action: 'decrypt', input: () => ({ encryptedMessage: elements.encryptedMessageInput.value.trim() }), resultElement: elements.decryptResult, button: elements.decryptBtn, successMsg: 'Message decrypted successfully!', copyValue: (response) => response.decryptedMessage, resultProcessor: (response) => createResultContainer('Decrypted Message', response.decryptedMessage, 'copyDecryptedBtn') }); const verifySignature = () => performCryptoOperation({ validation: () => elements.verifyMessageInput.value.trim() && elements.signatureToVerifyInput.value.trim() && selectedKeypairId, errorMsg: 'Please enter message, signature, and select a keypair', action: 'verify', input: () => ({ message: stringToUint8Array(elements.verifyMessageInput.value.trim()), signature: elements.signatureToVerifyInput.value.trim() }), resultElement: elements.verifyResult, button: elements.verifyBtn, successMsg: null, resultProcessor: (response) => { const isValid = response.isValid; const icon = isValid ? ` ` : ` `; const text = isValid ? 'Signature is valid' : 'Signature is invalid'; return `
${icon} ${text}
`; } }); // SigSocket functionality let sigSocketRequests = []; let sigSocketStatus = { isConnected: false, workspace: null }; let sigSocketElements = {}; // Will be initialized in DOMContentLoaded let isInitialLoad = true; // Track if this is the first load // Listen for messages from background script about SigSocket events if (backgroundPort) { backgroundPort.onMessage.addListener((message) => { if (message.type === 'NEW_SIGN_REQUEST') { handleNewSignRequest(message); } else if (message.type === 'REQUESTS_UPDATED') { updateRequestsList(message.pendingRequests); } else if (message.type === 'KEYSPACE_UNLOCKED') { handleKeypaceUnlocked(message); } else if (message.type === 'CONNECTION_STATUS_CHANGED') { handleConnectionStatusChanged(message); } else if (message.type === 'FOCUS_SIGSOCKET') { // Handle focus request from notification click console.log('🔔 Received focus request from notification'); focusOnSigSocketSection(); } }); } // Load SigSocket state when popup opens async function loadSigSocketState() { try { console.log('🔄 Loading SigSocket state...'); // Show loading state for requests showRequestsLoading(); // Show loading state for connection status on initial load if (isInitialLoad) { showConnectionLoading(); } // Force a fresh connection status check with enhanced testing const statusResponse = await sendMessage('getSigSocketStatusWithTest'); if (statusResponse?.success) { console.log('✅ Got SigSocket status:', statusResponse.status); updateConnectionStatus(statusResponse.status); } else { console.warn('Enhanced status check failed, trying fallback...'); // Fallback to regular status check const fallbackResponse = await sendMessage('getSigSocketStatus'); if (fallbackResponse?.success) { console.log('✅ Got fallback SigSocket status:', fallbackResponse.status); updateConnectionStatus(fallbackResponse.status); } else { // If both fail, show disconnected but don't show error on initial load updateConnectionStatus({ isConnected: false, workspace: null, publicKey: null, pendingRequestCount: 0, serverUrl: 'ws://localhost:8080/ws' }); } } // Get pending requests - this now works even when keyspace is locked console.log('📋 Fetching pending requests...'); const requestsResponse = await sendMessage('getPendingSignRequests'); if (requestsResponse?.success) { console.log(`📋 Retrieved ${requestsResponse.requests?.length || 0} pending requests:`, requestsResponse.requests); updateRequestsList(requestsResponse.requests); } else { console.warn('Failed to get pending requests:', requestsResponse); updateRequestsList([]); } // Mark initial load as complete isInitialLoad = false; } catch (error) { console.warn('Failed to load SigSocket state:', error); // Hide loading state and show error state hideRequestsLoading(); // Set disconnected state on error (but don't show error toast on initial load) updateConnectionStatus({ isConnected: false, workspace: null, publicKey: null, pendingRequestCount: 0, serverUrl: 'ws://localhost:8080/ws' }); // Still try to show any cached requests updateRequestsList([]); // Mark initial load as complete isInitialLoad = false; } } // Load cached SigSocket state for immediate display async function loadCachedSigSocketState() { try { // Try to get any cached requests from storage for immediate display const cachedData = await chrome.storage.local.get(['sigSocketPendingRequests']); if (cachedData.sigSocketPendingRequests && Array.isArray(cachedData.sigSocketPendingRequests)) { console.log('📋 Loading cached requests for immediate display'); updateRequestsList(cachedData.sigSocketPendingRequests); } } catch (error) { console.warn('Failed to load cached SigSocket state:', error); } } // Load SigSocket state with simple retry for session initialization timing async function loadSigSocketStateWithRetry() { // First try immediately (might already be connected) await loadSigSocketState(); // If still showing disconnected after initial load, try again after a short delay if (!sigSocketStatus.isConnected) { console.log('🔄 Initial load showed disconnected, retrying after delay...'); await new Promise(resolve => setTimeout(resolve, 500)); await loadSigSocketState(); } } // Show loading state for connection status function showConnectionLoading() { if (sigSocketElements.connectionDot && sigSocketElements.connectionText) { sigSocketElements.connectionDot.classList.remove('connected'); sigSocketElements.connectionDot.classList.add('loading'); sigSocketElements.connectionText.textContent = 'Checking...'; } } // Hide loading state for connection status function hideConnectionLoading() { if (sigSocketElements.connectionDot) { sigSocketElements.connectionDot.classList.remove('loading'); } } // Update connection status display function updateConnectionStatus(status) { sigSocketStatus = status; // Hide loading state hideConnectionLoading(); if (sigSocketElements.connectionDot && sigSocketElements.connectionText) { if (status.isConnected) { sigSocketElements.connectionDot.classList.add('connected'); sigSocketElements.connectionText.textContent = 'Connected'; } else { sigSocketElements.connectionDot.classList.remove('connected'); sigSocketElements.connectionText.textContent = 'Disconnected'; } } // Log connection details for debugging console.log('🔗 Connection status updated:', { connected: status.isConnected, workspace: status.workspace, publicKey: status.publicKey?.substring(0, 16) + '...', serverUrl: status.serverUrl }); } // Show loading state for requests function showRequestsLoading() { if (!sigSocketElements.requestsContainer) return; sigSocketElements.loadingRequestsMessage?.classList.remove('hidden'); sigSocketElements.noRequestsMessage?.classList.add('hidden'); sigSocketElements.requestsList?.classList.add('hidden'); } // Hide loading state for requests function hideRequestsLoading() { if (!sigSocketElements.requestsContainer) return; sigSocketElements.loadingRequestsMessage?.classList.add('hidden'); } // Update requests list display function updateRequestsList(requests) { sigSocketRequests = requests || []; if (!sigSocketElements.requestsContainer) return; // Hide loading state hideRequestsLoading(); if (sigSocketRequests.length === 0) { sigSocketElements.noRequestsMessage?.classList.remove('hidden'); sigSocketElements.requestsList?.classList.add('hidden'); } else { sigSocketElements.noRequestsMessage?.classList.add('hidden'); sigSocketElements.requestsList?.classList.remove('hidden'); if (sigSocketElements.requestsList) { sigSocketElements.requestsList.innerHTML = sigSocketRequests.map(request => createRequestItem(request) ).join(''); // Add event listeners to approve/reject buttons addRequestEventListeners(); } } } // Create HTML for a single request item function createRequestItem(request) { const requestTime = new Date(request.timestamp || Date.now()).toLocaleTimeString(); const shortId = request.id.substring(0, 8) + '...'; const decodedMessage = request.message ? atob(request.message) : 'No message'; // Check if keyspace is currently unlocked const isKeypaceUnlocked = currentKeyspace !== null; // Create different UI based on keyspace lock status let actionsHtml; let statusIndicator = ''; if (isKeypaceUnlocked) { // Normal approve/reject buttons when unlocked actionsHtml = `
`; } else { // Show pending status and unlock message when locked statusIndicator = '
⏳ Pending - Unlock keyspace to approve/reject
'; actionsHtml = `
`; } return `
${shortId}
${requestTime}
${decodedMessage.length > 100 ? decodedMessage.substring(0, 100) + '...' : decodedMessage}
${statusIndicator} ${actionsHtml}
`; } // Add event listeners to request action buttons function addRequestEventListeners() { // Approve buttons document.querySelectorAll('.btn-approve').forEach(btn => { btn.addEventListener('click', async (e) => { const requestId = e.target.getAttribute('data-request-id'); await approveSignRequest(requestId); }); }); // Reject buttons document.querySelectorAll('.btn-reject').forEach(btn => { btn.addEventListener('click', async (e) => { const requestId = e.target.getAttribute('data-request-id'); await rejectSignRequest(requestId); }); }); } // Handle new sign request notification function handleNewSignRequest(message) { // Update requests list if (message.pendingRequests) { updateRequestsList(message.pendingRequests); } // Show notification if workspace doesn't match if (!message.canApprove) { showWorkspaceMismatchWarning(); } } // Handle keyspace unlocked event - Clean flow implementation function handleKeypaceUnlocked(message) { console.log('🔓 Keyspace unlocked - applying clean flow for request display'); // Clean flow: Unlock -> Show loading -> Display requests -> Update UI try { // 1. Show loading state immediately showRequestsLoading(); // 2. Update requests list with restored + current requests if (message.pendingRequests && Array.isArray(message.pendingRequests)) { console.log(`📋 Displaying ${message.pendingRequests.length} restored requests`); updateRequestsList(message.pendingRequests); // 3. Update button states (should be enabled now) updateRequestButtonStates(message.canApprove !== false); // 4. Show appropriate notification const count = message.pendingRequests.length; if (count > 0) { showToast(`Keyspace unlocked! ${count} pending request${count > 1 ? 's' : ''} ready for review.`, 'info'); } else { showToast('Keyspace unlocked! No pending requests.', 'success'); } } else { // 5. If no requests in message, fetch fresh from server console.log('📋 No requests in unlock message, fetching from server...'); setTimeout(() => refreshSigSocketRequests(), 100); } console.log('✅ Keyspace unlock flow completed'); } catch (error) { console.error('❌ Error in keyspace unlock flow:', error); hideRequestsLoading(); showToast('Error loading requests after unlock', 'error'); } } // Handle connection status change event function handleConnectionStatusChanged(message) { console.log('🔄 Connection status changed:', message.status); // Store previous state for comparison const previousState = sigSocketStatus ? sigSocketStatus.isConnected : null; // Update the connection status display updateConnectionStatus(message.status); // Only show toast for actual changes, not initial status, and not during initial load if (!isInitialLoad && previousState !== null && previousState !== message.status.isConnected) { const statusText = message.status.isConnected ? 'Connected' : 'Disconnected'; const toastType = message.status.isConnected ? 'success' : 'warning'; showToast(`SigSocket ${statusText}`, toastType); } } // Show workspace mismatch warning function showWorkspaceMismatchWarning() { const existingWarning = document.querySelector('.workspace-mismatch'); if (existingWarning) return; // Don't show multiple warnings const warning = document.createElement('div'); warning.className = 'workspace-mismatch'; warning.innerHTML = ` ⚠️ Sign requests received for a different workspace. Switch to the correct workspace to approve requests. `; if (sigSocketElements.requestsContainer) { sigSocketElements.requestsContainer.insertBefore(warning, sigSocketElements.requestsContainer.firstChild); } // Auto-remove warning after 10 seconds setTimeout(() => { warning.remove(); }, 10000); } // Update request button states function updateRequestButtonStates(canApprove) { document.querySelectorAll('.btn-approve, .btn-reject').forEach(btn => { btn.disabled = !canApprove; }); } // Approve a sign request async function approveSignRequest(requestId) { let button = null; try { button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); setButtonLoading(button, true); const response = await sendMessage('approveSignRequest', { requestId }); if (response?.success) { showToast('Request approved and signed!', 'success'); showRequestsLoading(); await refreshSigSocketRequests(); } else { const errorMsg = getResponseError(response, 'approve request'); // Check for specific connection errors if (errorMsg.includes('Connection not found') || errorMsg.includes('public key')) { showToast('Connection error: Please check SigSocket connection and try again', 'error'); // Trigger a connection status refresh await loadSigSocketState(); } else if (errorMsg.includes('keyspace') || errorMsg.includes('locked')) { showToast('Keyspace is locked. Please unlock to approve requests.', 'error'); } else { throw new Error(errorMsg); } } } catch (error) { console.error('Error approving request:', error); showToast(`Failed to approve request: ${error.message}`, 'error'); } finally { // Re-query button in case DOM was updated during the operation const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); setButtonLoading(finalButton, false); } } // Reject a sign request async function rejectSignRequest(requestId) { let button = null; try { button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); setButtonLoading(button, true); const response = await sendMessage('rejectSignRequest', { requestId, reason: 'User rejected via extension' }); if (response?.success) { showToast('Request rejected', 'info'); showRequestsLoading(); await refreshSigSocketRequests(); } else { throw new Error(getResponseError(response, 'reject request')); } } catch (error) { showToast(`Failed to reject request: ${error.message}`, 'error'); } finally { // Re-query button in case DOM was updated during the operation const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); setButtonLoading(finalButton, false); } } // Refresh SigSocket requests async function refreshSigSocketRequests() { try { setButtonLoading(sigSocketElements.refreshRequestsBtn, true); showRequestsLoading(); console.log('🔄 Refreshing SigSocket requests...'); const response = await sendMessage('getPendingSignRequests'); if (response?.success) { console.log(`📋 Retrieved ${response.requests?.length || 0} pending requests`); updateRequestsList(response.requests); const count = response.requests?.length || 0; if (count > 0) { showToast(`${count} pending request${count > 1 ? 's' : ''} found`, 'success'); } else { showToast('No pending requests', 'info'); } } else { console.error('Failed to get pending requests:', response); hideRequestsLoading(); throw new Error(getResponseError(response, 'refresh requests')); } } catch (error) { console.error('Error refreshing requests:', error); hideRequestsLoading(); showToast(`Failed to refresh requests: ${error.message}`, 'error'); } finally { setButtonLoading(sigSocketElements.refreshRequestsBtn, false); } }