// 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 = `
`;
}
// 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