// Utility functions function showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = `toast ${type}`; setTimeout(() => toast.classList.add('hidden'), 3000); } function showLoading(show = true) { const overlay = document.getElementById('loadingOverlay'); overlay.classList.toggle('hidden', !show); } // Enhanced loading states for buttons function setButtonLoading(button, loading = true, originalText = null) { if (loading) { button.dataset.originalText = button.textContent; button.classList.add('loading'); button.disabled = true; } else { button.classList.remove('loading'); button.disabled = false; if (originalText) { button.textContent = originalText; } else if (button.dataset.originalText) { button.textContent = button.dataset.originalText; } } } // Show inline loading for specific operations function showInlineLoading(element, message = 'Processing...') { element.innerHTML = `
${message}
`; } // Enhanced error handling utility function getErrorMessage(error, fallback = 'An unexpected error occurred') { if (!error) return fallback; // If it's a string, return it if (typeof error === 'string') { return error.trim() || fallback; } // If it's an Error object if (error instanceof Error) { return error.message || fallback; } // If it's an object with error property if (error.error) { return getErrorMessage(error.error, fallback); } // If it's an object with message property if (error.message) { return error.message || fallback; } // Try to stringify if it's an object if (typeof error === 'object') { try { const stringified = JSON.stringify(error); if (stringified && stringified !== '{}') { return stringified; } } catch (e) { // Ignore JSON stringify errors } } return fallback; } // 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) { document.getElementById('statusText').textContent = text; const indicator = document.getElementById('statusIndicator'); indicator.classList.toggle('connected', isConnected); } // 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) { if (str.match(/^[0-9a-fA-F]+$/)) { // Hex string const bytes = []; for (let i = 0; i < str.length; i += 2) { bytes.push(parseInt(str.substr(i, 2), 16)); } return bytes; } else { // Regular string return Array.from(new TextEncoder().encode(str)); } } // DOM Elements const elements = { keyspaceInput: document.getElementById('keyspaceInput'), passwordInput: document.getElementById('passwordInput'), createKeyspaceBtn: document.getElementById('createKeyspaceBtn'), loginBtn: document.getElementById('loginBtn'), lockBtn: document.getElementById('lockBtn'), 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'), selectedKeypairCard: document.getElementById('selectedKeypairCard'), messageInput: document.getElementById('messageInput'), signBtn: document.getElementById('signBtn'), signatureResult: document.getElementById('signatureResult'), copyPublicKeyBtn: document.getElementById('copyPublicKeyBtn'), copySignatureBtn: document.getElementById('copySignatureBtn'), }; let currentKeyspace = null; let selectedKeypairId = null; // Initialize document.addEventListener('DOMContentLoaded', async function() { setStatus('Initializing...', false); // Event listeners elements.createKeyspaceBtn.addEventListener('click', createKeyspace); elements.loginBtn.addEventListener('click', login); elements.lockBtn.addEventListener('click', lockSession); elements.toggleAddKeypairBtn.addEventListener('click', toggleAddKeypairForm); elements.addKeypairBtn.addEventListener('click', addKeypair); elements.cancelAddKeypairBtn.addEventListener('click', hideAddKeypairForm); elements.signBtn.addEventListener('click', signMessage); elements.copyPublicKeyBtn.addEventListener('click', () => { const publicKey = document.getElementById('selectedPublicKey').textContent; copyToClipboard(publicKey); }); elements.copySignatureBtn.addEventListener('click', () => { const signature = document.getElementById('signatureValue').textContent; copyToClipboard(signature); }); // Enable sign button when message is entered elements.messageInput.addEventListener('input', () => { elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId; }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // Escape key closes the add keypair form if (e.key === 'Escape' && !elements.addKeypairCard.classList.contains('hidden')) { hideAddKeypairForm(); } // Enter key in the name input submits the form if (e.key === 'Enter' && e.target === elements.keyNameInput) { e.preventDefault(); if (elements.keyNameInput.value.trim()) { addKeypair(); } } }); // Check for existing session await checkExistingSession(); }); 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(`Connected to ${currentKeyspace}`, true); showSection('vaultSection'); await loadKeypairs(); showToast('Session restored!', 'success'); } else { // No active session setStatus('Ready', true); showSection('authSection'); } } catch (error) { console.error('Error checking session:', error); setStatus('Ready', true); showSection('authSection'); // Don't show toast for session check errors as it's not user-initiated } } // 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'); // Rotate the + icon to × when form is open const icon = elements.toggleAddKeypairBtn.querySelector('.btn-icon'); icon.style.transform = 'rotate(45deg)'; // Focus on the name input after animation setTimeout(() => { elements.keyNameInput.focus(); }, 300); } function hideAddKeypairForm() { elements.addKeypairCard.classList.add('hidden'); // Rotate the icon back to + const icon = elements.toggleAddKeypairBtn.querySelector('.btn-icon'); icon.style.transform = 'rotate(0deg)'; // Clear the form elements.keyNameInput.value = ''; elements.keyTypeSelect.selectedIndex = 0; } // Clear all vault-related state and UI function clearVaultState() { // Clear message input and signature result elements.messageInput.value = ''; elements.signatureResult.classList.add('hidden'); document.getElementById('signatureValue').textContent = ''; // Clear selected keypair state selectedKeypairId = null; elements.signBtn.disabled = true; // Clear selected keypair info (hidden elements) document.getElementById('selectedName').textContent = '-'; document.getElementById('selectedType').textContent = '-'; document.getElementById('selectedPublicKey').textContent = '-'; // Hide add keypair form if open hideAddKeypairForm(); // Clear keypairs list elements.keypairsList.innerHTML = '
Loading keypairs...
'; } async function createKeyspace() { const keyspace = elements.keyspaceInput.value.trim(); const password = elements.passwordInput.value.trim(); if (!keyspace || !password) { showToast('Please enter keyspace name and password', 'error'); return; } setButtonLoading(elements.createKeyspaceBtn, true); try { const response = await sendMessage('createKeyspace', { keyspace, password }); if (response && response.success) { showToast('Keyspace created successfully!', 'success'); // Clear any existing state before auto-login clearVaultState(); await login(); // Auto-login after creation } else { const errorMsg = getResponseError(response, 'create keyspace'); showToast(errorMsg, 'error'); } } catch (error) { const errorMsg = getErrorMessage(error, 'Failed to create keyspace'); console.error('Create keyspace error:', error); showToast(errorMsg, 'error'); } finally { setButtonLoading(elements.createKeyspaceBtn, false); } } async function login() { const keyspace = elements.keyspaceInput.value.trim(); const password = elements.passwordInput.value.trim(); if (!keyspace || !password) { showToast('Please enter keyspace name and password', 'error'); return; } setButtonLoading(elements.loginBtn, true); try { const response = await sendMessage('initSession', { keyspace, password }); if (response && response.success) { currentKeyspace = keyspace; setStatus(`Connected to ${keyspace}`, true); showSection('vaultSection'); // Clear any previous vault state before loading new keyspace clearVaultState(); await loadKeypairs(); showToast('Logged in successfully!', 'success'); } else { const errorMsg = getResponseError(response, 'login'); showToast(errorMsg, 'error'); } } catch (error) { const errorMsg = getErrorMessage(error, 'Failed to login'); console.error('Login error:', error); showToast(errorMsg, 'error'); } finally { setButtonLoading(elements.loginBtn, false); } } async function lockSession() { showLoading(true); try { await sendMessage('lockSession'); currentKeyspace = null; selectedKeypairId = null; setStatus('Locked', false); showSection('authSection'); // Clear all form inputs elements.keyspaceInput.value = ''; elements.passwordInput.value = ''; clearVaultState(); showToast('Session locked', 'info'); } catch (error) { showToast('Error: ' + error.message, 'error'); } finally { showLoading(false); } } 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; } // Use button loading instead of full overlay setButtonLoading(elements.addKeypairBtn, true); try { console.log('Adding keypair:', { keyType, keyName }); const metadata = JSON.stringify({ name: keyName }); console.log('Metadata:', metadata); const response = await sendMessage('addKeypair', { keyType, metadata }); console.log('Add keypair response:', response); if (response && response.success) { console.log('Keypair added successfully, clearing input and reloading list...'); hideAddKeypairForm(); // Hide the form after successful addition // Show inline loading in keypairs list while reloading showInlineLoading(elements.keypairsList, 'Adding keypair...'); await loadKeypairs(); showToast('Keypair added successfully!', 'success'); } else { const errorMsg = getResponseError(response, 'add keypair'); console.error('Failed to add keypair:', response); showToast(errorMsg, 'error'); } } catch (error) { const errorMsg = getErrorMessage(error, 'Failed to add keypair'); console.error('Error adding keypair:', error); showToast(errorMsg, 'error'); } finally { setButtonLoading(elements.addKeypairBtn, false); } } async function loadKeypairs() { try { console.log('Loading keypairs...'); const response = await sendMessage('listKeypairs'); console.log('Keypairs response:', response); if (response && response.success) { console.log('Keypairs data:', response.keypairs); console.log('Keypairs data type:', typeof response.keypairs); renderKeypairs(response.keypairs); } else { const errorMsg = getResponseError(response, 'load keypairs'); console.error('Failed to load keypairs:', response); const container = elements.keypairsList; container.innerHTML = '
Failed to load keypairs. Try refreshing.
'; showToast(errorMsg, 'error'); } } catch (error) { const errorMsg = getErrorMessage(error, 'Failed to load keypairs'); console.error('Error loading keypairs:', error); const container = elements.keypairsList; container.innerHTML = '
Error loading keypairs. Try refreshing.
'; showToast(errorMsg, 'error'); } } function renderKeypairs(keypairs) { console.log('Rendering keypairs:', keypairs); console.log('Keypairs type:', typeof keypairs); console.log('Keypairs is array:', Array.isArray(keypairs)); const container = elements.keypairsList; // Handle different data types that might be returned let keypairArray = []; if (Array.isArray(keypairs)) { keypairArray = keypairs; } else if (keypairs && typeof keypairs === 'object') { // If it's an object, try to extract array from common properties if (keypairs.keypairs && Array.isArray(keypairs.keypairs)) { keypairArray = keypairs.keypairs; } else if (keypairs.data && Array.isArray(keypairs.data)) { keypairArray = keypairs.data; } else { console.log('Keypairs object structure:', Object.keys(keypairs)); // Try to convert object to array if it has numeric keys const keys = Object.keys(keypairs); if (keys.length > 0 && keys.every(key => !isNaN(key))) { keypairArray = Object.values(keypairs); } } } console.log('Final keypair array:', keypairArray); console.log('Array length:', keypairArray.length); if (!keypairArray || keypairArray.length === 0) { console.log('No keypairs to render'); container.innerHTML = '
No keypairs found. Add one above.
'; return; } console.log('Rendering', keypairArray.length, 'keypairs'); container.innerHTML = keypairArray.map((keypair, index) => { console.log('Processing keypair:', 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 select buttons const selectButtons = container.querySelectorAll('.select-btn'); selectButtons.forEach(button => { button.addEventListener('click', (e) => { e.preventDefault(); // Prevent any default button behavior e.stopPropagation(); // Stop event bubbling const keypairId = e.target.getAttribute('data-keypair-id'); console.log('Select button clicked for keypair:', keypairId); selectKeypair(keypairId); }); }); } async function selectKeypair(keyId) { console.log('Selecting keypair:', 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) { const metadata = metadataResponse.metadata; // Store the details in hidden elements for internal use document.getElementById('selectedName').textContent = metadata.name || 'Unnamed'; document.getElementById('selectedType').textContent = metadata.key_type; document.getElementById('selectedPublicKey').textContent = publicKeyResponse.publicKey; // Enable sign button if message is entered elements.signBtn.disabled = !elements.messageInput.value.trim(); // Show a subtle success message without toast console.log(`Keypair "${metadata.name}" selected successfully`); } 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; console.error('Failed to get keypair details:', { metadataResponse, publicKeyResponse }); updateKeypairSelection(null); showToast(errorMsg, 'error'); } } catch (error) { const errorMsg = getErrorMessage(error, 'Failed to select keypair'); console.error('Error selecting keypair:', error); // Revert visual state if there was an error updateKeypairSelection(null); showToast(errorMsg, 'error'); } } function updateKeypairSelection(selectedId) { // Remove previous selection styling const allKeypairs = document.querySelectorAll('.keypair-item'); allKeypairs.forEach(item => { item.classList.remove('selected'); const button = item.querySelector('.select-btn'); button.textContent = 'Select'; button.classList.remove('selected'); }); // Add selection styling to the selected keypair (if any) if (selectedId) { const selectedKeypair = document.querySelector(`[data-id="${selectedId}"]`); if (selectedKeypair) { selectedKeypair.classList.add('selected'); const button = selectedKeypair.querySelector('.select-btn'); button.textContent = 'Selected'; button.classList.add('selected'); } } } async function signMessage() { const messageText = elements.messageInput.value.trim(); if (!messageText || !selectedKeypairId) { showToast('Please enter a message and select a keypair', 'error'); return; } // Use button loading and show inline loading in signature area setButtonLoading(elements.signBtn, true); // Show loading in signature result area elements.signatureResult.classList.remove('hidden'); showInlineLoading(elements.signatureResult, 'Signing message...'); try { const messageBytes = stringToUint8Array(messageText); const response = await sendMessage('sign', { message: messageBytes }); if (response && response.success) { // Restore signature result structure and show signature elements.signatureResult.innerHTML = `
${response.signature}
`; // Re-attach copy event listener document.getElementById('copySignatureBtn').addEventListener('click', () => { copyToClipboard(response.signature); }); showToast('Message signed successfully!', 'success'); } else { const errorMsg = getResponseError(response, 'sign message'); elements.signatureResult.classList.add('hidden'); showToast(errorMsg, 'error'); } } catch (error) { const errorMsg = getErrorMessage(error, 'Failed to sign message'); console.error('Sign message error:', error); elements.signatureResult.classList.add('hidden'); showToast(errorMsg, 'error'); } finally { setButtonLoading(elements.signBtn, false); } } // selectKeypair is now handled via event listeners, no need for global access