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