sal-modular/crypto_vault_extension/popup.js

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);
}
}