Add session timeout, refactor session manager, reduce code duplication, update icons & styling
| @@ -2,6 +2,9 @@ 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 | ||||
|  | ||||
| // Utility function to convert Uint8Array to hex | ||||
| function toHex(uint8Array) { | ||||
| @@ -9,53 +12,78 @@ function toHex(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(); | ||||
|         await sessionManager.clear(); | ||||
|         // 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 | ||||
|   try { | ||||
|     await chrome.storage.session.set({ cryptoVaultSession: currentSession }); | ||||
|     await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession }); | ||||
|   } catch (error) { | ||||
|     console.error('Failed to save session:', error); | ||||
|   } | ||||
|   await chrome.storage.session.set({ cryptoVaultSession: currentSession }); | ||||
|   await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession }); | ||||
| } | ||||
|  | ||||
| async function loadSession() { | ||||
|   try { | ||||
|     // Try session storage first | ||||
|     let result = await chrome.storage.session.get(['cryptoVaultSession']); | ||||
|     if (result.cryptoVaultSession) { | ||||
|       currentSession = result.cryptoVaultSession; | ||||
|       return currentSession; | ||||
|     } | ||||
|   // 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; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('Failed to load session:', error); | ||||
|   // 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; | ||||
|   try { | ||||
|     await chrome.storage.session.remove(['cryptoVaultSession']); | ||||
|     await chrome.storage.local.remove(['cryptoVaultSessionBackup']); | ||||
|   } catch (error) { | ||||
|     console.error('Failed to clear session:', error); | ||||
|   } | ||||
|   await chrome.storage.session.remove(['cryptoVaultSession']); | ||||
|   await chrome.storage.local.remove(['cryptoVaultSessionBackup']); | ||||
| } | ||||
|  | ||||
| // Keep service worker alive | ||||
| @@ -64,12 +92,8 @@ function startKeepAlive() { | ||||
|     clearInterval(keepAliveInterval); | ||||
|   } | ||||
|  | ||||
|   // Ping every 20 seconds to keep service worker alive | ||||
|   keepAliveInterval = setInterval(() => { | ||||
|     // Simple operation to keep service worker active | ||||
|     chrome.storage.session.get(['keepAlive']).catch(() => { | ||||
|       // Ignore errors | ||||
|     }); | ||||
|     chrome.storage.session.get(['keepAlive']).catch(() => {}); | ||||
|   }, 20000); | ||||
| } | ||||
|  | ||||
| @@ -80,54 +104,39 @@ function stopKeepAlive() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Enhanced session management with keep-alive | ||||
| async function saveSessionWithKeepAlive(keyspace) { | ||||
|   await saveSession(keyspace); | ||||
|   startKeepAlive(); | ||||
| } | ||||
|  | ||||
| async function clearSessionWithKeepAlive() { | ||||
|   await clearSession(); | ||||
|   stopKeepAlive(); | ||||
| } | ||||
| // 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) { | ||||
|     try { | ||||
|       // 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(); | ||||
|         return session; | ||||
|       } else { | ||||
|         await clearSessionWithKeepAlive(); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error('Error checking session validity:', error); | ||||
|       await clearSessionWithKeepAlive(); | ||||
|     // 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(); | ||||
|       return session; | ||||
|     } else { | ||||
|       await sessionManager.clear(); | ||||
|     } | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
|  | ||||
| // Import WASM module functions | ||||
| import init, { | ||||
|   create_keyspace, | ||||
|   init_session, | ||||
|   is_unlocked, | ||||
|   add_keypair, | ||||
|   list_keypairs, | ||||
|   select_keypair, | ||||
|   current_keypair_metadata, | ||||
|   current_keypair_public_key, | ||||
|   sign, | ||||
|   verify, | ||||
|   encrypt_data, | ||||
|   decrypt_data, | ||||
|   lock_session | ||||
| } from './wasm/wasm_app.js'; | ||||
| import init, * as wasmFunctions from './wasm/wasm_app.js'; | ||||
|  | ||||
| // Initialize WASM module | ||||
| async function initVault() { | ||||
| @@ -138,23 +147,8 @@ async function initVault() { | ||||
|     const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm'); | ||||
|     await init(wasmUrl); | ||||
|  | ||||
|     // Create a vault object with all the imported functions | ||||
|     vault = { | ||||
|       create_keyspace, | ||||
|       init_session, | ||||
|       is_unlocked, | ||||
|       add_keypair, | ||||
|       list_keypairs, | ||||
|       select_keypair, | ||||
|       current_keypair_metadata, | ||||
|       current_keypair_public_key, | ||||
|       sign, | ||||
|       verify, | ||||
|       encrypt_data, | ||||
|       decrypt_data, | ||||
|       lock_session | ||||
|     }; | ||||
|  | ||||
|     // Use imported functions directly | ||||
|     vault = wasmFunctions; | ||||
|     isInitialized = true; | ||||
|  | ||||
|     // Try to restore previous session | ||||
| @@ -167,20 +161,108 @@ async function initVault() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Handle popup connection/disconnection | ||||
| chrome.runtime.onConnect.addListener((port) => { | ||||
|   if (port.name === 'popup') { | ||||
|     // If we have an active session, ensure keep-alive is running | ||||
|     if (currentSession) { | ||||
|       startKeepAlive(); | ||||
|     } | ||||
|  | ||||
|     port.onDisconnect.addListener(() => { | ||||
|       // Keep the keep-alive running even after popup disconnects | ||||
|       // This ensures session persistence across popup closes | ||||
|     }); | ||||
| // 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); | ||||
|     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 }; | ||||
|   } | ||||
| }); | ||||
| }; | ||||
|  | ||||
| // Handle messages from popup and content scripts | ||||
| chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { | ||||
| @@ -190,143 +272,13 @@ chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => { | ||||
|         await initVault(); | ||||
|       } | ||||
|  | ||||
|       switch (request.action) { | ||||
|         case 'createKeyspace': | ||||
|           await vault.create_keyspace(request.keyspace, request.password); | ||||
|           return { success: true }; | ||||
|  | ||||
|         case 'initSession': | ||||
|           await vault.init_session(request.keyspace, request.password); | ||||
|           await saveSessionWithKeepAlive(request.keyspace); | ||||
|           return { success: true }; | ||||
|  | ||||
|         case 'isUnlocked': | ||||
|           const unlocked = vault.is_unlocked(); | ||||
|           return { success: true, unlocked }; | ||||
|  | ||||
|         case 'addKeypair': | ||||
|           const result = await vault.add_keypair(request.keyType, request.metadata); | ||||
|           return { success: true, result }; | ||||
|  | ||||
|         case 'listKeypairs': | ||||
|           // Check if session is unlocked first | ||||
|           const isUnlocked = vault.is_unlocked(); | ||||
|  | ||||
|           if (!isUnlocked) { | ||||
|             return { success: false, error: 'Session is not unlocked' }; | ||||
|           } | ||||
|  | ||||
|           try { | ||||
|             const keypairsRaw = await vault.list_keypairs(); | ||||
|  | ||||
|             // Parse JSON string if needed | ||||
|             let keypairs; | ||||
|             if (typeof keypairsRaw === 'string') { | ||||
|               keypairs = JSON.parse(keypairsRaw); | ||||
|             } else { | ||||
|               keypairs = keypairsRaw; | ||||
|             } | ||||
|  | ||||
|             return { success: true, keypairs }; | ||||
|           } catch (listError) { | ||||
|             console.error('Background: Error calling list_keypairs:', listError); | ||||
|             throw listError; | ||||
|           } | ||||
|  | ||||
|         case 'selectKeypair': | ||||
|           vault.select_keypair(request.keyId); | ||||
|           return { success: true }; | ||||
|  | ||||
|         case 'getCurrentKeypairMetadata': | ||||
|           const metadata = vault.current_keypair_metadata(); | ||||
|           return { success: true, metadata }; | ||||
|  | ||||
|         case 'getCurrentKeypairPublicKey': | ||||
|           const publicKey = vault.current_keypair_public_key(); | ||||
|           const hexKey = toHex(publicKey); | ||||
|           return { success: true, publicKey: hexKey }; | ||||
|  | ||||
|         case 'sign': | ||||
|           const signature = await vault.sign(new Uint8Array(request.message)); | ||||
|           return { success: true, signature }; | ||||
|  | ||||
|         case 'encrypt': | ||||
|           // Check if session is unlocked | ||||
|           if (!vault.is_unlocked()) { | ||||
|             return { success: false, error: 'Session is not unlocked' }; | ||||
|           } | ||||
|  | ||||
|           try { | ||||
|             // Convert message to Uint8Array for WASM | ||||
|             const messageBytes = new TextEncoder().encode(request.message); | ||||
|  | ||||
|             // Use WASM encrypt_data function with ChaCha20-Poly1305 | ||||
|             const encryptedData = await vault.encrypt_data(messageBytes); | ||||
|  | ||||
|             // Convert result to base64 for easy handling | ||||
|             const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData))); | ||||
|             return { success: true, encryptedMessage }; | ||||
|           } catch (error) { | ||||
|             console.error('Encryption error:', error); | ||||
|             return { success: false, error: error.message }; | ||||
|           } | ||||
|  | ||||
|         case 'decrypt': | ||||
|           // Check if session is unlocked | ||||
|           if (!vault.is_unlocked()) { | ||||
|             return { success: false, error: 'Session is not unlocked' }; | ||||
|           } | ||||
|  | ||||
|           try { | ||||
|             // Convert base64 back to Uint8Array | ||||
|             const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0))); | ||||
|  | ||||
|             // Use WASM decrypt_data function with ChaCha20-Poly1305 | ||||
|             const decryptedData = await vault.decrypt_data(encryptedBytes); | ||||
|  | ||||
|             // Convert result back to string | ||||
|             const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData)); | ||||
|             return { success: true, decryptedMessage }; | ||||
|           } catch (error) { | ||||
|             console.error('Decryption error:', error); | ||||
|             return { success: false, error: error.message }; | ||||
|           } | ||||
|  | ||||
|         case 'verify': | ||||
|           // Check if a keypair is selected | ||||
|           try { | ||||
|             const metadata = vault.current_keypair_metadata(); | ||||
|             if (!metadata) { | ||||
|               return { success: false, error: 'No keypair selected' }; | ||||
|             } | ||||
|  | ||||
|             // Use WASM verify function | ||||
|             const isValid = await vault.verify(new Uint8Array(request.message), request.signature); | ||||
|             return { success: true, isValid }; | ||||
|           } catch (error) { | ||||
|             console.error('Verification error:', error); | ||||
|             return { success: false, error: error.message }; | ||||
|           } | ||||
|  | ||||
|         case 'lockSession': | ||||
|           vault.lock_session(); | ||||
|           await clearSessionWithKeepAlive(); | ||||
|           return { success: true }; | ||||
|  | ||||
|         case 'getStatus': | ||||
|           const status = vault ? vault.is_unlocked() : false; | ||||
|           const session = await loadSession(); | ||||
|           return { | ||||
|             success: true, | ||||
|             status, | ||||
|             session: session ? { keyspace: session.keyspace } : null | ||||
|           }; | ||||
|  | ||||
|         default: | ||||
|           throw new Error('Unknown action: ' + request.action); | ||||
|       const handler = messageHandlers[request.action]; | ||||
|       if (handler) { | ||||
|         return await handler(request); | ||||
|       } else { | ||||
|         throw new Error('Unknown action: ' + request.action); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error('Background script error:', error); | ||||
|       return { success: false, error: error.message }; | ||||
|     } | ||||
|   }; | ||||
| @@ -342,4 +294,23 @@ chrome.runtime.onStartup.addListener(() => { | ||||
|  | ||||
| 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; | ||||
|  | ||||
|     // If we have an active session, ensure keep-alive is running | ||||
|     if (currentSession) { | ||||
|       startKeepAlive(); | ||||
|     } | ||||
|  | ||||
|     port.onDisconnect.addListener(() => { | ||||
|       // Popup closed, clear reference and stop keep-alive | ||||
|       popupPort = null; | ||||
|       stopKeepAlive(); | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
| Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 7.5 KiB | 
| Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 676 B | 
							
								
								
									
										
											BIN
										
									
								
								crypto_vault_extension/icons/icon32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								crypto_vault_extension/icons/icon36.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.1 KiB | 
| @@ -1,5 +1,3 @@ | ||||
| // Enhanced Error Handling System for CryptoVault Extension | ||||
|  | ||||
| class CryptoVaultError extends Error { | ||||
|   constructor(message, code, retryable = false, userMessage = null) { | ||||
|     super(message); | ||||
| @@ -11,35 +9,24 @@ class CryptoVaultError extends Error { | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Error codes for different types of errors | ||||
| const ERROR_CODES = { | ||||
|   // Network/Connection errors (retryable) | ||||
|   NETWORK_ERROR: 'NETWORK_ERROR', | ||||
|   TIMEOUT_ERROR: 'TIMEOUT_ERROR', | ||||
|   SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', | ||||
|  | ||||
|   // Authentication errors (not retryable) | ||||
|   INVALID_PASSWORD: 'INVALID_PASSWORD', | ||||
|   SESSION_EXPIRED: 'SESSION_EXPIRED', | ||||
|   UNAUTHORIZED: 'UNAUTHORIZED', | ||||
|  | ||||
|   // Crypto errors (not retryable) | ||||
|   CRYPTO_ERROR: 'CRYPTO_ERROR', | ||||
|   INVALID_SIGNATURE: 'INVALID_SIGNATURE', | ||||
|   ENCRYPTION_FAILED: 'ENCRYPTION_FAILED', | ||||
|  | ||||
|   // Validation errors (not retryable) | ||||
|   INVALID_INPUT: 'INVALID_INPUT', | ||||
|   MISSING_KEYPAIR: 'MISSING_KEYPAIR', | ||||
|   INVALID_FORMAT: 'INVALID_FORMAT', | ||||
|  | ||||
|   // System errors (sometimes retryable) | ||||
|   WASM_ERROR: 'WASM_ERROR', | ||||
|   STORAGE_ERROR: 'STORAGE_ERROR', | ||||
|   UNKNOWN_ERROR: 'UNKNOWN_ERROR' | ||||
| }; | ||||
|  | ||||
| // User-friendly error messages | ||||
| const ERROR_MESSAGES = { | ||||
|   [ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.', | ||||
|   [ERROR_CODES.TIMEOUT_ERROR]: 'Operation timed out. Please try again.', | ||||
| @@ -62,7 +49,6 @@ const ERROR_MESSAGES = { | ||||
|   [ERROR_CODES.UNKNOWN_ERROR]: 'An unexpected error occurred. Please try again.' | ||||
| }; | ||||
|  | ||||
| // Determine if an error is retryable | ||||
| const RETRYABLE_ERRORS = new Set([ | ||||
|   ERROR_CODES.NETWORK_ERROR, | ||||
|   ERROR_CODES.TIMEOUT_ERROR, | ||||
| @@ -71,11 +57,9 @@ const RETRYABLE_ERRORS = new Set([ | ||||
|   ERROR_CODES.STORAGE_ERROR | ||||
| ]); | ||||
|  | ||||
| // Enhanced error classification | ||||
| function classifyError(error) { | ||||
|   const errorMessage = getErrorMessage(error); | ||||
|  | ||||
|   // Network/Connection errors | ||||
|   if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) { | ||||
|     return new CryptoVaultError( | ||||
|       errorMessage, | ||||
| @@ -85,7 +69,6 @@ function classifyError(error) { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Authentication errors | ||||
|   if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) { | ||||
|     return new CryptoVaultError( | ||||
|       errorMessage, | ||||
| @@ -104,7 +87,6 @@ function classifyError(error) { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Crypto errors | ||||
|   if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) { | ||||
|     return new CryptoVaultError( | ||||
|       errorMessage, | ||||
| @@ -123,7 +105,6 @@ function classifyError(error) { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Validation errors | ||||
|   if (errorMessage.includes('No keypair selected')) { | ||||
|     return new CryptoVaultError( | ||||
|       errorMessage, | ||||
| @@ -133,7 +114,6 @@ function classifyError(error) { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // WASM errors | ||||
|   if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) { | ||||
|     return new CryptoVaultError( | ||||
|       errorMessage, | ||||
| @@ -143,7 +123,6 @@ function classifyError(error) { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Default to unknown error | ||||
|   return new CryptoVaultError( | ||||
|     errorMessage, | ||||
|     ERROR_CODES.UNKNOWN_ERROR, | ||||
| @@ -152,7 +131,6 @@ function classifyError(error) { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| // Get error message from various error types | ||||
| function getErrorMessage(error) { | ||||
|   if (!error) return 'Unknown error'; | ||||
|  | ||||
| @@ -179,14 +157,13 @@ function getErrorMessage(error) { | ||||
|         return stringified; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       // Ignore JSON stringify errors | ||||
|       // Silently handle JSON stringify errors | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return 'Unknown error'; | ||||
| } | ||||
|  | ||||
| // Retry logic with exponential backoff | ||||
| async function withRetry(operation, options = {}) { | ||||
|   const { | ||||
|     maxRetries = 3, | ||||
| @@ -205,20 +182,16 @@ async function withRetry(operation, options = {}) { | ||||
|       const classifiedError = classifyError(error); | ||||
|       lastError = classifiedError; | ||||
|  | ||||
|       // Don't retry if it's the last attempt or error is not retryable | ||||
|       if (attempt === maxRetries || !classifiedError.retryable) { | ||||
|         throw classifiedError; | ||||
|       } | ||||
|  | ||||
|       // Calculate delay with exponential backoff | ||||
|       const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay); | ||||
|  | ||||
|       // Call retry callback if provided | ||||
|       if (onRetry) { | ||||
|         onRetry(attempt + 1, delay, classifiedError); | ||||
|       } | ||||
|  | ||||
|       // Wait before retrying | ||||
|       await new Promise(resolve => setTimeout(resolve, delay)); | ||||
|     } | ||||
|   } | ||||
| @@ -226,7 +199,6 @@ async function withRetry(operation, options = {}) { | ||||
|   throw lastError; | ||||
| } | ||||
|  | ||||
| // Enhanced operation wrapper with loading states | ||||
| async function executeOperation(operation, options = {}) { | ||||
|   const { | ||||
|     loadingElement = null, | ||||
| @@ -235,7 +207,6 @@ async function executeOperation(operation, options = {}) { | ||||
|     onProgress = null | ||||
|   } = options; | ||||
|  | ||||
|   // Show loading state | ||||
|   if (loadingElement) { | ||||
|     setButtonLoading(loadingElement, true); | ||||
|   } | ||||
| @@ -253,25 +224,21 @@ async function executeOperation(operation, options = {}) { | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Show success message if provided | ||||
|     if (successMessage) { | ||||
|       showToast(successMessage, 'success'); | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
|   } catch (error) { | ||||
|     // Show user-friendly error message | ||||
|     showToast(error.userMessage || error.message, 'error'); | ||||
|     throw error; | ||||
|   } finally { | ||||
|     // Hide loading state | ||||
|     if (loadingElement) { | ||||
|       setButtonLoading(loadingElement, false); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Export for use in other modules | ||||
| window.CryptoVaultError = CryptoVaultError; | ||||
| window.ERROR_CODES = ERROR_CODES; | ||||
| window.classifyError = classifyError; | ||||
|   | ||||
| @@ -9,17 +9,23 @@ | ||||
|     "activeTab" | ||||
|   ], | ||||
|  | ||||
|   "icons": { | ||||
|     "16": "icons/icon16.png", | ||||
|     "32": "icons/icon32.png", | ||||
|     "48": "icons/icon48.png", | ||||
|     "128": "icons/icon128.png" | ||||
|   }, | ||||
|  | ||||
|   "background": { | ||||
|     "service_worker": "background.js", | ||||
|     "type": "module" | ||||
|   }, | ||||
|  | ||||
|  | ||||
|  | ||||
|   "action": { | ||||
|     "default_popup": "popup.html", | ||||
|     "default_icon": { | ||||
|       "16": "icons/icon16.png", | ||||
|       "32": "icons/icon32.png", | ||||
|       "48": "icons/icon48.png", | ||||
|       "128": "icons/icon128.png" | ||||
|     } | ||||
|   | ||||
| @@ -12,6 +12,23 @@ | ||||
|         <h1>CryptoVault</h1> | ||||
|       </div> | ||||
|       <div class="header-actions"> | ||||
|         <div class="settings-container"> | ||||
|           <button id="settingsToggle" class="btn-icon-only" title="Settings"> | ||||
|             <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|               <circle cx="12" cy="12" r="3"></circle> | ||||
|               <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> | ||||
|             </svg> | ||||
|           </button> | ||||
|           <div class="settings-dropdown hidden" id="settingsDropdown"> | ||||
|             <div class="settings-item"> | ||||
|               <label for="timeoutInput">Session Timeout</label> | ||||
|               <div class="timeout-input-group"> | ||||
|                 <input type="number" id="timeoutInput" min="3" max="300" value="15"> | ||||
|                 <span>seconds</span> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <button id="themeToggle" class="btn-icon-only" title="Switch to dark mode"> | ||||
|           <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|             <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> | ||||
| @@ -44,10 +61,7 @@ | ||||
|       <!-- Status Section --> | ||||
|       <div class="vault-status" id="vaultStatus"> | ||||
|         <div class="status-indicator" id="statusIndicator"> | ||||
|           <div class="status-content"> | ||||
|             <div class="status-dot"></div> | ||||
|             <span id="statusText">Initializing...</span> | ||||
|           </div> | ||||
|           <span id="statusText"></span> | ||||
|           <button id="lockBtn" class="btn btn-ghost btn-small hidden"> | ||||
|             <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|               <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> | ||||
|   | ||||
| @@ -1,33 +1,27 @@ | ||||
| // Enhanced toast system | ||||
| // Consolidated toast system | ||||
| function showToast(message, type = 'info') { | ||||
|   // Remove any existing toast | ||||
|   const existingToast = document.querySelector('.toast-notification'); | ||||
|   if (existingToast) { | ||||
|     existingToast.remove(); | ||||
|   } | ||||
|   document.querySelector('.toast-notification')?.remove(); | ||||
|  | ||||
|   // Create new toast element | ||||
|   const toast = document.createElement('div'); | ||||
|   toast.className = `toast-notification toast-${type}`; | ||||
|   const icons = { | ||||
|     success: '<polyline points="20,6 9,17 4,12"></polyline>', | ||||
|     error: '<circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line>', | ||||
|     info: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>' | ||||
|   }; | ||||
|  | ||||
|   // Add icon based on type | ||||
|   const icon = getToastIcon(type); | ||||
|   const toast = Object.assign(document.createElement('div'), { | ||||
|     className: `toast-notification toast-${type}`, | ||||
|     innerHTML: ` | ||||
|       <div class="toast-icon"> | ||||
|         <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|           ${icons[type] || icons.info} | ||||
|         </svg> | ||||
|       </div> | ||||
|       <div class="toast-content"><div class="toast-message">${message}</div></div> | ||||
|     ` | ||||
|   }); | ||||
|  | ||||
|   toast.innerHTML = ` | ||||
|     <div class="toast-icon">${icon}</div> | ||||
|     <div class="toast-content"> | ||||
|       <div class="toast-message">${message}</div> | ||||
|     </div> | ||||
|     <button class="toast-close" onclick="this.parentElement.remove()">×</button> | ||||
|   `; | ||||
|  | ||||
|   // Add to document | ||||
|   document.body.appendChild(toast); | ||||
|  | ||||
|   // Trigger entrance animation | ||||
|   setTimeout(() => toast.classList.add('toast-show'), 10); | ||||
|  | ||||
|   // Auto-remove after 4 seconds | ||||
|   setTimeout(() => { | ||||
|     if (toast.parentElement) { | ||||
|       toast.classList.add('toast-hide'); | ||||
| @@ -36,30 +30,6 @@ function showToast(message, type = 'info') { | ||||
|   }, 4000); | ||||
| } | ||||
|  | ||||
| function getToastIcon(type) { | ||||
|   switch (type) { | ||||
|     case 'success': | ||||
|       return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|         <polyline points="20,6 9,17 4,12"></polyline> | ||||
|       </svg>`; | ||||
|     case 'error': | ||||
|       return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|         <circle cx="12" cy="12" r="10"></circle> | ||||
|         <line x1="15" y1="9" x2="9" y2="15"></line> | ||||
|         <line x1="9" y1="9" x2="15" y2="15"></line> | ||||
|       </svg>`; | ||||
|     case 'info': | ||||
|     default: | ||||
|       return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|         <circle cx="12" cy="12" r="10"></circle> | ||||
|         <line x1="12" y1="16" x2="12" y2="12"></line> | ||||
|         <line x1="12" y1="8" x2="12.01" y2="8"></line> | ||||
|       </svg>`; | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| // Enhanced loading states for buttons | ||||
| function setButtonLoading(button, loading = true) { | ||||
|   if (loading) { | ||||
| @@ -109,16 +79,17 @@ function showSection(sectionId) { | ||||
| } | ||||
|  | ||||
| function setStatus(text, isConnected = false) { | ||||
|   document.getElementById('statusText').textContent = text; | ||||
|   const indicator = document.getElementById('statusIndicator'); | ||||
|   indicator.classList.toggle('connected', isConnected); | ||||
|  | ||||
|   // Show/hide lock button - only show when session is unlocked | ||||
|   const statusText = document.getElementById('statusText'); | ||||
|   const statusSection = document.getElementById('vaultStatus'); | ||||
|   const lockBtn = document.getElementById('lockBtn'); | ||||
|   if (lockBtn) { | ||||
|     // Only show lock button when connected AND status indicates unlocked session | ||||
|     const isUnlocked = isConnected && text.toLowerCase().startsWith('connected to'); | ||||
|     lockBtn.classList.toggle('hidden', !isUnlocked); | ||||
|  | ||||
|   if (isConnected && text) { | ||||
|     // Show keyspace name and status section | ||||
|     statusText.textContent = text; | ||||
|     statusSection.classList.remove('hidden'); | ||||
|     if (lockBtn) { | ||||
|       lockBtn.classList.remove('hidden'); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -146,12 +117,20 @@ function stringToUint8Array(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'), | ||||
|   settingsToggle: document.getElementById('settingsToggle'), | ||||
|   settingsDropdown: document.getElementById('settingsDropdown'), | ||||
|   timeoutInput: document.getElementById('timeoutInput'), | ||||
|  | ||||
|   // Keypair management elements | ||||
|   toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'), | ||||
|   addKeypairCard: document.getElementById('addKeypairCard'), | ||||
|   keyTypeSelect: document.getElementById('keyTypeSelect'), | ||||
| @@ -160,34 +139,91 @@ const elements = { | ||||
|   cancelAddKeypairBtn: document.getElementById('cancelAddKeypairBtn'), | ||||
|   keypairsList: document.getElementById('keypairsList'), | ||||
|  | ||||
|   // Sign tab | ||||
|   // Crypto operation elements - Sign tab | ||||
|   messageInput: document.getElementById('messageInput'), | ||||
|   signBtn: document.getElementById('signBtn'), | ||||
|   signatureResult: document.getElementById('signatureResult'), | ||||
|   copySignatureBtn: document.getElementById('copySignatureBtn'), | ||||
|  | ||||
|   // Encrypt tab | ||||
|   // Crypto operation elements - Encrypt tab | ||||
|   encryptMessageInput: document.getElementById('encryptMessageInput'), | ||||
|   encryptBtn: document.getElementById('encryptBtn'), | ||||
|   encryptResult: document.getElementById('encryptResult'), | ||||
|  | ||||
|   // Decrypt tab | ||||
|   // Crypto operation elements - Decrypt tab | ||||
|   encryptedMessageInput: document.getElementById('encryptedMessageInput'), | ||||
|   decryptBtn: document.getElementById('decryptBtn'), | ||||
|   decryptResult: document.getElementById('decryptResult'), | ||||
|  | ||||
|   // Verify tab | ||||
|   // 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 }); | ||||
| } | ||||
|  | ||||
| async function resetSessionTimeout() { | ||||
|   if (currentKeyspace) { | ||||
|     await sendMessage('resetTimeout'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Theme management | ||||
| function initializeTheme() { | ||||
| @@ -205,18 +241,46 @@ function toggleTheme() { | ||||
|   updateThemeIcon(newTheme); | ||||
| } | ||||
|  | ||||
| // Settings dropdown management | ||||
| function toggleSettingsDropdown() { | ||||
|   const dropdown = elements.settingsDropdown; | ||||
|   if (dropdown) { | ||||
|     dropdown.classList.toggle('hidden'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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') { | ||||
|     themeToggle.innerHTML = '☀️'; | ||||
|     // Bright sun SVG for dark theme | ||||
|     themeToggle.innerHTML = ` | ||||
|       <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|         <circle cx="12" cy="12" r="5"></circle> | ||||
|         <line x1="12" y1="1" x2="12" y2="3"></line> | ||||
|         <line x1="12" y1="21" x2="12" y2="23"></line> | ||||
|         <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> | ||||
|         <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line> | ||||
|         <line x1="1" y1="12" x2="3" y2="12"></line> | ||||
|         <line x1="21" y1="12" x2="23" y2="12"></line> | ||||
|         <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> | ||||
|         <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> | ||||
|       </svg> | ||||
|     `; | ||||
|     themeToggle.title = 'Switch to light mode'; | ||||
|   } else { | ||||
|     // Dark crescent moon SVG for better visibility | ||||
|     // Dark crescent moon SVG for light theme | ||||
|     themeToggle.innerHTML = ` | ||||
|       <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none"> | ||||
|         <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" fill="#333"/> | ||||
|       <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|         <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> | ||||
|       </svg> | ||||
|     `; | ||||
|     themeToggle.title = 'Switch to dark mode'; | ||||
| @@ -225,14 +289,30 @@ function updateThemeIcon(theme) { | ||||
|  | ||||
| // Establish connection to background script for keep-alive | ||||
| function connectToBackground() { | ||||
|   try { | ||||
|     backgroundPort = chrome.runtime.connect({ name: 'popup' }); | ||||
|     backgroundPort.onDisconnect.addListener(() => { | ||||
|       backgroundPort = null; | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     // Silently handle connection errors | ||||
|   } | ||||
|   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'); | ||||
|       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; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Initialize | ||||
| @@ -240,78 +320,78 @@ document.addEventListener('DOMContentLoaded', async function() { | ||||
|   // Initialize theme first | ||||
|   initializeTheme(); | ||||
|  | ||||
|   // Load timeout setting | ||||
|   await loadTimeoutSetting(); | ||||
|  | ||||
|   // Ensure lock button starts hidden | ||||
|   const lockBtn = document.getElementById('lockBtn'); | ||||
|   if (lockBtn) { | ||||
|     lockBtn.classList.add('hidden'); | ||||
|   } | ||||
|  | ||||
|   setStatus('Initializing...', false); | ||||
|  | ||||
|   // Connect to background script for keep-alive | ||||
|   connectToBackground(); | ||||
|  | ||||
|   // Event listeners (with null checks) | ||||
|   if (elements.createKeyspaceBtn) { | ||||
|     elements.createKeyspaceBtn.addEventListener('click', createKeyspace); | ||||
|   } | ||||
|   if (elements.loginBtn) { | ||||
|     elements.loginBtn.addEventListener('click', login); | ||||
|   } | ||||
|   if (elements.lockBtn) { | ||||
|     elements.lockBtn.addEventListener('click', lockSession); | ||||
|   } | ||||
|   if (elements.themeToggle) { | ||||
|     elements.themeToggle.addEventListener('click', toggleTheme); | ||||
|   } | ||||
|   // Consolidated event listeners | ||||
|   const eventMap = { | ||||
|     createKeyspaceBtn: createKeyspace, | ||||
|     loginBtn: login, | ||||
|     lockBtn: lockSession, | ||||
|     themeToggle: toggleTheme, | ||||
|     settingsToggle: toggleSettingsDropdown, | ||||
|     toggleAddKeypairBtn: toggleAddKeypairForm, | ||||
|     addKeypairBtn: addKeypair, | ||||
|     cancelAddKeypairBtn: hideAddKeypairForm, | ||||
|     signBtn: signMessage, | ||||
|     encryptBtn: encryptMessage, | ||||
|     decryptBtn: decryptMessage, | ||||
|     verifyBtn: verifySignature | ||||
|   }; | ||||
|  | ||||
|   if (elements.toggleAddKeypairBtn) { | ||||
|     elements.toggleAddKeypairBtn.addEventListener('click', toggleAddKeypairForm); | ||||
|   } | ||||
|   if (elements.addKeypairBtn) { | ||||
|     elements.addKeypairBtn.addEventListener('click', addKeypair); | ||||
|   } | ||||
|   if (elements.cancelAddKeypairBtn) { | ||||
|     elements.cancelAddKeypairBtn.addEventListener('click', hideAddKeypairForm); | ||||
|   } | ||||
|  | ||||
|   // Crypto operation buttons (with null checks) | ||||
|   if (elements.signBtn) { | ||||
|     elements.signBtn.addEventListener('click', signMessage); | ||||
|   } | ||||
|   if (elements.encryptBtn) { | ||||
|     elements.encryptBtn.addEventListener('click', encryptMessage); | ||||
|   } | ||||
|   if (elements.decryptBtn) { | ||||
|     elements.decryptBtn.addEventListener('click', decryptMessage); | ||||
|   } | ||||
|   if (elements.verifyBtn) { | ||||
|     elements.verifyBtn.addEventListener('click', verifySignature); | ||||
|   } | ||||
|   Object.entries(eventMap).forEach(([elementKey, handler]) => { | ||||
|     elements[elementKey]?.addEventListener('click', handler); | ||||
|   }); | ||||
|  | ||||
|   // Tab functionality | ||||
|   initializeTabs(); | ||||
|  | ||||
|   // Copy button event listeners (with null checks) | ||||
|   if (elements.copySignatureBtn) { | ||||
|     elements.copySignatureBtn.addEventListener('click', () => { | ||||
|       const signature = document.getElementById('signatureValue'); | ||||
|       if (signature) { | ||||
|         copyToClipboard(signature.textContent); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   // Additional event listeners | ||||
|   elements.copySignatureBtn?.addEventListener('click', () => { | ||||
|     copyToClipboard(document.getElementById('signatureValue')?.textContent); | ||||
|   }); | ||||
|  | ||||
|   // Enable sign button when message is entered (with null checks) | ||||
|   if (elements.messageInput && elements.signBtn) { | ||||
|     elements.messageInput.addEventListener('input', () => { | ||||
|   elements.messageInput?.addEventListener('input', () => { | ||||
|     if (elements.signBtn) { | ||||
|       elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId; | ||||
|     }); | ||||
|   } | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // Basic keyboard shortcuts | ||||
|   // 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 && !elements.addKeypairCard.classList.contains('hidden')) { | ||||
|     if (e.key === 'Escape' && !elements.addKeypairCard?.classList.contains('hidden')) { | ||||
|       hideAddKeypairForm(); | ||||
|     } | ||||
|     if (e.key === 'Enter' && e.target === elements.keyNameInput && elements.keyNameInput.value.trim()) { | ||||
| @@ -331,16 +411,16 @@ async function checkExistingSession() { | ||||
|       // Session is active | ||||
|       currentKeyspace = response.session.keyspace; | ||||
|       elements.keyspaceInput.value = currentKeyspace; | ||||
|       setStatus(`Connected to ${currentKeyspace}`, true); | ||||
|       setStatus(currentKeyspace, true); | ||||
|       showSection('vaultSection'); | ||||
|       await loadKeypairs(); | ||||
|     } else { | ||||
|       // No active session | ||||
|       setStatus('Ready', false); | ||||
|       setStatus('', false); | ||||
|       showSection('authSection'); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     setStatus('Ready', false); | ||||
|     setStatus('', false); | ||||
|     showSection('authSection'); | ||||
|   } | ||||
| } | ||||
| @@ -370,34 +450,43 @@ function hideAddKeypairForm() { | ||||
|  | ||||
| // Tab functionality | ||||
| function initializeTabs() { | ||||
|   const tabButtons = document.querySelectorAll('.tab-btn'); | ||||
|   const tabContents = document.querySelectorAll('.tab-content'); | ||||
|   const tabContainer = document.querySelector('.operation-tabs'); | ||||
|  | ||||
|   tabButtons.forEach(button => { | ||||
|     button.addEventListener('click', () => { | ||||
|       const targetTab = button.getAttribute('data-tab'); | ||||
|  | ||||
|       // 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 | ||||
|       button.classList.add('active'); | ||||
|       document.getElementById(`${targetTab}-tab`).classList.add('active'); | ||||
|  | ||||
|       // Clear results when switching tabs | ||||
|       clearTabResults(); | ||||
|  | ||||
|       // Update button states | ||||
|       updateButtonStates(); | ||||
|   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) | ||||
| @@ -488,8 +577,6 @@ function clearVaultState() { | ||||
|   selectedKeypairId = null; | ||||
|   updateButtonStates(); | ||||
|  | ||||
|  | ||||
|  | ||||
|   // Hide add keypair form if open | ||||
|   hideAddKeypairForm(); | ||||
|  | ||||
| @@ -499,24 +586,31 @@ function clearVaultState() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function createKeyspace() { | ||||
| // Validation utilities | ||||
| const validateAuth = () => { | ||||
|   const keyspace = elements.keyspaceInput.value.trim(); | ||||
|   const password = elements.passwordInput.value.trim(); | ||||
|  | ||||
|   if (!keyspace || !password) { | ||||
|     showToast('Please enter keyspace name and password', 'error'); | ||||
|     return; | ||||
|   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', { keyspace, password }); | ||||
|  | ||||
|         if (response && response.success) { | ||||
|           // Clear any existing state before auto-login | ||||
|         const response = await sendMessage('createKeyspace', auth); | ||||
|         if (response?.success) { | ||||
|           clearVaultState(); | ||||
|           await login(); // Auto-login after creation | ||||
|           return response; | ||||
| @@ -531,32 +625,25 @@ async function createKeyspace() { | ||||
|       } | ||||
|     ); | ||||
|   } catch (error) { | ||||
|     console.error('Create keyspace error:', error); | ||||
|     handleError(error, 'Create keyspace'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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; | ||||
|   } | ||||
|   const auth = validateAuth(); | ||||
|   if (!auth) return; | ||||
|  | ||||
|   try { | ||||
|     await executeOperation( | ||||
|       async () => { | ||||
|         const response = await sendMessage('initSession', { keyspace, password }); | ||||
|  | ||||
|         if (response && response.success) { | ||||
|           currentKeyspace = keyspace; | ||||
|           setStatus(`Connected to ${keyspace}`, true); | ||||
|         const response = await sendMessage('initSession', auth); | ||||
|         if (response?.success) { | ||||
|           currentKeyspace = auth.keyspace; | ||||
|           setStatus(auth.keyspace, true); | ||||
|           showSection('vaultSection'); | ||||
|  | ||||
|           // Clear any previous vault state before loading new keyspace | ||||
|           clearVaultState(); | ||||
|           await loadKeypairs(); | ||||
|  | ||||
|           return response; | ||||
|         } else { | ||||
|           throw new Error(getResponseError(response, 'login')); | ||||
| @@ -569,7 +656,7 @@ async function login() { | ||||
|       } | ||||
|     ); | ||||
|   } catch (error) { | ||||
|     console.error('Login error:', error); | ||||
|     handleError(error, 'Create keyspace'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -578,7 +665,7 @@ async function lockSession() { | ||||
|     await sendMessage('lockSession'); | ||||
|     currentKeyspace = null; | ||||
|     selectedKeypairId = null; | ||||
|     setStatus('Locked', false); | ||||
|     setStatus('', false); | ||||
|     showSection('authSection'); | ||||
|  | ||||
|     // Clear all form inputs | ||||
| @@ -601,47 +688,35 @@ async function addKeypair() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     await executeOperation( | ||||
|       async () => { | ||||
|         const metadata = JSON.stringify({ name: keyName }); | ||||
|         const response = await sendMessage('addKeypair', { keyType, metadata }); | ||||
|   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!' | ||||
|       if (response?.success) { | ||||
|         hideAddKeypairForm(); | ||||
|         await loadKeypairs(); | ||||
|         return response; | ||||
|       } else { | ||||
|         throw new Error(getResponseError(response, 'add keypair')); | ||||
|       } | ||||
|     ); | ||||
|   } catch (error) { | ||||
|     // Error already handled by executeOperation | ||||
|   } | ||||
|     }, | ||||
|     { | ||||
|       loadingElement: elements.addKeypairBtn, | ||||
|       successMessage: 'Keypair added successfully!' | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| async function loadKeypairs() { | ||||
|   try { | ||||
|     const response = await sendMessage('listKeypairs'); | ||||
|   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 = '<div class="empty-state">Failed to load keypairs. Try refreshing.</div>'; | ||||
|       showToast(errorMsg, 'error'); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     const errorMsg = getErrorMessage(error, 'Failed to load keypairs'); | ||||
|     console.error('Error loading keypairs:', error); | ||||
|   if (response && response.success) { | ||||
|     renderKeypairs(response.keypairs); | ||||
|   } else { | ||||
|     const errorMsg = getResponseError(response, 'load keypairs'); | ||||
|     const container = elements.keypairsList; | ||||
|     container.innerHTML = '<div class="empty-state">Error loading keypairs. Try refreshing.</div>'; | ||||
|     container.innerHTML = '<div class="empty-state">Failed to load keypairs. Try refreshing.</div>'; | ||||
|     showToast(errorMsg, 'error'); | ||||
|   } | ||||
| } | ||||
| @@ -724,7 +799,6 @@ async function selectKeypair(keyId) { | ||||
|     } | ||||
|   } 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'); | ||||
| @@ -747,183 +821,114 @@ function updateKeypairSelection(selectedId) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function signMessage() { | ||||
|   const messageText = elements.messageInput.value.trim(); | ||||
|   if (!messageText || !selectedKeypairId) { | ||||
|     showToast('Please enter a message and select a keypair', 'error'); | ||||
| // Shared templates | ||||
| const copyIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|   <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | ||||
|   <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | ||||
| </svg>`; | ||||
|  | ||||
| const createResultContainer = (label, value, btnId) => ` | ||||
|   <label>${label}:</label> | ||||
|   <div class="signature-container"> | ||||
|     <code id="${value}Value">${value}</code> | ||||
|     <button id="${btnId}" class="btn-copy" title="Copy">${copyIcon}</button> | ||||
|   </div> | ||||
| `; | ||||
|  | ||||
| // Unified crypto operation handler | ||||
| async function performCryptoOperation(config) { | ||||
|   const { input, validation, action, resultElement, button, successMsg, resultProcessor } = config; | ||||
|  | ||||
|   if (!validation()) { | ||||
|     showToast(config.errorMsg, 'error'); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     await executeOperation( | ||||
|       async () => { | ||||
|         const messageBytes = stringToUint8Array(messageText); | ||||
|         const response = await sendMessage('sign', { message: messageBytes }); | ||||
|   await executeOperation( | ||||
|     async () => { | ||||
|       const response = await sendMessage(action, input()); | ||||
|       if (response?.success) { | ||||
|         resultElement.classList.remove('hidden'); | ||||
|         resultElement.innerHTML = resultProcessor(response); | ||||
|  | ||||
|         if (response?.success) { | ||||
|           elements.signatureResult.classList.remove('hidden'); | ||||
|           elements.signatureResult.innerHTML = ` | ||||
|             <label>Signature:</label> | ||||
|             <div class="signature-container"> | ||||
|               <code id="signatureValue">${response.signature}</code> | ||||
|               <button id="copySignatureBtn" class="btn-copy" title="Copy"> | ||||
|                 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|                   <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | ||||
|                   <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | ||||
|                 </svg> | ||||
|               </button> | ||||
|             </div> | ||||
|           `; | ||||
|  | ||||
|           document.getElementById('copySignatureBtn').addEventListener('click', () => { | ||||
|             copyToClipboard(response.signature); | ||||
|           }); | ||||
|  | ||||
|           return response; | ||||
|         } else { | ||||
|           throw new Error(getResponseError(response, 'sign message')); | ||||
|         // 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))); | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         loadingElement: elements.signBtn, | ||||
|         successMessage: 'Message signed successfully!' | ||||
|  | ||||
|         return response; | ||||
|       } else { | ||||
|         throw new Error(getResponseError(response, action)); | ||||
|       } | ||||
|     ); | ||||
|   } catch (error) { | ||||
|     elements.signatureResult.classList.add('hidden'); | ||||
|   } | ||||
|     }, | ||||
|     { loadingElement: button, successMessage: successMsg } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| async function encryptMessage() { | ||||
|   const messageText = elements.encryptMessageInput.value.trim(); | ||||
|   if (!messageText || !currentKeyspace) { | ||||
|     showToast('Please enter a message and ensure you are connected to a keyspace', 'error'); | ||||
|     return; | ||||
|   } | ||||
| // 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') | ||||
| }); | ||||
|  | ||||
|   try { | ||||
|     await executeOperation( | ||||
|       async () => { | ||||
|         const response = await sendMessage('encrypt', { message: messageText }); | ||||
| 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') | ||||
| }); | ||||
|  | ||||
|         if (response?.success) { | ||||
|           elements.encryptResult.classList.remove('hidden'); | ||||
|           elements.encryptResult.innerHTML = ` | ||||
|             <label>Encrypted Message:</label> | ||||
|             <div class="signature-container"> | ||||
|               <code id="encryptedValue">${response.encryptedMessage}</code> | ||||
|               <button id="copyEncryptedBtn" class="btn-copy" title="Copy"> | ||||
|                 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|                   <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | ||||
|                   <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | ||||
|                 </svg> | ||||
|               </button> | ||||
|             </div> | ||||
|           `; | ||||
| 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') | ||||
| }); | ||||
|  | ||||
|           document.getElementById('copyEncryptedBtn').addEventListener('click', () => { | ||||
|             copyToClipboard(response.encryptedMessage); | ||||
|           }); | ||||
|  | ||||
|           return response; | ||||
|         } else { | ||||
|           throw new Error(getResponseError(response, 'encrypt message')); | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         loadingElement: elements.encryptBtn, | ||||
|         successMessage: 'Message encrypted successfully!' | ||||
|       } | ||||
|     ); | ||||
|   } catch (error) { | ||||
|     elements.encryptResult.classList.add('hidden'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function decryptMessage() { | ||||
|   const encryptedText = elements.encryptedMessageInput.value.trim(); | ||||
|   if (!encryptedText || !currentKeyspace) { | ||||
|     showToast('Please enter encrypted message and ensure you are connected to a keyspace', 'error'); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     await executeOperation( | ||||
|       async () => { | ||||
|         const response = await sendMessage('decrypt', { encryptedMessage: encryptedText }); | ||||
|  | ||||
|         if (response?.success) { | ||||
|           elements.decryptResult.classList.remove('hidden'); | ||||
|           elements.decryptResult.innerHTML = ` | ||||
|             <label>Decrypted Message:</label> | ||||
|             <div class="signature-container"> | ||||
|               <code id="decryptedValue">${response.decryptedMessage}</code> | ||||
|               <button id="copyDecryptedBtn" class="btn-copy" title="Copy"> | ||||
|                 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|                   <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | ||||
|                   <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | ||||
|                 </svg> | ||||
|               </button> | ||||
|             </div> | ||||
|           `; | ||||
|  | ||||
|           document.getElementById('copyDecryptedBtn').addEventListener('click', () => { | ||||
|             copyToClipboard(response.decryptedMessage); | ||||
|           }); | ||||
|  | ||||
|           return response; | ||||
|         } else { | ||||
|           throw new Error(getResponseError(response, 'decrypt message')); | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         loadingElement: elements.decryptBtn, | ||||
|         successMessage: 'Message decrypted successfully!' | ||||
|       } | ||||
|     ); | ||||
|   } catch (error) { | ||||
|     elements.decryptResult.classList.add('hidden'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function verifySignature() { | ||||
|   const messageText = elements.verifyMessageInput.value.trim(); | ||||
|   const signature = elements.signatureToVerifyInput.value.trim(); | ||||
|   if (!messageText || !signature || !selectedKeypairId) { | ||||
|     showToast('Please enter message, signature, and select a keypair', 'error'); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     await executeOperation( | ||||
|       async () => { | ||||
|         const messageBytes = stringToUint8Array(messageText); | ||||
|         const response = await sendMessage('verify', { message: messageBytes, signature }); | ||||
|  | ||||
|         if (response?.success) { | ||||
|           const isValid = response.isValid; | ||||
|           const icon = isValid ? '✅' : '❌'; | ||||
|           const text = isValid ? 'Signature is valid' : 'Signature is invalid'; | ||||
|  | ||||
|           elements.verifyResult.classList.remove('hidden'); | ||||
|           elements.verifyResult.innerHTML = ` | ||||
|             <div class="verification-status ${isValid ? 'valid' : 'invalid'}"> | ||||
|               <span>${icon}</span> | ||||
| 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 | ||||
|       ? `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|            <polyline points="20,6 9,17 4,12"></polyline> | ||||
|          </svg>` | ||||
|       : `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||||
|            <circle cx="12" cy="12" r="10"></circle> | ||||
|            <line x1="15" y1="9" x2="9" y2="15"></line> | ||||
|            <line x1="9" y1="9" x2="15" y2="15"></line> | ||||
|          </svg>`; | ||||
|     const text = isValid ? 'Signature is valid' : 'Signature is invalid'; | ||||
|     return `<div class="verification-status ${isValid ? 'valid' : 'invalid'}"> | ||||
|               <span class="verification-icon">${icon}</span> | ||||
|               <span>${text}</span> | ||||
|             </div> | ||||
|           `; | ||||
|  | ||||
|           return response; | ||||
|         } else { | ||||
|           throw new Error(getResponseError(response, 'verify signature')); | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         loadingElement: elements.verifyBtn, | ||||
|         successMessage: null // No success message for verification | ||||
|       } | ||||
|     ); | ||||
|   } catch (error) { | ||||
|     elements.verifyResult.classList.add('hidden'); | ||||
|             </div>`; | ||||
|   } | ||||
| } | ||||
| }); | ||||
| @@ -34,7 +34,7 @@ | ||||
|   --accent-success: hsl(var(--accent-hue), 65%, 45%); | ||||
|   --accent-error: hsl(0, 70%, 55%); | ||||
|   --accent-warning: hsl(35, 85%, 55%); | ||||
|   --accent-info: hsl(var(--secondary-hue), 70%, 55%); | ||||
|   --accent-info: hsl(var(--primary-hue), 35%, 60%); | ||||
|  | ||||
|   /* Spacing system */ | ||||
|   --spacing-xs: 4px; | ||||
| @@ -75,66 +75,63 @@ | ||||
|   --accent-success: hsl(var(--accent-hue), 60%, 55%); | ||||
|   --accent-error: hsl(0, 65%, 60%); | ||||
|   --accent-warning: hsl(35, 80%, 60%); | ||||
|   --accent-info: hsl(var(--secondary-hue), 65%, 60%); | ||||
|   --accent-info: hsl(var(--primary-hue), 30%, 70%); | ||||
| } | ||||
|  | ||||
| /* Harmonious button styling system */ | ||||
| .btn-primary { | ||||
|   background: var(--bg-button-primary); | ||||
|   color: var(--text-button-primary); | ||||
| /* Consolidated button styling system */ | ||||
| .btn-primary, .btn-secondary, .btn-ghost { | ||||
|   border: none; | ||||
|   font-weight: 600; | ||||
|   box-shadow: var(--shadow-button); | ||||
|   font-weight: 500; | ||||
|   transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | ||||
| } | ||||
|  | ||||
| .btn-primary:hover { | ||||
|   background: hsl(var(--primary-hue), var(--primary-saturation), 50%); | ||||
|   box-shadow: var(--shadow-button-hover); | ||||
|   transform: translateY(-1px); | ||||
| } | ||||
|  | ||||
| .btn-primary:active { | ||||
|   transform: translateY(0); | ||||
| .btn-primary { | ||||
|   background: var(--bg-button-primary); | ||||
|   color: var(--text-button-primary); | ||||
|   font-weight: 600; | ||||
|   box-shadow: var(--shadow-button); | ||||
| } | ||||
|  | ||||
| /* Dark theme primary button adjustments */ | ||||
| [data-theme="dark"] .btn-primary:hover { | ||||
|   background: hsl(var(--primary-hue), var(--primary-saturation), 65%); | ||||
| } | ||||
|  | ||||
| .btn-secondary { | ||||
|   background: var(--bg-button-secondary); | ||||
|   color: var(--text-button-secondary); | ||||
|   border: 1px solid var(--border-color); | ||||
|   font-weight: 500; | ||||
|   transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | ||||
| } | ||||
|  | ||||
| .btn-secondary:hover { | ||||
|   background: var(--bg-input); | ||||
|   border-color: var(--border-focus); | ||||
|   color: var(--text-primary); | ||||
|   transform: translateY(-1px); | ||||
| } | ||||
|  | ||||
| .btn-secondary:active { | ||||
|   transform: translateY(0); | ||||
| } | ||||
|  | ||||
| .btn-ghost { | ||||
|   background: var(--bg-button-ghost); | ||||
|   color: var(--border-focus); | ||||
|   border: 1px solid transparent; | ||||
|   font-weight: 500; | ||||
|   transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | ||||
| } | ||||
|  | ||||
| .btn-primary:hover, .btn-secondary:hover, .btn-ghost:hover { | ||||
|   transform: translateY(-1px); | ||||
| } | ||||
|  | ||||
| .btn-primary:active, .btn-secondary:active, .btn-ghost:active { | ||||
|   transform: translateY(0); | ||||
| } | ||||
|  | ||||
| .btn-primary:hover { | ||||
|   background: hsl(var(--primary-hue), var(--primary-saturation), 50%); | ||||
|   box-shadow: var(--shadow-button-hover); | ||||
| } | ||||
|  | ||||
| .btn-secondary:hover, .btn-ghost:hover { | ||||
|   background: var(--bg-input); | ||||
|   color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| .btn-secondary:hover { | ||||
|   border-color: var(--border-focus); | ||||
| } | ||||
|  | ||||
| .btn-ghost:hover { | ||||
|   background: var(--bg-input); | ||||
|   border-color: var(--border-color); | ||||
|   color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .btn-primary:hover { | ||||
|   background: hsl(var(--primary-hue), var(--primary-saturation), 65%); | ||||
| } | ||||
|  | ||||
| * { | ||||
| @@ -197,6 +194,73 @@ body { | ||||
|   gap: var(--spacing-md); | ||||
| } | ||||
|  | ||||
| .settings-container { | ||||
|   position: relative; | ||||
| } | ||||
|  | ||||
| .settings-dropdown { | ||||
|   position: absolute; | ||||
|   top: 100%; | ||||
|   right: 0; | ||||
|   margin-top: var(--spacing-sm); | ||||
|   background: var(--bg-card); | ||||
|   border: 1px solid var(--border-color); | ||||
|   border-radius: 12px; | ||||
|   padding: var(--spacing-lg); | ||||
|   box-shadow: var(--shadow-card); | ||||
|   backdrop-filter: blur(20px); | ||||
|   min-width: 200px; | ||||
|   z-index: 1000; | ||||
| } | ||||
|  | ||||
| .settings-dropdown.hidden { | ||||
|   display: none; | ||||
| } | ||||
|  | ||||
| .settings-item { | ||||
|   margin-bottom: var(--spacing-md); | ||||
| } | ||||
|  | ||||
| .settings-item:last-child { | ||||
|   margin-bottom: 0; | ||||
| } | ||||
|  | ||||
| .settings-item label { | ||||
|   display: block; | ||||
|   font-size: 14px; | ||||
|   font-weight: 500; | ||||
|   color: var(--text-secondary); | ||||
|   margin-bottom: var(--spacing-xs); | ||||
| } | ||||
|  | ||||
| .timeout-input-group { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: var(--spacing-sm); | ||||
| } | ||||
|  | ||||
| .timeout-input-group input { | ||||
|   width: 60px; | ||||
|   padding: var(--spacing-xs) var(--spacing-sm); | ||||
|   font-size: 14px; | ||||
|   border: 1px solid var(--border-color); | ||||
|   border-radius: 8px; | ||||
|   background: var(--bg-input); | ||||
|   color: var(--text-primary); | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .timeout-input-group input:focus { | ||||
|   outline: none; | ||||
|   border-color: var(--border-focus); | ||||
|   box-shadow: 0 0 0 2px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.15); | ||||
| } | ||||
|  | ||||
| .timeout-input-group span { | ||||
|   font-size: 14px; | ||||
|   color: var(--text-muted); | ||||
| } | ||||
|  | ||||
| .btn-icon-only { | ||||
|   background: var(--bg-button-ghost); | ||||
|   border: none; | ||||
| @@ -224,39 +288,15 @@ body { | ||||
|   gap: var(--spacing-md); | ||||
| } | ||||
|  | ||||
| .status-indicator .status-content { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: var(--spacing-sm); | ||||
| } | ||||
|  | ||||
| .status-dot { | ||||
|   width: 10px; | ||||
|   height: 10px; | ||||
|   border-radius: 50%; | ||||
|   background: var(--accent-warning); | ||||
|   box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2); | ||||
| } | ||||
|  | ||||
| .status-indicator.connected .status-dot { | ||||
|   background: var(--accent-success); | ||||
|   box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); | ||||
| } | ||||
|  | ||||
| #statusText { | ||||
|   font-size: 14px; | ||||
|   font-weight: 500; | ||||
|   font-size: 22px; | ||||
|   font-weight: 700; | ||||
|   color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| /* Vault status specific styling */ | ||||
| .vault-status .status-indicator { | ||||
|   color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| .vault-status #statusText { | ||||
|   color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| /* Enhanced lock button styling */ | ||||
| #lockBtn { | ||||
| @@ -515,7 +555,7 @@ input::placeholder, textarea::placeholder { | ||||
|  | ||||
| .vault-header h2 { | ||||
|   color: var(--text-primary); | ||||
|   font-size: 20px; | ||||
|   font-size: 18px; | ||||
|   font-weight: 600; | ||||
| } | ||||
|  | ||||
| @@ -749,31 +789,6 @@ input::placeholder, textarea::placeholder { | ||||
|   color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| /* Signature Result */ | ||||
| .signature-result { | ||||
|   margin-top: 16px; | ||||
|   padding: 16px; | ||||
|   background: hsla(var(--accent-hue), 60%, 95%, 0.8); | ||||
|   border-radius: 12px; | ||||
|   border: 1px solid hsla(var(--accent-hue), 50%, 70%, 0.3); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .signature-result { | ||||
|   background: hsla(var(--accent-hue), 40%, 15%, 0.6); | ||||
|   border-color: hsla(var(--accent-hue), 50%, 40%, 0.4); | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| .signature-result label { | ||||
|   color: var(--accent-success); | ||||
|   font-weight: 500; | ||||
|   margin-bottom: 8px; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| /* Enhanced Toast Notifications */ | ||||
| .toast-notification { | ||||
|   position: fixed; | ||||
| @@ -828,31 +843,7 @@ input::placeholder, textarea::placeholder { | ||||
|   word-wrap: break-word; | ||||
| } | ||||
|  | ||||
| .toast-close { | ||||
|   background: none; | ||||
|   border: none; | ||||
|   color: inherit; | ||||
|   cursor: pointer; | ||||
|   font-size: 18px; | ||||
|   font-weight: bold; | ||||
|   line-height: 1; | ||||
|   opacity: 0.7; | ||||
|   padding: 0; | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   border-radius: 4px; | ||||
|   transition: all 0.2s ease; | ||||
|   flex-shrink: 0; | ||||
|   margin-top: 2px; | ||||
| } | ||||
|  | ||||
| .toast-close:hover { | ||||
|   opacity: 1; | ||||
|   background: rgba(255, 255, 255, 0.1); | ||||
| } | ||||
|  | ||||
| /* Success Toast */ | ||||
| .toast-success { | ||||
| @@ -880,9 +871,9 @@ input::placeholder, textarea::placeholder { | ||||
|  | ||||
| /* Info Toast */ | ||||
| .toast-info { | ||||
|   background: linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(37, 99, 235, 0.95) 100%); | ||||
|   background: var(--accent-info); | ||||
|   color: white; | ||||
|   border-color: rgba(59, 130, 246, 0.3); | ||||
|   border-color: hsla(var(--primary-hue), 35%, 60%, 0.3); | ||||
| } | ||||
|  | ||||
| .toast-info .toast-icon { | ||||
| @@ -981,10 +972,7 @@ input::placeholder, textarea::placeholder { | ||||
|   color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .tab-btn:hover { | ||||
|   color: var(--border-focus); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .tab-btn:hover, | ||||
| [data-theme="dark"] .tab-btn.active { | ||||
|   color: var(--border-focus); | ||||
| } | ||||
| @@ -997,8 +985,8 @@ input::placeholder, textarea::placeholder { | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| /* Result Styling */ | ||||
| .encrypt-result, .decrypt-result, .verify-result { | ||||
| /* Consolidated result styling */ | ||||
| .encrypt-result, .decrypt-result, .verify-result, .signature-result { | ||||
|   margin-top: 16px; | ||||
|   padding: 16px; | ||||
|   border-radius: 12px; | ||||
| @@ -1006,55 +994,55 @@ input::placeholder, textarea::placeholder { | ||||
|   transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | ||||
| } | ||||
|  | ||||
| .encrypt-result { | ||||
| .encrypt-result, .signature-result { | ||||
|   background: hsla(var(--accent-hue), 60%, 95%, 0.8); | ||||
|   border-color: hsla(var(--accent-hue), 50%, 70%, 0.3); | ||||
|   box-shadow: 0 2px 12px hsla(var(--accent-hue), 50%, 50%, 0.1); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .encrypt-result { | ||||
|   background: hsla(var(--accent-hue), 40%, 15%, 0.6); | ||||
|   border-color: hsla(var(--accent-hue), 50%, 40%, 0.4); | ||||
| } | ||||
|  | ||||
| .encrypt-result label { | ||||
|   color: var(--accent-success); | ||||
|   font-weight: 600; | ||||
|   margin-bottom: 12px; | ||||
|   display: block; | ||||
|   font-size: 14px; | ||||
| } | ||||
|  | ||||
| .decrypt-result { | ||||
|   background: hsla(var(--secondary-hue), 60%, 95%, 0.8); | ||||
|   border-color: hsla(var(--secondary-hue), 50%, 70%, 0.3); | ||||
|   box-shadow: 0 2px 12px hsla(var(--secondary-hue), 50%, 50%, 0.1); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .decrypt-result { | ||||
|   background: hsla(var(--secondary-hue), 40%, 15%, 0.6); | ||||
|   border-color: hsla(var(--secondary-hue), 50%, 40%, 0.4); | ||||
| } | ||||
|  | ||||
| .decrypt-result label { | ||||
|   color: var(--accent-info); | ||||
|   font-weight: 600; | ||||
|   margin-bottom: 12px; | ||||
|   display: block; | ||||
|   font-size: 14px; | ||||
| } | ||||
|  | ||||
| .verify-result { | ||||
|   background: hsla(var(--primary-hue), 30%, 95%, 0.8); | ||||
|   border-color: hsla(var(--primary-hue), 40%, 70%, 0.3); | ||||
|   box-shadow: 0 2px 12px hsla(var(--primary-hue), 40%, 50%, 0.1); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .encrypt-result, | ||||
| [data-theme="dark"] .signature-result { | ||||
|   background: hsla(var(--accent-hue), 40%, 15%, 0.6); | ||||
|   border-color: hsla(var(--accent-hue), 50%, 40%, 0.4); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .decrypt-result { | ||||
|   background: hsla(var(--secondary-hue), 40%, 15%, 0.6); | ||||
|   border-color: hsla(var(--secondary-hue), 50%, 40%, 0.4); | ||||
| } | ||||
|  | ||||
| [data-theme="dark"] .verify-result { | ||||
|   background: hsla(var(--primary-hue), 20%, 15%, 0.6); | ||||
|   border-color: hsla(var(--primary-hue), 30%, 40%, 0.4); | ||||
| } | ||||
|  | ||||
| .encrypt-result label, .decrypt-result label, .signature-result label { | ||||
|   font-weight: 600; | ||||
|   margin-bottom: 12px; | ||||
|   display: block; | ||||
|   font-size: 14px; | ||||
| } | ||||
|  | ||||
| .encrypt-result label, .signature-result label { | ||||
|   color: var(--accent-success); | ||||
| } | ||||
|  | ||||
| .decrypt-result label { | ||||
|   color: var(--accent-info); | ||||
| } | ||||
|  | ||||
| .verification-status { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| @@ -1071,19 +1059,14 @@ input::placeholder, textarea::placeholder { | ||||
|   color: var(--accent-error); | ||||
| } | ||||
|  | ||||
| .verification-icon { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   flex-shrink: 0; | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| .verification-icon svg { | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
| } | ||||