`;
}).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 = ``;
const createResultContainer = (label, value, btnId) => `
${value}
`;
// 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
? ``
: ``;
const text = isValid ? 'Signature is valid' : 'Signature is invalid';
return `
${icon}${text}
`;
}
});
// 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 = `
`;
} else {
// Show pending status and unlock message when locked
statusIndicator = '