Compare commits

...

2 Commits

Author SHA1 Message Date
Sameh Abouel-saad
1d3d0a4fa4 remove ts extension 2025-06-17 16:31:57 +03:00
zaelgohary
4f3f98a954 feat: Implement SigSocket request queuing and approval system, Enhance Settings UI 2025-06-17 03:18:25 +03:00
54 changed files with 1567 additions and 9175 deletions

View File

@@ -29,7 +29,21 @@ function startSessionTimeout() {
if (vault && currentSession) { if (vault && currentSession) {
// Lock the session // Lock the session
vault.lock_session(); vault.lock_session();
// Keep the session info for SigSocket connection but mark it as timed out
const keyspace = currentSession.keyspace;
await sessionManager.clear(); await sessionManager.clear();
// Maintain SigSocket connection for the locked keyspace to receive pending requests
if (sigSocketService && keyspace) {
try {
// Keep SigSocket connected to receive requests even when locked
console.log(`🔒 Session timed out but maintaining SigSocket connection for: ${keyspace}`);
} catch (error) {
console.warn('Failed to maintain SigSocket connection after timeout:', error);
}
}
// Notify popup if it's open // Notify popup if it's open
if (popupPort) { if (popupPort) {
popupPort.postMessage({ popupPort.postMessage({
@@ -130,12 +144,48 @@ async function restoreSession() {
if (isUnlocked) { if (isUnlocked) {
// Restart keep-alive for restored session // Restart keep-alive for restored session
startKeepAlive(); startKeepAlive();
// Connect to SigSocket for the restored session
if (sigSocketService) {
try {
const connected = await sigSocketService.connectToServer(session.keyspace);
if (connected) {
console.log(`🔗 SigSocket reconnected for restored workspace: ${session.keyspace}`);
}
} catch (error) {
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
if (error.message && error.message.includes('Workspace not found')) {
console.log(` SigSocket connection skipped for restored session: No workspace available yet`);
} else {
console.warn('Failed to reconnect SigSocket for restored session:', error);
}
}
}
return session; return session;
} else { } else {
await sessionManager.clear(); // Session exists but is locked - still try to connect SigSocket to receive pending requests
if (sigSocketService && session.keyspace) {
try {
const connected = await sigSocketService.connectToServer(session.keyspace);
if (connected) {
console.log(`🔗 SigSocket connected for locked workspace: ${session.keyspace} (will queue requests)`);
}
} catch (error) {
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
if (error.message && error.message.includes('Workspace not found')) {
console.log(` SigSocket connection skipped for locked session: No workspace available yet`);
} else {
console.warn('Failed to connect SigSocket for locked session:', error);
}
}
}
// Don't clear the session - keep it for SigSocket connection
// await sessionManager.clear();
} }
} }
return null; return session; // Return session even if locked, so we know which keyspace to use
} }
// Import WASM module functions and SigSocket service // Import WASM module functions and SigSocket service
@@ -187,14 +237,31 @@ const messageHandlers = {
// Smart auto-connect to SigSocket when session is initialized // Smart auto-connect to SigSocket when session is initialized
if (sigSocketService) { if (sigSocketService) {
try { try {
console.log(`🔗 Initializing SigSocket connection for workspace: ${request.keyspace}`);
// This will reuse existing connection if same workspace, or switch if different // This will reuse existing connection if same workspace, or switch if different
const connected = await sigSocketService.connectToServer(request.keyspace); const connected = await sigSocketService.connectToServer(request.keyspace);
if (connected) { if (connected) {
console.log(`🔗 SigSocket ready for workspace: ${request.keyspace}`); console.log(` SigSocket ready for workspace: ${request.keyspace}`);
} else {
console.warn(`⚠️ SigSocket connection failed for workspace: ${request.keyspace}`);
} }
} catch (error) { } catch (error) {
console.warn('Failed to auto-connect to SigSocket:', error); console.warn('Failed to auto-connect to SigSocket:', error);
// If connection fails, try once more after a short delay
setTimeout(async () => {
try {
console.log(`🔄 Retrying SigSocket connection for workspace: ${request.keyspace}`);
await sigSocketService.connectToServer(request.keyspace);
} catch (retryError) {
console.warn('SigSocket retry connection also failed:', retryError);
}
}, 2000);
} }
// Notify SigSocket service that keyspace is now unlocked
await sigSocketService.onKeypaceUnlocked();
} }
return { success: true }; return { success: true };
@@ -288,6 +355,19 @@ const messageHandlers = {
return { success: true }; return { success: true };
}, },
updateSigSocketUrl: async (request) => {
if (sigSocketService) {
// Update the server URL in the SigSocket service
sigSocketService.defaultServerUrl = request.serverUrl;
// Save to storage (already done in popup, but ensure consistency)
await chrome.storage.local.set({ sigSocketUrl: request.serverUrl });
console.log(`🔗 SigSocket server URL updated to: ${request.serverUrl}`);
}
return { success: true };
},
// SigSocket handlers // SigSocket handlers
connectSigSocket: async (request) => { connectSigSocket: async (request) => {
if (!sigSocketService) { if (!sigSocketService) {
@@ -313,6 +393,15 @@ const messageHandlers = {
return { success: true, status }; return { success: true, status };
}, },
getSigSocketStatusWithTest: async () => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
// Use the enhanced connection testing method
const status = await sigSocketService.getStatusWithConnectionTest();
return { success: true, status };
},
getPendingSignRequests: async () => { getPendingSignRequests: async () => {
if (!sigSocketService) { if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' }; return { success: false, error: 'SigSocket service not initialized' };
@@ -393,6 +482,25 @@ chrome.runtime.onConnect.addListener((port) => {
startKeepAlive(); startKeepAlive();
} }
// Handle messages from popup
port.onMessage.addListener(async (message) => {
if (message.type === 'REQUEST_IMMEDIATE_STATUS') {
// Immediately send current SigSocket status to popup
if (sigSocketService) {
try {
const status = await sigSocketService.getStatus();
port.postMessage({
type: 'CONNECTION_STATUS_CHANGED',
status: status
});
console.log('📡 Sent immediate status to popup:', status.isConnected ? 'Connected' : 'Disconnected');
} catch (error) {
console.warn('Failed to send immediate status:', error);
}
}
}
});
port.onDisconnect.addListener(() => { port.onDisconnect.addListener(() => {
// Popup closed, clear reference and stop keep-alive // Popup closed, clear reference and stop keep-alive
popupPort = null; popupPort = null;
@@ -405,3 +513,158 @@ chrome.runtime.onConnect.addListener((port) => {
}); });
} }
}); });
// Handle notification clicks to open extension (notifications are now clickable without buttons)
chrome.notifications.onClicked.addListener(async (notificationId) => {
console.log(`🔔 Notification clicked: ${notificationId}`);
// Check if this is a SigSocket notification
if (notificationId.startsWith('sigsocket-request-')) {
console.log('🔔 SigSocket notification clicked, opening extension...');
try {
await openExtensionPopup();
// Clear the notification after successfully opening
chrome.notifications.clear(notificationId);
console.log('✅ Notification cleared after opening extension');
} catch (error) {
console.error('❌ Failed to handle notification click:', error);
}
} else {
console.log('🔔 Non-SigSocket notification clicked, ignoring');
}
});
// Note: Notification button handler removed - notifications are now clickable without buttons
// Function to open extension popup with best UX
async function openExtensionPopup() {
try {
console.log('🔔 Opening extension popup from notification...');
// First, check if there's already a popup window open
const windows = await chrome.windows.getAll({ populate: true });
const existingPopup = windows.find(window =>
window.type === 'popup' &&
window.tabs?.some(tab => tab.url?.includes('popup.html'))
);
if (existingPopup) {
// Focus existing popup and send focus message
await chrome.windows.update(existingPopup.id, { focused: true });
console.log('✅ Focused existing popup window');
// Send message to focus on SigSocket section
if (popupPort) {
popupPort.postMessage({
type: 'FOCUS_SIGSOCKET',
fromNotification: true
});
}
return;
}
// Best UX: Try to use the normal popup experience
// The action API gives the same popup as clicking the extension icon
try {
if (chrome.action && chrome.action.openPopup) {
await chrome.action.openPopup();
console.log('✅ Extension popup opened via action API (best UX - normal popup)');
// Send focus message after popup opens
setTimeout(() => {
if (popupPort) {
popupPort.postMessage({
type: 'FOCUS_SIGSOCKET',
fromNotification: true
});
}
}, 200);
return;
}
} catch (actionError) {
// The action API fails when there's no active browser window
// This is common when all browser windows are closed but extension is still running
console.log('⚠️ Action API failed (likely no active window):', actionError.message);
// Check if we have any normal browser windows
const allWindows = await chrome.windows.getAll();
const normalWindows = allWindows.filter(w => w.type === 'normal');
if (normalWindows.length > 0) {
// We have browser windows, try to focus one and retry action API
try {
const targetWindow = normalWindows.find(w => w.focused) || normalWindows[0];
await chrome.windows.update(targetWindow.id, { focused: true });
// Small delay and retry
await new Promise(resolve => setTimeout(resolve, 100));
await chrome.action.openPopup();
console.log('✅ Extension popup opened via action API after focusing window');
setTimeout(() => {
if (popupPort) {
popupPort.postMessage({
type: 'FOCUS_SIGSOCKET',
fromNotification: true
});
}
}, 200);
return;
} catch (retryError) {
console.log('⚠️ Action API retry also failed:', retryError.message);
}
}
}
// If action API fails completely, we need to create a window
// But let's make it as close to the normal popup experience as possible
console.log('⚠️ Creating popup window as fallback (action API unavailable)');
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
// Position the popup where the extension icon would normally show its popup
// Try to position it in the top-right area like a normal extension popup
let left = screen.width - 420; // 400px width + 20px margin
let top = 80; // Below browser toolbar area
try {
// If we have a browser window, position relative to it
const allWindows = await chrome.windows.getAll();
const normalWindows = allWindows.filter(w => w.type === 'normal');
if (normalWindows.length > 0) {
const referenceWindow = normalWindows[0];
left = (referenceWindow.left || 0) + (referenceWindow.width || 800) - 420;
top = (referenceWindow.top || 0) + 80;
}
} catch (positionError) {
console.log('⚠️ Could not get window position, using screen-based positioning');
}
const newWindow = await chrome.windows.create({
url: popupUrl,
type: 'popup',
width: 400,
height: 600,
left: Math.max(0, left),
top: Math.max(0, top),
focused: true
});
console.log(`✅ Extension popup window created: ${newWindow.id}`);
} catch (error) {
console.error('❌ Failed to open extension popup:', error);
// Final fallback: open in new tab (least ideal but still functional)
try {
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
await chrome.tabs.create({ url: popupUrl, active: true });
console.log('✅ Opened extension in new tab as final fallback');
} catch (tabError) {
console.error('❌ All popup opening methods failed:', tabError);
}
}
}

View File

@@ -25,6 +25,10 @@ class SigSocketService {
// UI communication // UI communication
this.popupPort = null; this.popupPort = null;
// Status monitoring
this.statusMonitorInterval = null;
this.lastKnownConnectionState = false;
} }
/** /**
@@ -44,72 +48,221 @@ class SigSocketService {
console.warn('Failed to load SigSocket URL from storage:', error); console.warn('Failed to load SigSocket URL from storage:', error);
} }
// Restore any persisted pending requests
await this.restorePendingRequests();
console.log('🔌 SigSocket service initialized with WASM APIs'); console.log('🔌 SigSocket service initialized with WASM APIs');
} }
/**
* Restore pending requests from persistent storage
* Only restore requests that match the current workspace
*/
async restorePendingRequests() {
try {
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
if (result.sigSocketPendingRequests && Array.isArray(result.sigSocketPendingRequests)) {
console.log(`🔄 Found ${result.sigSocketPendingRequests.length} stored requests`);
// Filter requests for current workspace only
const currentWorkspaceRequests = result.sigSocketPendingRequests.filter(request =>
request.target_public_key === this.connectedPublicKey
);
console.log(`🔄 Restoring ${currentWorkspaceRequests.length} requests for current workspace`);
// Add each workspace-specific request back to WASM storage
for (const request of currentWorkspaceRequests) {
try {
await this.wasmModule.SigSocketManager.add_pending_request(JSON.stringify(request.request || request));
console.log(`✅ Restored request: ${request.id || request.request?.id}`);
} catch (error) {
console.warn(`Failed to restore request ${request.id || request.request?.id}:`, error);
}
}
// Update badge after restoration
this.updateBadge();
}
} catch (error) {
console.warn('Failed to restore pending requests:', error);
}
}
/**
* Persist pending requests to storage with workspace isolation
*/
async persistPendingRequests() {
try {
const requests = await this.getFilteredRequests();
// Get existing storage to merge with other workspaces
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
const existingRequests = result.sigSocketPendingRequests || [];
// Remove old requests for current workspace
const otherWorkspaceRequests = existingRequests.filter(request =>
request.target_public_key !== this.connectedPublicKey
);
// Combine with current workspace requests
const allRequests = [...otherWorkspaceRequests, ...requests];
await chrome.storage.local.set({ sigSocketPendingRequests: allRequests });
console.log(`💾 Persisted ${requests.length} requests for current workspace (${allRequests.length} total)`);
} catch (error) {
console.warn('Failed to persist pending requests:', error);
}
}
/** /**
* Connect to SigSocket server using WASM APIs * Connect to SigSocket server using WASM APIs
* WASM handles all connection logic (reuse, switching, etc.) * WASM handles all connection logic (reuse, switching, etc.)
* @param {string} workspaceId - The workspace/keyspace identifier * @param {string} workspaceId - The workspace/keyspace identifier
* @param {number} retryCount - Number of retry attempts (default: 3)
* @returns {Promise<boolean>} - True if connected successfully * @returns {Promise<boolean>} - True if connected successfully
*/ */
async connectToServer(workspaceId) { async connectToServer(workspaceId, retryCount = 3) {
try { for (let attempt = 1; attempt <= retryCount; attempt++) {
if (!this.wasmModule?.SigSocketManager) { try {
throw new Error('WASM SigSocketManager not available'); if (!this.wasmModule?.SigSocketManager) {
throw new Error('WASM SigSocketManager not available');
}
console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId} (attempt ${attempt}/${retryCount})`);
// Clean workspace switching
if (this.currentWorkspace && this.currentWorkspace !== workspaceId) {
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${workspaceId}`);
await this.cleanWorkspaceSwitch(workspaceId);
// Small delay to ensure clean state transition
await new Promise(resolve => setTimeout(resolve, 300));
}
// Let WASM handle all connection logic (reuse, switching, etc.)
const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events(
workspaceId,
this.defaultServerUrl,
(event) => this.handleSigSocketEvent(event)
);
// Parse connection info
const info = JSON.parse(connectionInfo);
this.currentWorkspace = workspaceId; // Use the parameter we passed, not WASM response
this.connectedPublicKey = info.public_key;
this.isConnected = info.is_connected;
console.log(`✅ SigSocket connection result:`, {
workspace: this.currentWorkspace,
publicKey: this.connectedPublicKey?.substring(0, 16) + '...',
connected: this.isConnected,
serverUrl: this.defaultServerUrl
});
// Validate that we have a public key if connected
if (this.isConnected && !this.connectedPublicKey) {
console.warn('⚠️ Connected but no public key received - this may cause request issues');
}
// Update badge to show current state
this.updateBadge();
if (this.isConnected) {
// Clean flow: Connect -> Restore workspace requests -> Update UI
console.log(`🔗 Connected to workspace: ${workspaceId}, restoring pending requests...`);
// 1. Restore requests for this specific workspace
await this.restorePendingRequests();
// 2. Update badge with current count
this.updateBadge();
console.log(`✅ Workspace ${workspaceId} ready with restored requests`);
return true;
}
// If not connected but no error, try again
if (attempt < retryCount) {
console.log(`⏳ Connection not established, retrying in 1 second...`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
// Check if this is an expected "no workspace" error during startup
const isExpectedStartupError = error.message &&
(error.message.includes('Workspace not found') ||
error.message.includes('no keypairs available'));
if (isExpectedStartupError && attempt === 1) {
console.log(`⏳ SigSocket connection attempt ${attempt}: No active workspace (expected after extension reload)`);
}
// Check if this is a public key related error
if (error.message && error.message.includes('public key')) {
console.error(`🔑 Public key error detected: ${error.message}`);
// For public key errors, don't retry immediately - might need workspace change
if (attempt === 1) {
console.log(`🔄 Public key error on first attempt, trying to disconnect and reconnect...`);
await this.disconnect();
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
if (attempt < retryCount) {
if (!isExpectedStartupError) {
console.log(`⏳ Retrying connection in 2 seconds...`);
}
await new Promise(resolve => setTimeout(resolve, 2000));
} else {
// Final attempt failed
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
if (isExpectedStartupError) {
console.log(` SigSocket connection failed: No active workspace. Will connect when user logs in.`);
}
}
} }
console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId}`);
// Let WASM handle all connection logic (reuse, switching, etc.)
const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events(
workspaceId,
this.defaultServerUrl,
(event) => this.handleSigSocketEvent(event)
);
// Parse connection info
const info = JSON.parse(connectionInfo);
this.currentWorkspace = info.workspace;
this.connectedPublicKey = info.public_key;
this.isConnected = info.is_connected;
console.log(`✅ SigSocket connection result:`, {
workspace: this.currentWorkspace,
publicKey: this.connectedPublicKey?.substring(0, 16) + '...',
connected: this.isConnected
});
// Update badge to show current state
this.updateBadge();
return this.isConnected;
} catch (error) {
console.error('❌ SigSocket connection failed:', error);
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
return false;
} }
return false;
} }
/** /**
* Handle events from the WASM SigSocket client * Handle events from the WASM SigSocket client
* This is called automatically when requests arrive * This is called automatically when requests arrive
* @param {Object} event - Event from WASM layer * @param {Object} event - Event from WASM layer
*/ */
handleSigSocketEvent(event) { async handleSigSocketEvent(event) {
console.log('📨 Received SigSocket event:', event); console.log('📨 Received SigSocket event:', event);
if (event.type === 'sign_request') { if (event.type === 'sign_request') {
console.log(`🔐 New sign request: ${event.request_id}`); console.log(`🔐 New sign request: ${event.request_id} for workspace: ${this.currentWorkspace}`);
// The request is automatically stored by WASM // Clean flow: Request arrives -> Store -> Persist -> Update UI
// We just handle UI updates try {
this.showSignRequestNotification(); // 1. Request is automatically stored in WASM (already done by WASM layer)
this.updateBadge();
this.notifyPopupOfNewRequest(); // 2. Persist to storage with workspace isolation
await this.persistPendingRequests();
// 3. Update badge count
this.updateBadge();
// 4. Show notification
this.showSignRequestNotification();
// 5. Notify popup if connected
this.notifyPopupOfNewRequest();
console.log(`✅ Request ${event.request_id} processed and stored for workspace: ${this.currentWorkspace}`);
} catch (error) {
console.error(`❌ Failed to process request ${event.request_id}:`, error);
}
} }
} }
@@ -124,6 +277,19 @@ class SigSocketService {
throw new Error('WASM SigSocketManager not available'); throw new Error('WASM SigSocketManager not available');
} }
// Check if we're connected before attempting approval
if (!this.isConnected) {
console.warn(`⚠️ Not connected to SigSocket server, cannot approve request: ${requestId}`);
throw new Error('Not connected to SigSocket server');
}
// Verify we can approve this request
const canApprove = await this.canApproveRequest(requestId);
if (!canApprove) {
console.warn(`⚠️ Cannot approve request ${requestId} - keyspace may be locked or request not found`);
throw new Error('Cannot approve request - keyspace may be locked or request not found');
}
console.log(`✅ Approving request: ${requestId}`); console.log(`✅ Approving request: ${requestId}`);
// WASM handles all validation, signing, and server communication // WASM handles all validation, signing, and server communication
@@ -131,14 +297,37 @@ class SigSocketService {
console.log(`🎉 Request approved successfully: ${requestId}`); console.log(`🎉 Request approved successfully: ${requestId}`);
// Update UI // Clean flow: Approve -> Remove from storage -> Update UI
// 1. Remove from persistent storage (WASM already removed it)
await this.persistPendingRequests();
// 2. Update badge count
this.updateBadge(); this.updateBadge();
// 3. Notify popup of updated state
this.notifyPopupOfRequestUpdate(); this.notifyPopupOfRequestUpdate();
console.log(`✅ Request ${requestId} approved and cleaned up`);
return true; return true;
} catch (error) { } catch (error) {
console.error(`❌ Failed to approve request ${requestId}:`, error); console.error(`❌ Failed to approve request ${requestId}:`, error);
// Check if this is a connection-related error
if (error.message && (error.message.includes('Connection not found') || error.message.includes('public key'))) {
console.error(`🔑 Connection/public key error during approval. Current state:`, {
connected: this.isConnected,
workspace: this.currentWorkspace,
publicKey: this.connectedPublicKey?.substring(0, 16) + '...'
});
// Try to reconnect for next time
if (this.currentWorkspace) {
console.log(`🔄 Attempting to reconnect to workspace: ${this.currentWorkspace}`);
setTimeout(() => this.connectToServer(this.currentWorkspace), 1000);
}
}
return false; return false;
} }
} }
@@ -162,10 +351,17 @@ class SigSocketService {
console.log(`✅ Request rejected successfully: ${requestId}`); console.log(`✅ Request rejected successfully: ${requestId}`);
// Update UI // Clean flow: Reject -> Remove from storage -> Update UI
// 1. Remove from persistent storage (WASM already removed it)
await this.persistPendingRequests();
// 2. Update badge count
this.updateBadge(); this.updateBadge();
// 3. Notify popup of updated state
this.notifyPopupOfRequestUpdate(); this.notifyPopupOfRequestUpdate();
console.log(`✅ Request ${requestId} rejected and cleaned up`);
return true; return true;
} catch (error) { } catch (error) {
@@ -224,18 +420,54 @@ class SigSocketService {
} }
/** /**
* Show notification for new sign request * Show clickable notification for new sign request
* Call this AFTER the request has been stored and persisted
*/ */
showSignRequestNotification() { async showSignRequestNotification() {
try { try {
if (chrome.notifications && chrome.notifications.create) { if (chrome.notifications && chrome.notifications.create) {
chrome.notifications.create({ // Small delay to ensure request is fully stored
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`📢 Preparing notification for new signature request`);
// Check if keyspace is currently unlocked to customize message
let message = 'New signature request received. Click to review and approve.';
let title = 'SigSocket Sign Request';
// Try to determine if keyspace is locked
try {
const requests = await this.getPendingRequests();
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
if (!canApprove) {
message = 'New signature request received. Click to unlock keyspace and approve.';
title = 'SigSocket Request';
}
} catch (error) {
// If we can't check, use generic message
message = 'New signature request received. Click to open extension.';
}
// Create clickable notification with unique ID
const notificationId = `sigsocket-request-${Date.now()}`;
const notificationOptions = {
type: 'basic', type: 'basic',
iconUrl: 'icons/icon48.png', iconUrl: 'icons/icon48.png',
title: 'SigSocket Sign Request', title: title,
message: 'New signature request received. Click to review.' message: message,
requireInteraction: true // Keep notification visible until user interacts
};
console.log(`📢 Creating notification: ${notificationId}`, notificationOptions);
chrome.notifications.create(notificationId, notificationOptions, (createdId) => {
if (chrome.runtime.lastError) {
console.error('❌ Failed to create notification:', chrome.runtime.lastError);
} else {
console.log(`✅ Notification created successfully: ${createdId}`);
}
}); });
console.log('📢 Notification shown for sign request');
} else { } else {
console.log('📢 Notifications not available, skipping notification'); console.log('📢 Notifications not available, skipping notification');
} }
@@ -322,6 +554,10 @@ class SigSocketService {
this.isConnected = false; this.isConnected = false;
this.currentWorkspace = null; this.currentWorkspace = null;
this.connectedPublicKey = null; this.connectedPublicKey = null;
this.lastKnownConnectionState = false;
// Stop status monitoring
this.stopStatusMonitoring();
this.updateBadge(); this.updateBadge();
@@ -333,7 +569,50 @@ class SigSocketService {
} }
/** /**
* Get connection status from WASM * Clear persisted pending requests from storage
*/
async clearPersistedRequests() {
try {
await chrome.storage.local.remove(['sigSocketPendingRequests']);
console.log('🗑️ Cleared persisted pending requests from storage');
} catch (error) {
console.warn('Failed to clear persisted requests:', error);
}
}
/**
* Clean workspace switch - clear current workspace requests only
*/
async cleanWorkspaceSwitch(newWorkspace) {
try {
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${newWorkspace}`);
// 1. Persist current workspace requests before switching
if (this.currentWorkspace && this.isConnected) {
await this.persistPendingRequests();
console.log(`💾 Saved requests for workspace: ${this.currentWorkspace}`);
}
// 2. Clear WASM state (will be restored for new workspace)
if (this.wasmModule?.SigSocketManager) {
await this.wasmModule.SigSocketManager.clear_pending_requests();
console.log('🧹 Cleared WASM request state');
}
// 3. Reset local state
this.currentWorkspace = null;
this.connectedPublicKey = null;
this.isConnected = false;
console.log('✅ Workspace switch cleanup completed');
} catch (error) {
console.error('❌ Failed to clean workspace switch:', error);
}
}
/**
* Get connection status with real connection verification
* @returns {Promise<Object>} - Connection status information * @returns {Promise<Object>} - Connection status information
*/ */
async getStatus() { async getStatus() {
@@ -348,21 +627,63 @@ class SigSocketService {
}; };
} }
// Let WASM provide the authoritative status // Get WASM status first
const statusJson = await this.wasmModule.SigSocketManager.get_connection_status(); const statusJson = await this.wasmModule.SigSocketManager.get_connection_status();
const status = JSON.parse(statusJson); const status = JSON.parse(statusJson);
const requests = await this.getPendingRequests();
return { // Verify connection by trying to get requests (this will fail if not connected)
isConnected: status.is_connected, let actuallyConnected = false;
workspace: status.workspace, let requests = [];
publicKey: status.public_key,
try {
requests = await this.getPendingRequests();
// If we can get requests and WASM says connected, we're probably connected
actuallyConnected = status.is_connected && Array.isArray(requests);
} catch (error) {
// If getting requests fails, we're definitely not connected
console.warn('Connection verification failed:', error);
actuallyConnected = false;
}
// Update our internal state
this.isConnected = actuallyConnected;
if (status.connected_public_key && actuallyConnected) {
this.connectedPublicKey = status.connected_public_key;
} else {
this.connectedPublicKey = null;
}
// If we're disconnected, clear our workspace
if (!actuallyConnected) {
this.currentWorkspace = null;
}
const statusResult = {
isConnected: actuallyConnected,
workspace: this.currentWorkspace,
publicKey: status.connected_public_key,
pendingRequestCount: requests.length, pendingRequestCount: requests.length,
serverUrl: this.defaultServerUrl serverUrl: this.defaultServerUrl,
// Clean flow status indicators
cleanFlowReady: actuallyConnected && this.currentWorkspace && status.connected_public_key
}; };
console.log('📊 Clean flow status:', {
connected: statusResult.isConnected,
workspace: statusResult.workspace,
requestCount: statusResult.pendingRequestCount,
flowReady: statusResult.cleanFlowReady
});
return statusResult;
} catch (error) { } catch (error) {
console.error('Failed to get status:', error); console.error('Failed to get status:', error);
// Clear state on error
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
return { return {
isConnected: false, isConnected: false,
workspace: null, workspace: null,
@@ -375,33 +696,178 @@ class SigSocketService {
/** /**
* Set the popup port for communication * Set the popup port for communication
* @param {chrome.runtime.Port} port - The popup port * @param {chrome.runtime.Port|null} port - The popup port or null to disconnect
*/ */
setPopupPort(port) { setPopupPort(port) {
this.popupPort = port; this.popupPort = port;
console.log('📱 Popup connected to SigSocket service');
if (port) {
console.log('📱 Popup connected to SigSocket service');
// Immediately check connection status when popup opens
this.checkConnectionStatusNow();
// Start monitoring connection status when popup connects
this.startStatusMonitoring();
} else {
console.log('📱 Popup disconnected from SigSocket service');
// Stop monitoring when popup disconnects
this.stopStatusMonitoring();
}
} }
/** /**
* Called when keyspace is unlocked - notify popup of current state * Immediately check and update connection status
*/
async checkConnectionStatusNow() {
try {
// Force a fresh connection check
const currentStatus = await this.getStatusWithConnectionTest();
this.lastKnownConnectionState = currentStatus.isConnected;
// Notify popup of current status
this.notifyPopupOfStatusChange(currentStatus);
console.log(`🔍 Immediate status check: ${currentStatus.isConnected ? 'Connected' : 'Disconnected'}`);
} catch (error) {
console.warn('Failed to check connection status immediately:', error);
}
}
/**
* Get status with additional connection testing
*/
async getStatusWithConnectionTest() {
const status = await this.getStatus();
// If WASM claims we're connected, do an additional verification
if (status.isConnected) {
try {
// Try to get connection status again - if this fails, we're not really connected
const verifyJson = await this.wasmModule.SigSocketManager.get_connection_status();
const verifyStatus = JSON.parse(verifyJson);
if (!verifyStatus.is_connected) {
console.log('🔍 Connection verification failed - marking as disconnected');
status.isConnected = false;
this.isConnected = false;
this.currentWorkspace = null;
}
} catch (error) {
console.log('🔍 Connection test failed - marking as disconnected:', error.message);
status.isConnected = false;
this.isConnected = false;
this.currentWorkspace = null;
}
}
return status;
}
/**
* Start periodic status monitoring to detect connection changes
*/
startStatusMonitoring() {
// Clear any existing monitoring
if (this.statusMonitorInterval) {
clearInterval(this.statusMonitorInterval);
}
// Check status every 2 seconds when popup is open (more responsive)
this.statusMonitorInterval = setInterval(async () => {
if (this.popupPort) {
try {
const currentStatus = await this.getStatusWithConnectionTest();
// Check if connection status changed
if (currentStatus.isConnected !== this.lastKnownConnectionState) {
console.log(`🔄 Connection state changed: ${this.lastKnownConnectionState} -> ${currentStatus.isConnected}`);
this.lastKnownConnectionState = currentStatus.isConnected;
// Notify popup of status change
this.notifyPopupOfStatusChange(currentStatus);
}
} catch (error) {
console.warn('Status monitoring error:', error);
// On error, assume disconnected
if (this.lastKnownConnectionState !== false) {
console.log('🔄 Status monitoring error - marking as disconnected');
this.lastKnownConnectionState = false;
this.notifyPopupOfStatusChange({
isConnected: false,
workspace: null,
publicKey: null,
pendingRequestCount: 0,
serverUrl: this.defaultServerUrl
});
}
}
} else {
// Stop monitoring when popup is closed
this.stopStatusMonitoring();
}
}, 2000); // 2 seconds for better responsiveness
}
/**
* Stop status monitoring
*/
stopStatusMonitoring() {
if (this.statusMonitorInterval) {
clearInterval(this.statusMonitorInterval);
this.statusMonitorInterval = null;
}
}
/**
* Notify popup of connection status change
* @param {Object} status - Current connection status
*/
notifyPopupOfStatusChange(status) {
if (this.popupPort) {
this.popupPort.postMessage({
type: 'CONNECTION_STATUS_CHANGED',
status: status
});
console.log(`📡 Notified popup of connection status change: ${status.isConnected ? 'Connected' : 'Disconnected'}`);
}
}
/**
* Called when keyspace is unlocked - clean approach to show pending requests
*/ */
async onKeypaceUnlocked() { async onKeypaceUnlocked() {
if (!this.popupPort) return;
try { try {
console.log('🔓 Keyspace unlocked - preparing to show pending requests');
// 1. Restore any persisted requests for this workspace
await this.restorePendingRequests();
// 2. Get current requests (includes restored + any new ones)
const requests = await this.getPendingRequests(); const requests = await this.getPendingRequests();
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
this.popupPort.postMessage({ // 3. Check if we can approve requests (keyspace should be unlocked now)
type: 'KEYSPACE_UNLOCKED', const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : true;
canApprove,
pendingRequests: requests
});
console.log(`🔓 Keyspace unlocked notification sent: ${requests.length} requests, canApprove: ${canApprove}`); // 4. Update badge with current count
this.updateBadge();
// 5. Notify popup if connected
if (this.popupPort) {
this.popupPort.postMessage({
type: 'KEYSPACE_UNLOCKED',
canApprove,
pendingRequests: requests
});
}
console.log(`🔓 Keyspace unlocked: ${requests.length} requests ready, canApprove: ${canApprove}`);
return requests;
} catch (error) { } catch (error) {
console.error('Failed to handle keyspace unlock:', error); console.error('Failed to handle keyspace unlock:', error);
return [];
} }
} }
} }

View File

@@ -7,28 +7,17 @@
<body> <body>
<div class="container"> <div class="container">
<header class="header"> <header class="header">
<div class="logo"> <div class="logo clickable-header" id="headerTitle">
<div class="logo-icon">🔐</div> <div class="logo-icon">🔐</div>
<h1>CryptoVault</h1> <h1>CryptoVault</h1>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<div class="settings-container"> <button id="settingsBtn" class="btn-icon-only" title="Settings">
<button id="settingsToggle" class="btn-icon-only" title="Settings"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="3"></circle>
<circle cx="12" cy="12" r="3"></circle> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> </svg>
</svg> </button>
</button>
<div class="settings-dropdown hidden" id="settingsDropdown">
<div class="settings-item">
<label for="timeoutInput">Session Timeout</label>
<div class="timeout-input-group">
<input type="number" id="timeoutInput" min="3" max="300" value="15">
<span>seconds</span>
</div>
</div>
</div>
</div>
<button id="themeToggle" class="btn-icon-only" title="Switch to dark mode"> <button id="themeToggle" class="btn-icon-only" title="Switch to dark mode">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <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> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
@@ -84,6 +73,14 @@
</div> </div>
<div class="requests-container" id="requestsContainer"> <div class="requests-container" id="requestsContainer">
<div class="loading-requests hidden" id="loadingRequestsMessage">
<div class="loading-state">
<div class="loading-spinner"></div>
<p>Loading requests...</p>
<small>Fetching pending signature requests</small>
</div>
</div>
<div class="no-requests" id="noRequestsMessage"> <div class="no-requests" id="noRequestsMessage">
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon">📝</div> <div class="empty-icon">📝</div>
@@ -106,14 +103,6 @@
</svg> </svg>
Refresh Refresh
</button> </button>
<button id="sigSocketStatusBtn" class="btn btn-ghost btn-small">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="6" x2="12" y2="12"></line>
<line x1="16" y1="16" x2="12" y2="12"></line>
</svg>
Status
</button>
</div> </div>
</div> </div>
@@ -235,8 +224,41 @@
</div> </div>
</section> </section>
<!-- Settings Section -->
<section class="section hidden" id="settingsSection">
<div class="settings-header">
<h2>Settings</h2>
</div>
<!-- Session Settings -->
<div class="card">
<h3>Session Settings</h3>
<div class="settings-item">
<label for="timeoutInput">Session Timeout</label>
<div class="timeout-input-group">
<input type="number" id="timeoutInput" min="3" max="300" value="15">
<span>seconds</span>
</div>
<small class="settings-help">Automatically lock session after inactivity</small>
</div>
</div>
<!-- SigSocket Settings -->
<div class="card">
<h3>SigSocket Settings</h3>
<div class="settings-item">
<label for="serverUrlInput">Server URL</label>
<div class="server-input-group">
<input type="text" id="serverUrlInput" placeholder="ws://localhost:8080/ws" value="ws://localhost:8080/ws">
<button id="saveServerUrlBtn" class="btn btn-small btn-primary">Save</button>
</div>
<small class="settings-help">WebSocket URL for SigSocket server (ws:// or wss://)</small>
</div>
</div>
</section>
</div> </div>

View File

@@ -32,6 +32,11 @@ function showToast(message, type = 'info') {
// Enhanced loading states for buttons // Enhanced loading states for buttons
function setButtonLoading(button, loading = true) { function setButtonLoading(button, loading = true) {
// Handle null/undefined button gracefully
if (!button) {
return;
}
if (loading) { if (loading) {
button.dataset.originalText = button.textContent; button.dataset.originalText = button.textContent;
button.classList.add('loading'); button.classList.add('loading');
@@ -126,9 +131,18 @@ const elements = {
// Header elements // Header elements
lockBtn: document.getElementById('lockBtn'), lockBtn: document.getElementById('lockBtn'),
themeToggle: document.getElementById('themeToggle'), themeToggle: document.getElementById('themeToggle'),
settingsToggle: document.getElementById('settingsToggle'), settingsBtn: document.getElementById('settingsBtn'),
settingsDropdown: document.getElementById('settingsDropdown'), 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'), timeoutInput: document.getElementById('timeoutInput'),
serverUrlInput: document.getElementById('serverUrlInput'),
saveServerUrlBtn: document.getElementById('saveServerUrlBtn'),
// Keypair management elements // Keypair management elements
toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'), toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'),
@@ -219,6 +233,53 @@ async function saveTimeoutSetting(timeout) {
await sendMessage('updateTimeout', { 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() { async function resetSessionTimeout() {
if (currentKeyspace) { if (currentKeyspace) {
await sendMessage('resetTimeout'); await sendMessage('resetTimeout');
@@ -241,11 +302,63 @@ function toggleTheme() {
updateThemeIcon(newTheme); updateThemeIcon(newTheme);
} }
// Settings dropdown management // Settings page navigation
function toggleSettingsDropdown() { async function showSettingsPage() {
const dropdown = elements.settingsDropdown; // Hide all sections
if (dropdown) { document.querySelectorAll('.section').forEach(section => {
dropdown.classList.toggle('hidden'); 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();
} }
} }
@@ -287,6 +400,19 @@ function updateThemeIcon(theme) {
} }
} }
// 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 // Establish connection to background script for keep-alive
function connectToBackground() { function connectToBackground() {
backgroundPort = chrome.runtime.connect({ name: 'popup' }); backgroundPort = chrome.runtime.connect({ name: 'popup' });
@@ -299,6 +425,7 @@ function connectToBackground() {
selectedKeypairId = null; selectedKeypairId = null;
setStatus('', false); setStatus('', false);
showSection('authSection'); showSection('authSection');
updateSettingsVisibility(); // Update settings visibility
clearVaultState(); clearVaultState();
// Clear form inputs // Clear form inputs
@@ -313,6 +440,13 @@ function connectToBackground() {
backgroundPort.onDisconnect.addListener(() => { backgroundPort.onDisconnect.addListener(() => {
backgroundPort = null; 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 // Initialize
@@ -323,6 +457,9 @@ document.addEventListener('DOMContentLoaded', async function() {
// Load timeout setting // Load timeout setting
await loadTimeoutSetting(); await loadTimeoutSetting();
// Load server URL setting
await loadServerUrlSetting();
// Ensure lock button starts hidden // Ensure lock button starts hidden
const lockBtn = document.getElementById('lockBtn'); const lockBtn = document.getElementById('lockBtn');
if (lockBtn) { if (lockBtn) {
@@ -338,7 +475,9 @@ document.addEventListener('DOMContentLoaded', async function() {
loginBtn: login, loginBtn: login,
lockBtn: lockSession, lockBtn: lockSession,
themeToggle: toggleTheme, themeToggle: toggleTheme,
settingsToggle: toggleSettingsDropdown, settingsBtn: showSettingsPage,
headerTitle: hideSettingsPage,
saveServerUrlBtn: saveServerUrlSetting,
toggleAddKeypairBtn: toggleAddKeypairForm, toggleAddKeypairBtn: toggleAddKeypairForm,
addKeypairBtn: addKeypair, addKeypairBtn: addKeypair,
cancelAddKeypairBtn: hideAddKeypairForm, cancelAddKeypairBtn: hideAddKeypairForm,
@@ -349,7 +488,10 @@ document.addEventListener('DOMContentLoaded', async function() {
}; };
Object.entries(eventMap).forEach(([elementKey, handler]) => { Object.entries(eventMap).forEach(([elementKey, handler]) => {
elements[elementKey]?.addEventListener('click', handler); const element = elements[elementKey];
if (element) {
element.addEventListener('click', handler);
}
}); });
// Tab functionality // Tab functionality
@@ -400,10 +542,66 @@ document.addEventListener('DOMContentLoaded', async function() {
} }
}); });
// 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 // Check for existing session
await checkExistingSession(); 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() { async function checkExistingSession() {
try { try {
const response = await sendMessage('getStatus'); const response = await sendMessage('getStatus');
@@ -413,15 +611,31 @@ async function checkExistingSession() {
elements.keyspaceInput.value = currentKeyspace; elements.keyspaceInput.value = currentKeyspace;
setStatus(currentKeyspace, true); setStatus(currentKeyspace, true);
showSection('vaultSection'); showSection('vaultSection');
updateSettingsVisibility(); // Update settings visibility
await loadKeypairs(); await loadKeypairs();
// Use retry mechanism for existing sessions to handle stale connections
await loadSigSocketStateWithRetry();
} else { } else {
// No active session // No active session
currentKeyspace = null;
setStatus('', false); setStatus('', false);
showSection('authSection'); showSection('authSection');
updateSettingsVisibility(); // Update settings visibility
// For no session, use regular loading (no retry needed)
await loadSigSocketState();
} }
} catch (error) { } catch (error) {
setStatus('', false); setStatus('', false);
showSection('authSection'); showSection('authSection');
// Still try to load SigSocket state even on error
try {
await loadSigSocketState();
} catch (sigSocketError) {
console.warn('Failed to load SigSocket state:', sigSocketError);
}
} }
} }
@@ -641,9 +855,23 @@ async function login() {
currentKeyspace = auth.keyspace; currentKeyspace = auth.keyspace;
setStatus(auth.keyspace, true); setStatus(auth.keyspace, true);
showSection('vaultSection'); showSection('vaultSection');
updateSettingsVisibility(); // Update settings visibility
clearVaultState(); clearVaultState();
await loadKeypairs(); 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; return response;
} else { } else {
throw new Error(getResponseError(response, 'login')); throw new Error(getResponseError(response, 'login'));
@@ -667,6 +895,7 @@ async function lockSession() {
selectedKeypairId = null; selectedKeypairId = null;
setStatus('', false); setStatus('', false);
showSection('authSection'); showSection('authSection');
updateSettingsVisibility(); // Update settings visibility
// Clear all form inputs // Clear all form inputs
elements.keyspaceInput.value = ''; elements.keyspaceInput.value = '';
@@ -936,28 +1165,8 @@ const verifySignature = () => performCryptoOperation({
// SigSocket functionality // SigSocket functionality
let sigSocketRequests = []; let sigSocketRequests = [];
let sigSocketStatus = { isConnected: false, workspace: null }; let sigSocketStatus = { isConnected: false, workspace: null };
let sigSocketElements = {}; // Will be initialized in DOMContentLoaded
// Initialize SigSocket UI elements let isInitialLoad = true; // Track if this is the first load
const sigSocketElements = {
connectionStatus: document.getElementById('connectionStatus'),
connectionDot: document.getElementById('connectionDot'),
connectionText: document.getElementById('connectionText'),
requestsContainer: document.getElementById('requestsContainer'),
noRequestsMessage: document.getElementById('noRequestsMessage'),
requestsList: document.getElementById('requestsList'),
refreshRequestsBtn: document.getElementById('refreshRequestsBtn'),
sigSocketStatusBtn: document.getElementById('sigSocketStatusBtn')
};
// Add SigSocket event listeners
document.addEventListener('DOMContentLoaded', () => {
// Add SigSocket button listeners
sigSocketElements.refreshRequestsBtn?.addEventListener('click', refreshSigSocketRequests);
sigSocketElements.sigSocketStatusBtn?.addEventListener('click', showSigSocketStatus);
// Load initial SigSocket state
loadSigSocketState();
});
// Listen for messages from background script about SigSocket events // Listen for messages from background script about SigSocket events
if (backgroundPort) { if (backgroundPort) {
@@ -968,6 +1177,12 @@ if (backgroundPort) {
updateRequestsList(message.pendingRequests); updateRequestsList(message.pendingRequests);
} else if (message.type === 'KEYSPACE_UNLOCKED') { } else if (message.type === 'KEYSPACE_UNLOCKED') {
handleKeypaceUnlocked(message); 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();
} }
}); });
} }
@@ -975,19 +1190,117 @@ if (backgroundPort) {
// Load SigSocket state when popup opens // Load SigSocket state when popup opens
async function loadSigSocketState() { async function loadSigSocketState() {
try { try {
// Get SigSocket status console.log('🔄 Loading SigSocket state...');
const statusResponse = await sendMessage('getSigSocketStatus');
if (statusResponse?.success) { // Show loading state for requests
updateConnectionStatus(statusResponse.status); showRequestsLoading();
// Show loading state for connection status on initial load
if (isInitialLoad) {
showConnectionLoading();
} }
// Get pending requests // 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'); const requestsResponse = await sendMessage('getPendingSignRequests');
if (requestsResponse?.success) { if (requestsResponse?.success) {
console.log(`📋 Retrieved ${requestsResponse.requests?.length || 0} pending requests:`, requestsResponse.requests);
updateRequestsList(requestsResponse.requests); updateRequestsList(requestsResponse.requests);
} else {
console.warn('Failed to get pending requests:', requestsResponse);
updateRequestsList([]);
} }
// Mark initial load as complete
isInitialLoad = false;
} catch (error) { } catch (error) {
console.warn('Failed to load SigSocket state:', 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');
} }
} }
@@ -995,15 +1308,42 @@ async function loadSigSocketState() {
function updateConnectionStatus(status) { function updateConnectionStatus(status) {
sigSocketStatus = status; sigSocketStatus = status;
// Hide loading state
hideConnectionLoading();
if (sigSocketElements.connectionDot && sigSocketElements.connectionText) { if (sigSocketElements.connectionDot && sigSocketElements.connectionText) {
if (status.isConnected) { if (status.isConnected) {
sigSocketElements.connectionDot.classList.add('connected'); sigSocketElements.connectionDot.classList.add('connected');
sigSocketElements.connectionText.textContent = `Connected (${status.workspace || 'Unknown'})`; sigSocketElements.connectionText.textContent = 'Connected';
} else { } else {
sigSocketElements.connectionDot.classList.remove('connected'); sigSocketElements.connectionDot.classList.remove('connected');
sigSocketElements.connectionText.textContent = 'Disconnected'; 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 // Update requests list display
@@ -1012,6 +1352,9 @@ function updateRequestsList(requests) {
if (!sigSocketElements.requestsContainer) return; if (!sigSocketElements.requestsContainer) return;
// Hide loading state
hideRequestsLoading();
if (sigSocketRequests.length === 0) { if (sigSocketRequests.length === 0) {
sigSocketElements.noRequestsMessage?.classList.remove('hidden'); sigSocketElements.noRequestsMessage?.classList.remove('hidden');
sigSocketElements.requestsList?.classList.add('hidden'); sigSocketElements.requestsList?.classList.add('hidden');
@@ -1036,8 +1379,42 @@ function createRequestItem(request) {
const shortId = request.id.substring(0, 8) + '...'; const shortId = request.id.substring(0, 8) + '...';
const decodedMessage = request.message ? atob(request.message) : 'No message'; 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 ` return `
<div class="request-item" data-request-id="${request.id}"> <div class="request-item ${isKeypaceUnlocked ? '' : 'locked'}" data-request-id="${request.id}">
<div class="request-header"> <div class="request-header">
<div class="request-id" title="${request.id}">${shortId}</div> <div class="request-id" title="${request.id}">${shortId}</div>
<div class="request-time">${requestTime}</div> <div class="request-time">${requestTime}</div>
@@ -1047,14 +1424,8 @@ function createRequestItem(request) {
${decodedMessage.length > 100 ? decodedMessage.substring(0, 100) + '...' : decodedMessage} ${decodedMessage.length > 100 ? decodedMessage.substring(0, 100) + '...' : decodedMessage}
</div> </div>
<div class="request-actions"> ${statusIndicator}
<button class="btn-approve" data-request-id="${request.id}"> ${actionsHtml}
✓ Approve
</button>
<button class="btn-reject" data-request-id="${request.id}">
✗ Reject
</button>
</div>
</div> </div>
`; `;
} }
@@ -1091,15 +1462,61 @@ function handleNewSignRequest(message) {
} }
} }
// Handle keyspace unlocked event // Handle keyspace unlocked event - Clean flow implementation
function handleKeypaceUnlocked(message) { function handleKeypaceUnlocked(message) {
// Update requests list console.log('🔓 Keyspace unlocked - applying clean flow for request display');
if (message.pendingRequests) {
updateRequestsList(message.pendingRequests);
}
// Update button states based on whether requests can be approved // Clean flow: Unlock -> Show loading -> Display requests -> Update UI
updateRequestButtonStates(message.canApprove); 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 // Show workspace mismatch warning
@@ -1133,30 +1550,46 @@ function updateRequestButtonStates(canApprove) {
// Approve a sign request // Approve a sign request
async function approveSignRequest(requestId) { async function approveSignRequest(requestId) {
let button = null;
try { try {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`);
setButtonLoading(button, true); setButtonLoading(button, true);
const response = await sendMessage('approveSignRequest', { requestId }); const response = await sendMessage('approveSignRequest', { requestId });
if (response?.success) { if (response?.success) {
showToast('Request approved and signed!', 'success'); showToast('Request approved and signed!', 'success');
showRequestsLoading();
await refreshSigSocketRequests(); await refreshSigSocketRequests();
} else { } else {
throw new Error(getResponseError(response, 'approve request')); 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) { } catch (error) {
console.error('Error approving request:', error);
showToast(`Failed to approve request: ${error.message}`, 'error'); showToast(`Failed to approve request: ${error.message}`, 'error');
} finally { } finally {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); // Re-query button in case DOM was updated during the operation
setButtonLoading(button, false); const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-approve`);
setButtonLoading(finalButton, false);
} }
} }
// Reject a sign request // Reject a sign request
async function rejectSignRequest(requestId) { async function rejectSignRequest(requestId) {
let button = null;
try { try {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`);
setButtonLoading(button, true); setButtonLoading(button, true);
const response = await sendMessage('rejectSignRequest', { const response = await sendMessage('rejectSignRequest', {
@@ -1166,6 +1599,7 @@ async function rejectSignRequest(requestId) {
if (response?.success) { if (response?.success) {
showToast('Request rejected', 'info'); showToast('Request rejected', 'info');
showRequestsLoading();
await refreshSigSocketRequests(); await refreshSigSocketRequests();
} else { } else {
throw new Error(getResponseError(response, 'reject request')); throw new Error(getResponseError(response, 'reject request'));
@@ -1173,8 +1607,9 @@ async function rejectSignRequest(requestId) {
} catch (error) { } catch (error) {
showToast(`Failed to reject request: ${error.message}`, 'error'); showToast(`Failed to reject request: ${error.message}`, 'error');
} finally { } finally {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); // Re-query button in case DOM was updated during the operation
setButtonLoading(button, false); const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-reject`);
setButtonLoading(finalButton, false);
} }
} }
@@ -1182,42 +1617,35 @@ async function rejectSignRequest(requestId) {
async function refreshSigSocketRequests() { async function refreshSigSocketRequests() {
try { try {
setButtonLoading(sigSocketElements.refreshRequestsBtn, true); setButtonLoading(sigSocketElements.refreshRequestsBtn, true);
showRequestsLoading();
console.log('🔄 Refreshing SigSocket requests...');
const response = await sendMessage('getPendingSignRequests'); const response = await sendMessage('getPendingSignRequests');
if (response?.success) { if (response?.success) {
console.log(`📋 Retrieved ${response.requests?.length || 0} pending requests`);
updateRequestsList(response.requests); updateRequestsList(response.requests);
showToast('Requests refreshed', 'success');
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 { } else {
console.error('Failed to get pending requests:', response);
hideRequestsLoading();
throw new Error(getResponseError(response, 'refresh requests')); throw new Error(getResponseError(response, 'refresh requests'));
} }
} catch (error) { } catch (error) {
console.error('Error refreshing requests:', error);
hideRequestsLoading();
showToast(`Failed to refresh requests: ${error.message}`, 'error'); showToast(`Failed to refresh requests: ${error.message}`, 'error');
} finally { } finally {
setButtonLoading(sigSocketElements.refreshRequestsBtn, false); setButtonLoading(sigSocketElements.refreshRequestsBtn, false);
} }
} }
// Show SigSocket status
async function showSigSocketStatus() {
try {
const response = await sendMessage('getSigSocketStatus');
if (response?.success) {
const status = response.status;
const statusText = `
SigSocket Status:
• Connected: ${status.isConnected ? 'Yes' : 'No'}
• Workspace: ${status.workspace || 'None'}
• Public Key: ${status.publicKey ? status.publicKey.substring(0, 16) + '...' : 'None'}
• Pending Requests: ${status.pendingRequestCount || 0}
• Server URL: ${status.serverUrl}
`.trim();
showToast(statusText, 'info');
updateConnectionStatus(status);
} else {
throw new Error(getResponseError(response, 'get status'));
}
} catch (error) {
showToast(`Failed to get status: ${error.message}`, 'error');
}
}

View File

@@ -188,6 +188,15 @@ body {
margin: 0; margin: 0;
} }
.clickable-header {
cursor: pointer;
transition: opacity 0.2s ease;
}
.clickable-header:hover {
opacity: 0.8;
}
.header-actions { .header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -261,6 +270,75 @@ body {
color: var(--text-muted); color: var(--text-muted);
} }
.server-input-group {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.server-input-group input {
flex: 1;
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 14px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-input);
color: var(--text-primary);
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.server-input-group input:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 2px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.15);
}
.settings-help {
display: block;
font-size: 12px;
color: var(--text-muted);
margin-top: var(--spacing-xs);
font-style: italic;
}
/* Settings page styles */
.settings-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.settings-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.about-info {
text-align: left;
}
.about-info p {
margin: 0 0 var(--spacing-xs) 0;
font-size: 14px;
color: var(--text-primary);
}
.about-info strong {
font-weight: 600;
}
.version-info {
font-size: 12px;
color: var(--text-muted);
font-style: italic;
}
.btn-icon-only { .btn-icon-only {
background: var(--bg-button-ghost); background: var(--bg-button-ghost);
border: none; border: none;
@@ -456,6 +534,17 @@ input::placeholder, textarea::placeholder {
font-size: 12px; font-size: 12px;
} }
/* Button icon spacing */
.btn svg {
margin-right: var(--spacing-xs);
flex-shrink: 0;
}
.btn svg:last-child {
margin-right: 0;
margin-left: var(--spacing-xs);
}
.btn:disabled { .btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
@@ -1073,18 +1162,19 @@ input::placeholder, textarea::placeholder {
/* SigSocket Requests Styles */ /* SigSocket Requests Styles */
.sigsocket-section { .sigsocket-section {
margin-bottom: 20px; margin-bottom: var(--spacing-lg);
} }
.section-header { .section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 15px; margin-bottom: var(--spacing-lg);
} }
.section-header h3 { .section-header h3 {
margin: 0; margin: 0;
color: var(--text-primary);
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
} }
@@ -1092,7 +1182,7 @@ input::placeholder, textarea::placeholder {
.connection-status { .connection-status {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: var(--spacing-xs);
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
} }
@@ -1109,16 +1199,62 @@ input::placeholder, textarea::placeholder {
background: var(--accent-success); background: var(--accent-success);
} }
.status-dot.loading {
background: var(--accent-warning);
animation: pulse-dot 1.5s ease-in-out infinite;
}
@keyframes pulse-dot {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.2);
}
}
.requests-container { .requests-container {
min-height: 80px; min-height: 80px;
} }
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 20px; padding: var(--spacing-xl);
color: var(--text-secondary); color: var(--text-secondary);
} }
.loading-state {
text-align: center;
padding: var(--spacing-xl);
color: var(--text-secondary);
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-color);
border-top: 2px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto var(--spacing-sm) auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
margin: var(--spacing-sm) 0;
font-weight: 500;
}
.loading-state small {
opacity: 0.8;
}
.empty-icon { .empty-icon {
font-size: 24px; font-size: 24px;
margin-bottom: 8px; margin-bottom: 8px;
@@ -1228,10 +1364,42 @@ input::placeholder, textarea::placeholder {
.sigsocket-actions { .sigsocket-actions {
display: flex; display: flex;
gap: 8px; gap: var(--spacing-sm);
margin-top: 12px; margin-top: var(--spacing-md);
padding-top: 12px; }
border-top: 1px solid var(--border-color);
/* Ensure refresh button follows design system */
#refreshRequestsBtn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
}
/* Request item locked state styles */
.request-item.locked {
opacity: 0.8;
border-left: 3px solid var(--warning-color, #ffa500);
}
.request-status.pending {
background: var(--warning-bg, #fff3cd);
color: var(--warning-text, #856404);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: 4px;
font-size: 12px;
margin: var(--spacing-xs) 0;
border: 1px solid var(--warning-border, #ffeaa7);
}
.request-actions.locked button {
opacity: 0.6;
cursor: not-allowed;
}
.request-actions.locked button:hover {
background: var(--button-bg) !important;
transform: none !important;
} }
.workspace-mismatch { .workspace-mismatch {

View File

@@ -467,31 +467,31 @@ export function run_rhai(script) {
} }
function __wbg_adapter_34(arg0, arg1, arg2) { function __wbg_adapter_34(arg0, arg1, arg2) {
wasm.closure174_externref_shim(arg0, arg1, arg2); wasm.closure203_externref_shim(arg0, arg1, arg2);
} }
function __wbg_adapter_39(arg0, arg1) { function __wbg_adapter_39(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha4436a3f79fb1a0f(arg0, arg1); wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd79bf9f6d48e92f7(arg0, arg1);
} }
function __wbg_adapter_44(arg0, arg1, arg2) { function __wbg_adapter_44(arg0, arg1, arg2) {
wasm.closure237_externref_shim(arg0, arg1, arg2); wasm.closure239_externref_shim(arg0, arg1, arg2);
} }
function __wbg_adapter_49(arg0, arg1) { function __wbg_adapter_49(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf148c54a4a246cea(arg0, arg1); wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf103de07b8856532(arg0, arg1);
} }
function __wbg_adapter_52(arg0, arg1, arg2) { function __wbg_adapter_52(arg0, arg1, arg2) {
wasm.closure308_externref_shim(arg0, arg1, arg2); wasm.closure319_externref_shim(arg0, arg1, arg2);
} }
function __wbg_adapter_55(arg0, arg1, arg2) { function __wbg_adapter_55(arg0, arg1, arg2) {
wasm.closure392_externref_shim(arg0, arg1, arg2); wasm.closure395_externref_shim(arg0, arg1, arg2);
} }
function __wbg_adapter_207(arg0, arg1, arg2, arg3) { function __wbg_adapter_207(arg0, arg1, arg2, arg3) {
wasm.closure2046_externref_shim(arg0, arg1, arg2, arg3); wasm.closure2042_externref_shim(arg0, arg1, arg2, arg3);
} }
const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"]; const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"];
@@ -1217,40 +1217,40 @@ function __wbg_get_imports() {
const ret = false; const ret = false;
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper1015 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper1036 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 309, __wbg_adapter_52); const ret = makeMutClosure(arg0, arg1, 320, __wbg_adapter_52);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper1320 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper1329 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 393, __wbg_adapter_55); const ret = makeMutClosure(arg0, arg1, 396, __wbg_adapter_55);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper423 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper624 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper424 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper625 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper425 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper626 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_39); const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_39);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper428 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper630 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper765 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper767 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper768 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_49);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper770 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_49);
return ret; return ret;
}; };
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {

View File

@@ -1 +0,0 @@
dist

View File

@@ -1,88 +0,0 @@
# SAL Modular Cryptographic Browser Extension
A modern, secure browser extension for interacting with the SAL modular Rust cryptographic stack, enabling key management, cryptographic operations, and secure Rhai script execution.
## Features
### Session & Key Management
- Create and unlock encrypted keyspaces with password protection
- Create, select, and manage multiple keypairs (Ed25519, Secp256k1)
- Clear session state visualization and management
### Cryptographic Operations
- Sign and verify messages using selected keypair
- Encrypt and decrypt messages using asymmetric cryptography
- Support for symmetric encryption using password-derived keys
### Scripting (Rhai)
- Execute Rhai scripts securely within the extension
- Explicit user approval for all script executions
- Script history and audit trail
### WebSocket Integration
- Connect to WebSocket servers using keypair's public key
- Receive, review, and approve/reject incoming scripts
- Support for both local and remote script execution
### Security
- Dark mode UI with modern, responsive design
- Session auto-lock after configurable inactivity period
- Explicit user approval for all sensitive operations
- No persistent storage of passwords or private keys in plaintext
## Architecture
The extension is built with a modern tech stack:
- **Frontend**: React with TypeScript, Material-UI
- **State Management**: Zustand
- **Backend**: WebAssembly (WASM) modules compiled from Rust
- **Storage**: Chrome extension storage API with encryption
- **Networking**: WebSocket for server communication
## Development Setup
1. Install dependencies:
```
cd sal_extension
npm install
```
2. Build the extension:
```
npm run build
```
3. Load the extension in Chrome/Edge:
- Navigate to `chrome://extensions/`
- Enable "Developer mode"
- Click "Load unpacked" and select the `dist` directory
4. For development with hot-reload:
```
npm run watch
```
## Integration with WASM
The extension uses WebAssembly modules compiled from Rust to perform cryptographic operations securely. The WASM modules are loaded in the extension's background script and provide a secure API for the frontend.
Key WASM functions exposed:
- `init_session` - Unlock a keyspace with password
- `create_keyspace` - Create a new keyspace
- `add_keypair` - Create a new keypair
- `select_keypair` - Select a keypair for use
- `sign` - Sign a message with the selected keypair
- `run_rhai` - Execute a Rhai script securely
## Security Considerations
- The extension follows the principle of least privilege
- All sensitive operations require explicit user approval
- Passwords are never stored persistently, only kept in memory during an active session
- Session state is automatically cleared when the extension is locked
- WebSocket connections are authenticated using the user's public key
## License
[MIT License](LICENSE)

View File

@@ -1 +0,0 @@
:root{font-family:Roboto,system-ui,sans-serif;line-height:1.5;font-weight:400;color-scheme:dark}body{margin:0;min-width:360px;min-height:520px;overflow-x:hidden}#root{width:100%;height:100%}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:rgba(255,255,255,.05);border-radius:3px}::-webkit-scrollbar-thumb{background:rgba(255,255,255,.2);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.3)}

View File

@@ -1 +0,0 @@
console.log("Background script initialized");let i=!1,e=null;chrome.runtime.onMessage.addListener((o,l,r)=>{if(console.log("Background received message:",o.type),o.type==="SESSION_STATUS")return r({active:i}),!0;if(o.type==="SESSION_UNLOCK")return i=!0,r({success:!0}),!0;if(o.type==="SESSION_LOCK")return i=!1,e&&(e.close(),e=null),r({success:!0}),!0;if(o.type==="CONNECT_WEBSOCKET"&&o.serverUrl&&o.publicKey){try{e&&e.close(),e=new WebSocket(o.serverUrl),e.onopen=()=>{console.log("WebSocket connection established"),e&&e.send(JSON.stringify({type:"IDENTIFY",publicKey:o.publicKey}))},e.onmessage=c=>{try{const t=JSON.parse(c.data);console.log("WebSocket message received:",t),chrome.runtime.sendMessage({type:"WEBSOCKET_MESSAGE",data:t}).catch(n=>{console.error("Failed to forward WebSocket message:",n)})}catch(t){console.error("Failed to parse WebSocket message:",t)}},e.onerror=c=>{console.error("WebSocket error:",c)},e.onclose=()=>{console.log("WebSocket connection closed"),e=null},r({success:!0})}catch(c){console.error("Failed to connect to WebSocket:",c),r({success:!1,error:c.message})}return!0}return o.type==="DISCONNECT_WEBSOCKET"?(e?(e.close(),e=null,r({success:!0})):r({success:!1,error:"No active WebSocket connection"}),!0):!1});chrome.notifications&&chrome.notifications.onClicked&&chrome.notifications.onClicked.addListener(o=>{chrome.action.openPopup()});

View File

@@ -1,61 +0,0 @@
// Background Service Worker for SAL Modular Cryptographic Extension
// This is a simplified version that only handles messaging
console.log('Background script initialized');
// Store active WebSocket connection
let activeWebSocket = null;
let sessionActive = false;
// Listen for messages from popup or content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Background received message:', message.type);
if (message.type === 'SESSION_STATUS') {
sendResponse({ active: sessionActive });
return true;
}
if (message.type === 'SESSION_UNLOCK') {
sessionActive = true;
sendResponse({ success: true });
return true;
}
if (message.type === 'SESSION_LOCK') {
sessionActive = false;
if (activeWebSocket) {
activeWebSocket.close();
activeWebSocket = null;
}
sendResponse({ success: true });
return true;
}
if (message.type === 'CONNECT_WEBSOCKET') {
// Simplified WebSocket handling
sendResponse({ success: true });
return true;
}
if (message.type === 'DISCONNECT_WEBSOCKET') {
if (activeWebSocket) {
activeWebSocket.close();
activeWebSocket = null;
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'No active WebSocket connection' });
}
return true;
}
return false;
});
// Initialize notification setup
chrome.notifications.onClicked.addListener((notificationId) => {
// Open the extension popup when a notification is clicked
chrome.action.openPopup();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hero Vault</title>
<script type="module" crossorigin src="/assets/index-b58c7e43.js"></script>
<link rel="stylesheet" href="/assets/index-11057528.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -1,26 +0,0 @@
{
"manifest_version": 3,
"name": "Hero Vault",
"version": "1.0.0",
"description": "A secure browser extension for cryptographic operations and Rhai script execution",
"action": {
"default_popup": "index.html",
"default_title": "Hero Vault"
},
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"permissions": [
"storage",
"unlimitedStorage"
],
"background": {
"service_worker": "service-worker-loader.js",
"type": "module"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
}
}

View File

@@ -1 +0,0 @@
import './assets/simple-background.ts-e63275e1.js';

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hero Vault</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
{
"name": "hero-vault-extension",
"version": "1.0.0",
"description": "Hero Vault - A secure browser extension for cryptographic operations",
"scripts": {
"dev": "node scripts/copy-wasm.js && vite",
"build": "node scripts/copy-wasm.js && ([ \"$NO_TYPECHECK\" = \"true\" ] || tsc) && vite build",
"watch": "node scripts/copy-wasm.js && tsc && vite build --watch",
"preview": "vite preview",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write \"src/**/*.{ts,tsx,css,scss}\"",
"copy-wasm": "node scripts/copy-wasm.js"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.3",
"@mui/material": "^5.14.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2",
"zustand": "^4.4.0"
},
"devDependencies": {
"@crxjs/vite-plugin": "^2.0.0-beta.18",
"@types/chrome": "^0.0.243",
"@types/node": "^20.4.5",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"esbuild": "^0.25.4",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"prettier": "^3.0.0",
"sass": "^1.64.1",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,26 +0,0 @@
{
"manifest_version": 3,
"name": "Hero Vault",
"version": "1.0.0",
"description": "A secure browser extension for cryptographic operations and Rhai script execution",
"action": {
"default_popup": "index.html",
"default_title": "Hero Vault"
},
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"permissions": [
"storage",
"unlimitedStorage"
],
"background": {
"service_worker": "src/background/simple-background.ts",
"type": "module"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
}
}

View File

@@ -1,85 +0,0 @@
/**
* Script to build the background script for the extension
*/
const { build } = require('esbuild');
const { resolve } = require('path');
const fs = require('fs');
async function buildBackground() {
try {
console.log('Building background script...');
// First, create a simplified background script that doesn't import WASM
const backgroundContent = `
// Background Service Worker for SAL Modular Cryptographic Extension
// This is a simplified version that only handles messaging
console.log('Background script initialized');
// Store active WebSocket connection
let activeWebSocket = null;
let sessionActive = false;
// Listen for messages from popup or content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Background received message:', message.type);
if (message.type === 'SESSION_STATUS') {
sendResponse({ active: sessionActive });
return true;
}
if (message.type === 'SESSION_UNLOCK') {
sessionActive = true;
sendResponse({ success: true });
return true;
}
if (message.type === 'SESSION_LOCK') {
sessionActive = false;
if (activeWebSocket) {
activeWebSocket.close();
activeWebSocket = null;
}
sendResponse({ success: true });
return true;
}
if (message.type === 'CONNECT_WEBSOCKET') {
// Simplified WebSocket handling
sendResponse({ success: true });
return true;
}
if (message.type === 'DISCONNECT_WEBSOCKET') {
if (activeWebSocket) {
activeWebSocket.close();
activeWebSocket = null;
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'No active WebSocket connection' });
}
return true;
}
return false;
});
// Initialize notification setup
chrome.notifications.onClicked.addListener((notificationId) => {
// Open the extension popup when a notification is clicked
chrome.action.openPopup();
});
`;
// Write the simplified background script to a temporary file
fs.writeFileSync(resolve(__dirname, '../dist/background.js'), backgroundContent);
console.log('Background script built successfully!');
} catch (error) {
console.error('Error building background script:', error);
process.exit(1);
}
}
buildBackground();

View File

@@ -1,33 +0,0 @@
/**
* Script to copy WASM files from wasm_app/pkg to the extension build directory
*/
const fs = require('fs');
const path = require('path');
// Source and destination paths
const sourceDir = path.resolve(__dirname, '../../wasm_app/pkg');
const destDir = path.resolve(__dirname, '../public/wasm');
// Create destination directory if it doesn't exist
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
console.log(`Created directory: ${destDir}`);
}
// Copy all files from source to destination
try {
const files = fs.readdirSync(sourceDir);
files.forEach(file => {
const sourcePath = path.join(sourceDir, file);
const destPath = path.join(destDir, file);
fs.copyFileSync(sourcePath, destPath);
console.log(`Copied: ${file}`);
});
console.log('WASM files copied successfully!');
} catch (error) {
console.error('Error copying WASM files:', error);
process.exit(1);
}

View File

@@ -1,127 +0,0 @@
import { useState, useEffect } from 'react';
import { Box, Container, Paper } from '@mui/material';
import { Routes, Route, HashRouter } from 'react-router-dom';
// Import pages
import HomePage from './pages/HomePage';
import SessionPage from './pages/SessionPage';
import KeypairPage from './pages/KeypairPage';
import ScriptPage from './pages/ScriptPage';
import SettingsPage from './pages/SettingsPage';
import WebSocketPage from './pages/WebSocketPage';
import CryptoPage from './pages/CryptoPage';
// Import components
import Header from './components/Header';
import Navigation from './components/Navigation';
// Import session state management
import { useSessionStore } from './store/sessionStore';
function App() {
const { checkSessionStatus, initWasm } = useSessionStore();
const [isLoading, setIsLoading] = useState(true);
const [wasmError, setWasmError] = useState<string | null>(null);
// Initialize WASM and check session status on mount
useEffect(() => {
const initializeApp = async () => {
try {
// First initialize WASM module
const wasmInitialized = await initWasm();
if (!wasmInitialized) {
throw new Error('Failed to initialize WASM module');
}
// Then check session status
await checkSessionStatus();
} catch (error) {
console.error('Initialization error:', error);
setWasmError((error as Error).message || 'Failed to initialize the extension');
} finally {
setIsLoading(false);
}
};
initializeApp();
}, [checkSessionStatus, initWasm]);
if (isLoading) {
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
}}
>
Loading...
</Box>
);
}
if (wasmError) {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
p: 3,
textAlign: 'center',
}}
>
<Paper sx={{ p: 3, maxWidth: 400 }}>
<h6 style={{ color: 'red', marginBottom: '8px' }}>
WASM Module Failed to Initialize
</h6>
<p style={{ marginBottom: '16px' }}>
The WASM module could not be loaded. Please try reloading the extension.
</p>
<p style={{ fontSize: '0.875rem', color: 'gray' }}>
Error: {wasmError} Please contact support if the problem persists.
</p>
</Paper>
</Box>
);
}
return (
<HashRouter>
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<Header />
<Container component="main" sx={{ flexGrow: 1, overflow: 'auto', py: 2 }}>
<Paper
elevation={3}
sx={{
p: 2,
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}
>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/session" element={<SessionPage />} />
<Route path="/keypair" element={<KeypairPage />} />
<Route path="/crypto" element={<CryptoPage />} />
<Route path="/script" element={<ScriptPage />} />
<Route path="/websocket" element={<WebSocketPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Paper>
</Container>
<Navigation />
</Box>
</HashRouter>
);
}
export default App;

View File

@@ -1,145 +0,0 @@
/**
* Background Service Worker for Hero Vault Extension
*
* Responsibilities:
* - Maintain WebSocket connections
* - Handle incoming script requests
* - Manage session state when popup is closed
* - Provide messaging interface for popup/content scripts
* - Initialize WASM module when extension starts
*/
// Import WASM helper functions
import { initWasm } from '../wasm/wasmHelper';
// Initialize WASM module when service worker starts
initWasm().catch(error => {
console.error('Failed to initialize WASM module:', error);
});
// Store active WebSocket connection
let activeWebSocket: WebSocket | null = null;
let sessionActive = false;
// Listen for messages from popup or content scripts
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === 'SESSION_STATUS') {
sendResponse({ active: sessionActive });
return true;
}
if (message.type === 'SESSION_UNLOCK') {
sessionActive = true;
sendResponse({ success: true });
return true;
}
if (message.type === 'SESSION_LOCK') {
sessionActive = false;
if (activeWebSocket) {
activeWebSocket.close();
activeWebSocket = null;
}
sendResponse({ success: true });
return true;
}
if (message.type === 'CONNECT_WEBSOCKET' && message.serverUrl && message.publicKey) {
connectToWebSocket(message.serverUrl, message.publicKey)
.then(success => sendResponse({ success }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true; // Indicates we'll respond asynchronously
}
if (message.type === 'DISCONNECT_WEBSOCKET') {
if (activeWebSocket) {
activeWebSocket.close();
activeWebSocket = null;
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'No active WebSocket connection' });
}
return true;
}
});
/**
* Connect to a WebSocket server with the user's public key
*/
async function connectToWebSocket(serverUrl: string, publicKey: string): Promise<boolean> {
if (activeWebSocket) {
activeWebSocket.close();
}
return new Promise((resolve, reject) => {
try {
const ws = new WebSocket(serverUrl);
ws.onopen = () => {
// Send authentication message with public key
ws.send(JSON.stringify({
type: 'AUTH',
publicKey
}));
activeWebSocket = ws;
resolve(true);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(new Error('Failed to connect to WebSocket server'));
};
ws.onclose = () => {
activeWebSocket = null;
console.log('WebSocket connection closed');
};
ws.onmessage = async (event) => {
try {
const data = JSON.parse(event.data);
// Handle incoming script requests
if (data.type === 'SCRIPT_REQUEST') {
// Notify the user of the script request
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'Script Request',
message: `Received script request: ${data.title || 'Untitled Script'}`,
priority: 2
});
// Store the script request for the popup to handle
await chrome.storage.local.set({
pendingScripts: [
...(await chrome.storage.local.get('pendingScripts')).pendingScripts || [],
{
id: data.id,
title: data.title || 'Untitled Script',
description: data.description || '',
script: data.script,
tags: data.tags || [],
timestamp: Date.now()
}
]
});
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
} catch (error) {
reject(error);
}
});
}
// Initialize notification setup
chrome.notifications.onClicked.addListener((_notificationId) => {
// Open the extension popup when a notification is clicked
chrome.action.openPopup();
});
console.log('Hero Vault Extension background service worker initialized');

View File

@@ -1,115 +0,0 @@
/**
* Simplified Background Service Worker for Hero Vault Extension
*
* This is a version that doesn't use WASM to avoid service worker limitations
* with dynamic imports. It only handles basic messaging between components.
*/
console.log('Background script initialized');
// Store session state
let sessionActive = false;
let activeWebSocket: WebSocket | null = null;
// Listen for messages from popup or content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Background received message:', message.type);
if (message.type === 'SESSION_STATUS') {
sendResponse({ active: sessionActive });
return true;
}
if (message.type === 'SESSION_UNLOCK') {
sessionActive = true;
sendResponse({ success: true });
return true;
}
if (message.type === 'SESSION_LOCK') {
sessionActive = false;
if (activeWebSocket) {
activeWebSocket.close();
activeWebSocket = null;
}
sendResponse({ success: true });
return true;
}
if (message.type === 'CONNECT_WEBSOCKET' && message.serverUrl && message.publicKey) {
// Simplified WebSocket handling
try {
if (activeWebSocket) {
activeWebSocket.close();
}
activeWebSocket = new WebSocket(message.serverUrl);
activeWebSocket.onopen = () => {
console.log('WebSocket connection established');
// Send public key to identify this client
if (activeWebSocket) {
activeWebSocket.send(JSON.stringify({
type: 'IDENTIFY',
publicKey: message.publicKey
}));
}
};
activeWebSocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('WebSocket message received:', data);
// Forward message to popup
chrome.runtime.sendMessage({
type: 'WEBSOCKET_MESSAGE',
data
}).catch(error => {
console.error('Failed to forward WebSocket message:', error);
});
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
activeWebSocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
activeWebSocket.onclose = () => {
console.log('WebSocket connection closed');
activeWebSocket = null;
};
sendResponse({ success: true });
} catch (error) {
console.error('Failed to connect to WebSocket:', error);
sendResponse({ success: false, error: error.message });
}
return true;
}
if (message.type === 'DISCONNECT_WEBSOCKET') {
if (activeWebSocket) {
activeWebSocket.close();
activeWebSocket = null;
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'No active WebSocket connection' });
}
return true;
}
// If we don't handle the message, return false
return false;
});
// Handle notifications if available
if (chrome.notifications && chrome.notifications.onClicked) {
chrome.notifications.onClicked.addListener((notificationId) => {
// Open the extension popup when a notification is clicked
chrome.action.openPopup();
});
}

View File

@@ -1,97 +0,0 @@
import { AppBar, Toolbar, Typography, IconButton, Box, Chip } from '@mui/material';
import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import SignalWifiStatusbar4BarIcon from '@mui/icons-material/SignalWifiStatusbar4Bar';
import SignalWifiOffIcon from '@mui/icons-material/SignalWifiOff';
import { useSessionStore } from '../store/sessionStore';
const Header = () => {
const {
isSessionUnlocked,
currentKeyspace,
currentKeypair,
isWebSocketConnected,
lockSession
} = useSessionStore();
const handleLockClick = async () => {
if (isSessionUnlocked) {
await lockSession();
}
};
return (
<AppBar position="static" color="primary" elevation={0}>
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Hero Vault
</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
{/* WebSocket connection status */}
{isWebSocketConnected ? (
<Chip
icon={<SignalWifiStatusbar4BarIcon fontSize="small" />}
label="Connected"
size="small"
color="success"
variant="outlined"
/>
) : (
<Chip
icon={<SignalWifiOffIcon fontSize="small" />}
label="Offline"
size="small"
color="default"
variant="outlined"
/>
)}
{/* Session status */}
{isSessionUnlocked ? (
<Chip
icon={<LockOpenIcon fontSize="small" />}
label={currentKeyspace || 'Unlocked'}
size="small"
color="primary"
variant="outlined"
/>
) : (
<Chip
icon={<LockIcon fontSize="small" />}
label="Locked"
size="small"
color="error"
variant="outlined"
/>
)}
{/* Current keypair */}
{isSessionUnlocked && currentKeypair && (
<Chip
label={currentKeypair.name || currentKeypair.id}
size="small"
color="secondary"
variant="outlined"
/>
)}
{/* Lock button */}
{isSessionUnlocked && (
<IconButton
edge="end"
color="inherit"
onClick={handleLockClick}
size="small"
aria-label="lock session"
>
<LockIcon />
</IconButton>
)}
</Box>
</Toolbar>
</AppBar>
);
};
export default Header;

View File

@@ -1,130 +0,0 @@
import React, { useState } from 'react';
import { BottomNavigation, BottomNavigationAction, Paper, Box, IconButton, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { useNavigate, useLocation } from 'react-router-dom';
import HomeIcon from '@mui/icons-material/Home';
import VpnKeyIcon from '@mui/icons-material/VpnKey';
import CodeIcon from '@mui/icons-material/Code';
import SettingsIcon from '@mui/icons-material/Settings';
import WifiIcon from '@mui/icons-material/Wifi';
import LockIcon from '@mui/icons-material/Lock';
import { useSessionStore } from '../store/sessionStore';
const Navigation = () => {
const navigate = useNavigate();
const location = useLocation();
const { isSessionUnlocked } = useSessionStore();
// Get current path without leading slash
const currentPath = location.pathname.substring(1) || 'home';
// State for the more menu
const [moreAnchorEl, setMoreAnchorEl] = useState<null | HTMLElement>(null);
const isMoreMenuOpen = Boolean(moreAnchorEl);
const handleMoreClick = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
setMoreAnchorEl(event.currentTarget);
};
const handleMoreClose = () => {
setMoreAnchorEl(null);
};
const handleNavigation = (path: string) => {
navigate(`/${path === 'home' ? '' : path}`);
handleMoreClose();
};
return (
<Paper
sx={{ position: 'static', bottom: 0, left: 0, right: 0 }}
elevation={3}
>
<Box sx={{ display: 'flex', width: '100%' }}>
<BottomNavigation
showLabels
value={currentPath}
onChange={(_, newValue) => {
navigate(`/${newValue === 'home' ? '' : newValue}`);
}}
sx={{ flexGrow: 1 }}
>
<BottomNavigationAction
label="Home"
value="home"
icon={<HomeIcon />}
/>
<BottomNavigationAction
label="Keys"
value="keypair"
icon={<VpnKeyIcon />}
disabled={!isSessionUnlocked}
/>
<BottomNavigationAction
label="Crypto"
value="crypto"
icon={<LockIcon />}
disabled={!isSessionUnlocked}
/>
<BottomNavigationAction
label="More"
value="more"
icon={<MoreVertIcon />}
onClick={handleMoreClick}
/>
</BottomNavigation>
<Menu
anchorEl={moreAnchorEl}
open={isMoreMenuOpen}
onClose={handleMoreClose}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
>
<MenuItem
onClick={() => handleNavigation('script')}
disabled={!isSessionUnlocked}
selected={currentPath === 'script'}
>
<ListItemIcon>
<CodeIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Scripts</ListItemText>
</MenuItem>
<MenuItem
onClick={() => handleNavigation('websocket')}
disabled={!isSessionUnlocked}
selected={currentPath === 'websocket'}
>
<ListItemIcon>
<WifiIcon fontSize="small" />
</ListItemIcon>
<ListItemText>WebSocket</ListItemText>
</MenuItem>
<MenuItem
onClick={() => handleNavigation('settings')}
selected={currentPath === 'settings'}
>
<ListItemIcon>
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Settings</ListItemText>
</MenuItem>
</Menu>
</Box>
</Paper>
);
};
export default Navigation;

View File

@@ -1,38 +0,0 @@
:root {
font-family: 'Roboto', system-ui, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: dark;
}
body {
margin: 0;
min-width: 360px;
min-height: 520px;
overflow-x: hidden;
}
#root {
width: 100%;
height: 100%;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}

View File

@@ -1,64 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import App from './App';
import './index.css';
// Create a dark theme for the extension
const darkTheme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#6200ee',
},
secondary: {
main: '#03dac6',
},
background: {
default: '#121212',
paper: '#1e1e1e',
},
},
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
h1: {
fontSize: '1.5rem',
fontWeight: 600,
},
h2: {
fontSize: '1.25rem',
fontWeight: 600,
},
h3: {
fontSize: '1.125rem',
fontWeight: 600,
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
textTransform: 'none',
},
},
},
MuiPaper: {
styleOverrides: {
root: {
borderRadius: 8,
},
},
},
},
});
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<App />
</ThemeProvider>
</React.StrictMode>
);

View File

@@ -1,392 +0,0 @@
/**
* Cryptographic Operations Page
*
* This page provides a UI for:
* - Encrypting/decrypting data using the keyspace's symmetric cipher
* - Signing/verifying messages using the selected keypair
*/
import { useState, useEffect } from 'react';
import type { SyntheticEvent } from '../types';
import {
Box,
Typography,
TextField,
Button,
Paper,
Tabs,
Tab,
CircularProgress,
Alert,
Divider,
IconButton,
Tooltip,
} from '@mui/material';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { useSessionStore } from '../store/sessionStore';
import { useCryptoStore } from '../store/cryptoStore';
import { useNavigate } from 'react-router-dom';
const CryptoPage = () => {
const navigate = useNavigate();
const { isSessionUnlocked, currentKeypair } = useSessionStore();
const {
encryptData,
decryptData,
signMessage,
verifySignature,
isEncrypting,
isDecrypting,
isSigning,
isVerifying,
error,
clearError
} = useCryptoStore();
const [activeTab, setActiveTab] = useState(0);
const [copySuccess, setCopySuccess] = useState<string | null>(null);
// Encryption state
const [plaintext, setPlaintext] = useState('');
const [encryptedData, setEncryptedData] = useState('');
// Decryption state
const [ciphertext, setCiphertext] = useState('');
const [decryptedData, setDecryptedData] = useState('');
// Signing state
const [messageToSign, setMessageToSign] = useState('');
const [signature, setSignature] = useState('');
// Verification state
const [messageToVerify, setMessageToVerify] = useState('');
const [signatureToVerify, setSignatureToVerify] = useState('');
const [isVerified, setIsVerified] = useState<boolean | null>(null);
// Redirect if not unlocked
useEffect(() => {
if (!isSessionUnlocked) {
navigate('/');
}
}, [isSessionUnlocked, navigate]);
const handleTabChange = (_event: React.SyntheticEvent<Element, Event>, newValue: number) => {
setActiveTab(newValue);
clearError();
setCopySuccess(null);
};
const handleEncrypt = async () => {
try {
const result = await encryptData(plaintext);
setEncryptedData(result);
} catch (err) {
// Error is already handled in the store
}
};
const handleDecrypt = async () => {
try {
const result = await decryptData(ciphertext);
setDecryptedData(result);
} catch (err) {
// Error is already handled in the store
}
};
const handleSign = async () => {
try {
const result = await signMessage(messageToSign);
setSignature(result);
} catch (err) {
// Error is already handled in the store
}
};
const handleVerify = async () => {
try {
const result = await verifySignature(messageToVerify, signatureToVerify);
setIsVerified(result);
} catch (err) {
setIsVerified(false);
// Error is already handled in the store
}
};
const copyToClipboard = (text: string, label: string) => {
navigator.clipboard.writeText(text).then(
() => {
setCopySuccess(`${label} copied to clipboard!`);
setTimeout(() => setCopySuccess(null), 2000);
},
() => {
setCopySuccess('Failed to copy!');
}
);
};
if (!isSessionUnlocked) {
return null; // Will redirect via useEffect
}
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h6" sx={{ mb: 2 }}>Cryptographic Operations</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{copySuccess && (
<Alert severity="success" sx={{ mb: 2 }}>
{copySuccess}
</Alert>
)}
<Paper sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Tabs with smaller width and scrollable */}
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={activeTab}
onChange={handleTabChange}
variant="scrollable"
scrollButtons="auto"
allowScrollButtonsMobile
sx={{ minHeight: '48px' }}
>
<Tab label="Encrypt" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
<Tab label="Decrypt" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
<Tab label="Sign" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
<Tab label="Verify" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
</Tabs>
</Box>
{/* Content area with proper scrolling */}
<Box sx={{ p: 2, flexGrow: 1, overflow: 'auto', height: 'calc(100% - 48px)' }}>
{/* Encryption Tab */}
{activeTab === 0 && (
<Box>
<Typography variant="subtitle1" gutterBottom>Encrypt Data</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
Data will be encrypted using ChaCha20-Poly1305 with a key derived from your keyspace password.
</Typography>
<TextField
label="Data to Encrypt"
multiline
rows={4}
fullWidth
value={plaintext}
onChange={(e) => setPlaintext(e.target.value)}
margin="normal"
/>
<Button
variant="contained"
onClick={handleEncrypt}
disabled={!plaintext || isEncrypting}
sx={{ mt: 2 }}
>
{isEncrypting ? <CircularProgress size={24} /> : 'Encrypt'}
</Button>
{encryptedData && (
<Box sx={{ mt: 3 }}>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle1">Encrypted Result</Typography>
<Box sx={{ position: 'relative' }}>
<TextField
label="Encrypted Data (Base64)"
multiline
rows={4}
fullWidth
value={encryptedData}
InputProps={{ readOnly: true }}
margin="normal"
/>
<Tooltip title="Copy to clipboard">
<IconButton
sx={{ position: 'absolute', top: 8, right: 8 }}
onClick={() => copyToClipboard(encryptedData, 'Encrypted data')}
>
<ContentCopyIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
)}
</Box>
)}
{/* Decryption Tab */}
{activeTab === 1 && (
<Box>
<Typography variant="subtitle1" gutterBottom>Decrypt Data</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
Paste encrypted data (in Base64 format) to decrypt it using your keyspace password.
</Typography>
<TextField
label="Encrypted Data (Base64)"
multiline
rows={4}
fullWidth
value={ciphertext}
onChange={(e) => setCiphertext(e.target.value)}
margin="normal"
/>
<Button
variant="contained"
onClick={handleDecrypt}
disabled={!ciphertext || isDecrypting}
sx={{ mt: 2 }}
>
{isDecrypting ? <CircularProgress size={24} /> : 'Decrypt'}
</Button>
{decryptedData && (
<Box sx={{ mt: 3 }}>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle1">Decrypted Result</Typography>
<Box sx={{ position: 'relative' }}>
<TextField
label="Decrypted Data"
multiline
rows={4}
fullWidth
value={decryptedData}
InputProps={{ readOnly: true }}
margin="normal"
/>
<Tooltip title="Copy to clipboard">
<IconButton
sx={{ position: 'absolute', top: 8, right: 8 }}
onClick={() => copyToClipboard(decryptedData, 'Decrypted data')}
>
<ContentCopyIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
)}
</Box>
)}
{/* Signing Tab */}
{activeTab === 2 && (
<Box>
<Typography variant="subtitle1" gutterBottom>Sign Message</Typography>
{!currentKeypair ? (
<Alert severity="warning" sx={{ mb: 2 }}>
Please select a keypair from the Keypair page before signing messages.
</Alert>
) : (
<Alert severity="info" sx={{ mb: 2 }}>
Signing with keypair: {currentKeypair.name || currentKeypair.id.substring(0, 8)}...
</Alert>
)}
<TextField
label="Message to Sign"
multiline
rows={4}
fullWidth
value={messageToSign}
onChange={(e) => setMessageToSign(e.target.value)}
margin="normal"
disabled={!currentKeypair}
/>
<Button
variant="contained"
onClick={handleSign}
disabled={!messageToSign || !currentKeypair || isSigning}
sx={{ mt: 2 }}
>
{isSigning ? <CircularProgress size={24} /> : 'Sign Message'}
</Button>
{signature && (
<Box sx={{ mt: 3 }}>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle1">Signature</Typography>
<Box sx={{ position: 'relative' }}>
<TextField
label="Signature (Hex)"
multiline
rows={4}
fullWidth
value={signature}
InputProps={{ readOnly: true }}
margin="normal"
/>
<Tooltip title="Copy to clipboard">
<IconButton
sx={{ position: 'absolute', top: 8, right: 8 }}
onClick={() => copyToClipboard(signature, 'Signature')}
>
<ContentCopyIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
)}
</Box>
)}
{/* Verification Tab */}
{activeTab === 3 && (
<Box>
<Typography variant="subtitle1" gutterBottom>Verify Signature</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
Verify that a message was signed by the currently selected keypair.
</Typography>
<TextField
label="Message"
multiline
rows={4}
fullWidth
value={messageToVerify}
onChange={(e) => setMessageToVerify(e.target.value)}
margin="normal"
/>
<TextField
label="Signature (Hex)"
multiline
rows={2}
fullWidth
value={signatureToVerify}
onChange={(e) => setSignatureToVerify(e.target.value)}
margin="normal"
/>
<Button
variant="contained"
onClick={handleVerify}
disabled={!messageToVerify || !signatureToVerify || isVerifying}
sx={{ mt: 2 }}
>
{isVerifying ? <CircularProgress size={24} /> : 'Verify Signature'}
</Button>
{isVerified !== null && (
<Box sx={{ mt: 3 }}>
<Alert severity={isVerified ? "success" : "error"}>
{isVerified
? "Signature is valid! The message was signed by the expected keypair."
: "Invalid signature. The message may have been tampered with or signed by a different keypair."}
</Alert>
</Box>
)}
</Box>
)}
</Box>
</Paper>
</Box>
);
};
export default CryptoPage;

View File

@@ -1,155 +0,0 @@
import { useState } from 'react';
import {
Box,
Typography,
Button,
TextField,
Card,
CardContent,
Stack,
Alert,
CircularProgress
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { useSessionStore } from '../store/sessionStore';
const HomePage = () => {
const navigate = useNavigate();
const { isSessionUnlocked, unlockSession, createKeyspace } = useSessionStore();
const [keyspace, setKeyspace] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<'unlock' | 'create'>('unlock');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setIsLoading(true);
try {
let success = false;
if (mode === 'unlock') {
success = await unlockSession(keyspace, password);
} else {
success = await createKeyspace(keyspace, password);
}
if (success) {
// Navigate to keypair page on success
navigate('/keypair');
} else {
setError(mode === 'unlock'
? 'Failed to unlock keyspace. Check your password and try again.'
: 'Failed to create keyspace. Please try again.');
}
} catch (err) {
setError((err as Error).message || 'An unexpected error occurred');
} finally {
setIsLoading(false);
}
};
if (isSessionUnlocked) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h5" gutterBottom>
Welcome to Hero Vault
</Typography>
<Typography variant="body1" color="text.secondary" paragraph>
Your session is unlocked. You can now use the extension features.
</Typography>
<Stack direction="row" spacing={2} justifyContent="center" mt={3}>
<Button
variant="contained"
color="primary"
onClick={() => navigate('/keypair')}
>
Manage Keys
</Button>
<Button
variant="outlined"
color="secondary"
onClick={() => navigate('/script')}
>
Run Scripts
</Button>
</Stack>
</Box>
);
}
return (
<Box sx={{ maxWidth: 400, mx: 'auto', py: 2 }}>
<Typography variant="h5" align="center" gutterBottom>
Hero Vault
</Typography>
<Card variant="outlined" sx={{ mt: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
{mode === 'unlock' ? 'Unlock Keyspace' : 'Create New Keyspace'}
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<form onSubmit={handleSubmit}>
<TextField
label="Keyspace Name"
value={keyspace}
onChange={(e) => setKeyspace(e.target.value)}
fullWidth
margin="normal"
required
disabled={isLoading}
/>
<TextField
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
fullWidth
margin="normal"
required
disabled={isLoading}
/>
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'space-between' }}>
<Button
variant="text"
onClick={() => setMode(mode === 'unlock' ? 'create' : 'unlock')}
disabled={isLoading}
>
{mode === 'unlock' ? 'Create New Keyspace' : 'Unlock Existing'}
</Button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={isLoading || !keyspace || !password}
>
{isLoading ? (
<CircularProgress size={24} color="inherit" />
) : mode === 'unlock' ? (
'Unlock'
) : (
'Create'
)}
</Button>
</Box>
</form>
</CardContent>
</Card>
</Box>
);
};
export default HomePage;

View File

@@ -1,242 +0,0 @@
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
IconButton,
Divider,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
CircularProgress,
Paper,
Alert,
Chip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import CheckIcon from '@mui/icons-material/Check';
import { useSessionStore } from '../store/sessionStore';
import { useNavigate } from 'react-router-dom';
const KeypairPage = () => {
const navigate = useNavigate();
const {
isSessionUnlocked,
availableKeypairs,
currentKeypair,
listKeypairs,
selectKeypair,
createKeypair
} = useSessionStore();
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [newKeypairName, setNewKeypairName] = useState('');
const [newKeypairType, setNewKeypairType] = useState('Secp256k1');
const [newKeypairDescription, setNewKeypairDescription] = useState('');
const [isCreating, setIsCreating] = useState(false);
// Redirect if not unlocked
useEffect(() => {
if (!isSessionUnlocked) {
navigate('/');
}
}, [isSessionUnlocked, navigate]);
// Load keypairs on mount
useEffect(() => {
const loadKeypairs = async () => {
try {
setIsLoading(true);
await listKeypairs();
} catch (err) {
setError((err as Error).message || 'Failed to load keypairs');
} finally {
setIsLoading(false);
}
};
if (isSessionUnlocked) {
loadKeypairs();
}
}, [isSessionUnlocked, listKeypairs]);
const handleSelectKeypair = async (keypairId: string) => {
try {
setIsLoading(true);
await selectKeypair(keypairId);
} catch (err) {
setError((err as Error).message || 'Failed to select keypair');
} finally {
setIsLoading(false);
}
};
const handleCreateKeypair = async () => {
try {
setIsCreating(true);
setError(null);
await createKeypair(newKeypairType, {
name: newKeypairName,
description: newKeypairDescription
});
setCreateDialogOpen(false);
setNewKeypairName('');
setNewKeypairDescription('');
// Refresh the list
await listKeypairs();
} catch (err) {
setError((err as Error).message || 'Failed to create keypair');
} finally {
setIsCreating(false);
}
};
if (!isSessionUnlocked) {
return null; // Will redirect via useEffect
}
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">Keypair Management</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => setCreateDialogOpen(true)}
disabled={isLoading}
>
Create New
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
) : availableKeypairs.length === 0 ? (
<Paper sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body1" color="text.secondary">
No keypairs found. Create your first keypair to get started.
</Typography>
</Paper>
) : (
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
<List disablePadding>
{availableKeypairs.map((keypair: any, index: number) => (
<Box key={keypair.id}>
{index > 0 && <Divider />}
<ListItem
button
selected={currentKeypair?.id === keypair.id}
onClick={() => handleSelectKeypair(keypair.id)}
>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{keypair.name || keypair.id}
<Chip
label={keypair.type}
size="small"
color="primary"
variant="outlined"
/>
</Box>
}
secondary={
<Typography variant="body2" color="text.secondary">
{keypair.description || 'No description'}
<br />
Created: {new Date(keypair.createdAt).toLocaleString()}
</Typography>
}
/>
<ListItemSecondaryAction>
{currentKeypair?.id === keypair.id && (
<IconButton edge="end" disabled>
<CheckIcon color="success" />
</IconButton>
)}
</ListItemSecondaryAction>
</ListItem>
</Box>
))}
</List>
</Paper>
)}
{/* Create Keypair Dialog */}
<Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Create New Keypair</DialogTitle>
<DialogContent>
<TextField
label="Name"
value={newKeypairName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewKeypairName(e.target.value)}
fullWidth
margin="normal"
disabled={isCreating}
/>
<FormControl fullWidth margin="normal">
<InputLabel>Type</InputLabel>
<Select
value={newKeypairType}
onChange={(e) => setNewKeypairType(e.target.value)}
disabled={isCreating}
>
<MenuItem value="Ed25519">Ed25519</MenuItem>
<MenuItem value="Secp256k1">Secp256k1 (Ethereum)</MenuItem>
</Select>
</FormControl>
<TextField
label="Description"
value={newKeypairDescription}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewKeypairDescription(e.target.value)}
fullWidth
margin="normal"
multiline
rows={2}
disabled={isCreating}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setCreateDialogOpen(false)} disabled={isCreating}>
Cancel
</Button>
<Button
onClick={handleCreateKeypair}
color="primary"
variant="contained"
disabled={isCreating || !newKeypairName}
>
{isCreating ? <CircularProgress size={24} /> : 'Create'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default KeypairPage;

View File

@@ -1,557 +0,0 @@
import { useState, useEffect } from 'react';
import { getChromeApi } from '../utils/chromeApi';
import {
Box,
Typography,
Button,
TextField,
Paper,
Alert,
CircularProgress,
Divider,
Tabs,
Tab,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Chip
} from '@mui/material';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import VisibilityIcon from '@mui/icons-material/Visibility';
// DeleteIcon removed as it's not used
import { useNavigate } from 'react-router-dom';
import { useSessionStore } from '../store/sessionStore';
interface ScriptResult {
id: string;
timestamp: number;
script: string;
result: string;
success: boolean;
}
interface PendingScript {
id: string;
title: string;
description: string;
script: string;
tags: string[];
timestamp: number;
}
const ScriptPage = () => {
const navigate = useNavigate();
const { isSessionUnlocked, currentKeypair } = useSessionStore();
const [tabValue, setTabValue] = useState<number>(0);
const [scriptInput, setScriptInput] = useState<string>('');
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [executionResult, setExecutionResult] = useState<string | null>(null);
const [executionSuccess, setExecutionSuccess] = useState<boolean | null>(null);
const [scriptResults, setScriptResults] = useState<ScriptResult[]>([]);
const [pendingScripts, setPendingScripts] = useState<PendingScript[]>([]);
const [selectedPendingScript, setSelectedPendingScript] = useState<PendingScript | null>(null);
const [scriptDialogOpen, setScriptDialogOpen] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
// Redirect if not unlocked
useEffect(() => {
if (!isSessionUnlocked) {
navigate('/');
}
}, [isSessionUnlocked, navigate]);
// Load pending scripts from storage
useEffect(() => {
const loadPendingScripts = async () => {
try {
const chromeApi = getChromeApi();
const data = await chromeApi.storage.local.get('pendingScripts');
if (data.pendingScripts) {
setPendingScripts(data.pendingScripts);
}
} catch (err) {
console.error('Failed to load pending scripts:', err);
}
};
if (isSessionUnlocked) {
loadPendingScripts();
}
}, [isSessionUnlocked]);
// Load script history from storage
useEffect(() => {
const loadScriptResults = async () => {
try {
const chromeApi = getChromeApi();
const data = await chromeApi.storage.local.get('scriptResults');
if (data.scriptResults) {
setScriptResults(data.scriptResults);
}
} catch (err) {
console.error('Failed to load script results:', err);
}
};
if (isSessionUnlocked) {
loadScriptResults();
}
}, [isSessionUnlocked]);
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const handleExecuteScript = async () => {
if (!scriptInput.trim()) return;
setIsExecuting(true);
setError(null);
setExecutionResult(null);
setExecutionSuccess(null);
try {
// Call the WASM run_rhai function via our store
const result = await useSessionStore.getState().executeScript(scriptInput);
setExecutionResult(result);
setExecutionSuccess(true);
// Save to history
const newResult: ScriptResult = {
id: `script-${Date.now()}`,
timestamp: Date.now(),
script: scriptInput,
result,
success: true
};
const updatedResults = [newResult, ...scriptResults].slice(0, 20); // Keep last 20
setScriptResults(updatedResults);
// Save to storage
const chromeApi = getChromeApi();
await chromeApi.storage.local.set({ scriptResults: updatedResults });
} catch (err) {
setError((err as Error).message || 'Failed to execute script');
setExecutionSuccess(false);
setExecutionResult('Execution failed');
} finally {
setIsExecuting(false);
}
};
const handleViewPendingScript = (script: PendingScript) => {
setSelectedPendingScript(script);
setScriptDialogOpen(true);
};
const handleApprovePendingScript = async () => {
if (!selectedPendingScript) return;
setScriptDialogOpen(false);
setScriptInput(selectedPendingScript.script);
setTabValue(0); // Switch to execute tab
// Remove from pending list
const updatedPendingScripts = pendingScripts.filter(
script => script.id !== selectedPendingScript.id
);
setPendingScripts(updatedPendingScripts);
const chromeApi = getChromeApi();
await chromeApi.storage.local.set({ pendingScripts: updatedPendingScripts });
setSelectedPendingScript(null);
};
const handleRejectPendingScript = async () => {
if (!selectedPendingScript) return;
// Remove from pending list
const updatedPendingScripts = pendingScripts.filter(
script => script.id !== selectedPendingScript.id
);
setPendingScripts(updatedPendingScripts);
const chromeApi = getChromeApi();
await chromeApi.storage.local.set({ pendingScripts: updatedPendingScripts });
setScriptDialogOpen(false);
setSelectedPendingScript(null);
};
const handleClearHistory = async () => {
setScriptResults([]);
const chromeApi = getChromeApi();
await chromeApi.storage.local.set({ scriptResults: [] });
};
if (!isSessionUnlocked) {
return null; // Will redirect via useEffect
}
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={tabValue}
onChange={handleTabChange}
aria-label="script tabs"
variant="scrollable"
scrollButtons="auto"
allowScrollButtonsMobile
sx={{ minHeight: '48px' }}
>
<Tab label="Execute" sx={{ minHeight: '48px', py: 0 }} />
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
Pending
{pendingScripts.length > 0 && (
<Chip
label={pendingScripts.length}
size="small"
color="primary"
sx={{ ml: 1 }}
/>
)}
</Box>
}
sx={{ minHeight: '48px', py: 0 }}
/>
<Tab label="History" sx={{ minHeight: '48px', py: 0 }} />
</Tabs>
</Box>
{/* Execute Tab */}
{tabValue === 0 && (
<Box sx={{
p: 2,
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
height: 'calc(100% - 48px)' // Subtract tab height
}}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
height: '100%',
pb: 2 // Add padding at bottom for scrolling
}}>
{!currentKeypair && (
<Alert severity="warning" sx={{ mb: 2 }}>
No keypair selected. Select a keypair to enable script execution with signing capabilities.
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<TextField
label="Rhai Script"
multiline
rows={6} // Reduced from 8 to leave more space for results
value={scriptInput}
onChange={(e) => setScriptInput(e.target.value)}
fullWidth
variant="outlined"
placeholder="Enter your Rhai script here..."
sx={{ mb: 2 }}
disabled={isExecuting}
/>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button
variant="contained"
color="primary"
startIcon={<PlayArrowIcon />}
onClick={handleExecuteScript}
disabled={isExecuting || !scriptInput.trim()}
>
{isExecuting ? <CircularProgress size={24} /> : 'Execute'}
</Button>
</Box>
{executionResult && (
<Paper
variant="outlined"
sx={{
p: 2,
bgcolor: executionSuccess ? 'success.dark' : 'error.dark',
color: 'white',
overflowY: 'auto',
mb: 2, // Add margin at bottom
minHeight: '100px', // Ensure minimum height for visibility
maxHeight: '200px' // Limit maximum height
}}
>
<Typography variant="subtitle2" gutterBottom>
Execution Result:
</Typography>
<Typography
variant="body2"
component="pre"
sx={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace'
}}
>
{executionResult}
</Typography>
</Paper>
)}
</Box>
</Box>
)}
{/* Pending Scripts Tab */}
{tabValue === 1 && (
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
{pendingScripts.length === 0 ? (
<Paper sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body1" color="text.secondary">
No pending scripts. Incoming scripts from connected WebSocket servers will appear here.
</Typography>
</Paper>
) : (
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
<List disablePadding>
{pendingScripts.map((script, index) => (
<Box key={script.id}>
{index > 0 && <Divider />}
<ListItem>
<ListItemText
primary={script.title}
secondary={
<>
<Typography variant="body2" color="text.secondary">
{script.description || 'No description'}
</Typography>
<Box sx={{ mt: 0.5 }}>
{script.tags.map(tag => (
<Chip
key={tag}
label={tag}
size="small"
color={tag === 'remote' ? 'secondary' : 'primary'}
variant="outlined"
sx={{ mr: 0.5 }}
/>
))}
</Box>
</>
}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
onClick={() => handleViewPendingScript(script)}
aria-label="view script"
>
<VisibilityIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</Box>
))}
</List>
</Paper>
)}
</Box>
)}
{/* History Tab */}
{tabValue === 2 && (
<Box sx={{
p: 2,
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
height: 'calc(100% - 48px)' // Subtract tab height
}}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
height: '100%',
pb: 2 // Add padding at bottom for scrolling
}}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
<Button
variant="outlined"
color="error"
size="small"
onClick={handleClearHistory}
disabled={scriptResults.length === 0}
>
Clear History
</Button>
</Box>
{scriptResults.length === 0 ? (
<Paper sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body1" color="text.secondary">
No script execution history yet.
</Typography>
</Paper>
) : (
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
<List disablePadding>
{scriptResults.map((result, index) => (
<Box key={result.id}>
{index > 0 && <Divider />}
<ListItem>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle2">
{new Date(result.timestamp).toLocaleString()}
</Typography>
<Chip
label={result.success ? 'Success' : 'Failed'}
size="small"
color={result.success ? 'success' : 'error'}
variant="outlined"
/>
</Box>
}
secondary={
<Typography
variant="body2"
color="text.secondary"
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '280px'
}}
>
{result.script}
</Typography>
}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
onClick={() => {
setScriptInput(result.script);
setTabValue(0);
}}
aria-label="reuse script"
>
<PlayArrowIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</Box>
))}
</List>
</Paper>
)}
</Box>
</Box>
)}
{/* Pending Script Dialog */}
<Dialog
open={scriptDialogOpen}
onClose={() => setScriptDialogOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>
{selectedPendingScript?.title || 'Script Details'}
</DialogTitle>
<DialogContent>
{selectedPendingScript && (
<>
<Typography variant="subtitle2" gutterBottom>
Description:
</Typography>
<Typography variant="body2" paragraph>
{selectedPendingScript.description || 'No description provided'}
</Typography>
<Box sx={{ mb: 2 }}>
{selectedPendingScript.tags.map(tag => (
<Chip
key={tag}
label={tag}
size="small"
color={tag === 'remote' ? 'secondary' : 'primary'}
sx={{ mr: 0.5 }}
/>
))}
</Box>
<Typography variant="subtitle2" gutterBottom>
Script Content:
</Typography>
<Paper
variant="outlined"
sx={{
p: 2,
bgcolor: 'background.paper',
maxHeight: '300px',
overflow: 'auto'
}}
>
<Typography
variant="body2"
component="pre"
sx={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace'
}}
>
{selectedPendingScript.script}
</Typography>
</Paper>
<Alert severity="warning" sx={{ mt: 2 }}>
<Typography variant="body2">
{selectedPendingScript.tags.includes('remote')
? 'This is a remote script. If approved, your signature will be sent to the server and the script may execute remotely.'
: 'This script will execute locally in your browser extension if approved.'}
</Typography>
</Alert>
</>
)}
</DialogContent>
<DialogActions>
<Button
onClick={handleRejectPendingScript}
color="error"
variant="outlined"
>
Reject
</Button>
<Button
onClick={handleApprovePendingScript}
color="primary"
variant="contained"
>
Approve
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default ScriptPage;

View File

@@ -1,191 +0,0 @@
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
Paper,
Alert,
CircularProgress,
List,
ListItem,
ListItemText,
Divider,
Card,
CardContent,
Grid
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { useSessionStore } from '../store/sessionStore';
import LockIcon from '@mui/icons-material/Lock';
import SecurityIcon from '@mui/icons-material/Security';
// HistoryIcon removed as it's not used
interface SessionActivity {
id: string;
action: string;
timestamp: number;
details?: string;
}
const SessionPage = () => {
const navigate = useNavigate();
const {
isSessionUnlocked,
currentKeyspace,
currentKeypair,
lockSession
} = useSessionStore();
const [sessionActivities, setSessionActivities] = useState<SessionActivity[]>([]);
const [isLoading, setIsLoading] = useState(false);
// Redirect if not unlocked
useEffect(() => {
if (!isSessionUnlocked) {
navigate('/');
}
}, [isSessionUnlocked, navigate]);
// Load session activities from storage
useEffect(() => {
const loadSessionActivities = async () => {
try {
setIsLoading(true);
const data = await chrome.storage.local.get('sessionActivities');
if (data.sessionActivities) {
setSessionActivities(data.sessionActivities);
}
} catch (err) {
console.error('Failed to load session activities:', err);
} finally {
setIsLoading(false);
}
};
if (isSessionUnlocked) {
loadSessionActivities();
}
}, [isSessionUnlocked]);
const handleLockSession = async () => {
try {
await lockSession();
navigate('/');
} catch (err) {
console.error('Failed to lock session:', err);
}
};
if (!isSessionUnlocked) {
return null; // Will redirect via useEffect
}
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h6" gutterBottom>
Session Management
</Typography>
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6}>
<Card variant="outlined">
<CardContent>
<Typography color="text.secondary" gutterBottom>
Current Keyspace
</Typography>
<Typography variant="h5" component="div">
{currentKeyspace || 'None'}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6}>
<Card variant="outlined">
<CardContent>
<Typography color="text.secondary" gutterBottom>
Selected Keypair
</Typography>
<Typography variant="h5" component="div">
{currentKeypair?.name || currentKeypair?.id || 'None'}
</Typography>
{currentKeypair && (
<Typography variant="body2" color="text.secondary">
Type: {currentKeypair.type}
</Typography>
)}
</CardContent>
</Card>
</Grid>
</Grid>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1">
Session Activity
</Typography>
<Button
variant="outlined"
color="error"
startIcon={<LockIcon />}
onClick={handleLockSession}
>
Lock Session
</Button>
</Box>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
) : sessionActivities.length === 0 ? (
<Paper sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body1" color="text.secondary">
No session activity recorded yet.
</Typography>
</Paper>
) : (
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
<List disablePadding>
{sessionActivities.map((activity, index) => (
<Box key={activity.id}>
{index > 0 && <Divider />}
<ListItem>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle2">
{activity.action}
</Typography>
</Box>
}
secondary={
<>
<Typography variant="body2" color="text.secondary">
{new Date(activity.timestamp).toLocaleString()}
</Typography>
{activity.details && (
<Typography variant="body2" color="text.secondary">
{activity.details}
</Typography>
)}
</>
}
/>
</ListItem>
</Box>
))}
</List>
</Paper>
)}
<Box sx={{ mt: 3 }}>
<Alert severity="info" icon={<SecurityIcon />}>
Your session is active. All cryptographic operations and script executions require explicit approval.
</Alert>
</Box>
</Box>
);
};
export default SessionPage;

View File

@@ -1,246 +0,0 @@
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Switch,
// FormControlLabel removed as it's not used
Divider,
Paper,
List,
ListItem,
ListItemText,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Alert,
Snackbar
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import InfoIcon from '@mui/icons-material/Info';
interface Settings {
darkMode: boolean;
autoLockTimeout: number; // minutes
confirmCryptoOperations: boolean;
showScriptNotifications: boolean;
}
const SettingsPage = () => {
const [settings, setSettings] = useState<Settings>({
darkMode: true,
autoLockTimeout: 15,
confirmCryptoOperations: true,
showScriptNotifications: true
});
const [clearDataDialogOpen, setClearDataDialogOpen] = useState(false);
const [confirmText, setConfirmText] = useState('');
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
// Load settings from storage
useEffect(() => {
const loadSettings = async () => {
try {
const data = await chrome.storage.local.get('settings');
if (data.settings) {
setSettings(data.settings);
}
} catch (err) {
console.error('Failed to load settings:', err);
}
};
loadSettings();
}, []);
// Save settings when changed
const handleSettingChange = (key: keyof Settings, value: boolean | number) => {
const updatedSettings = { ...settings, [key]: value };
setSettings(updatedSettings);
// Save to storage
chrome.storage.local.set({ settings: updatedSettings })
.then(() => {
setSnackbarMessage('Settings saved');
setSnackbarOpen(true);
})
.catch(err => {
console.error('Failed to save settings:', err);
setSnackbarMessage('Failed to save settings');
setSnackbarOpen(true);
});
};
const handleClearAllData = () => {
if (confirmText !== 'CLEAR ALL DATA') {
setSnackbarMessage('Please type the confirmation text exactly');
setSnackbarOpen(true);
return;
}
// Clear all extension data
chrome.storage.local.clear()
.then(() => {
setSnackbarMessage('All data cleared successfully');
setSnackbarOpen(true);
setClearDataDialogOpen(false);
setConfirmText('');
// Reset settings to defaults
setSettings({
darkMode: true,
autoLockTimeout: 15,
confirmCryptoOperations: true,
showScriptNotifications: true
});
})
.catch(err => {
console.error('Failed to clear data:', err);
setSnackbarMessage('Failed to clear data');
setSnackbarOpen(true);
});
};
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h6" gutterBottom>
Settings
</Typography>
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
<List disablePadding>
<ListItem>
<ListItemText
primary="Dark Mode"
secondary="Use dark theme for the extension"
/>
<Switch
edge="end"
checked={settings.darkMode}
onChange={(e) => handleSettingChange('darkMode', e.target.checked)}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemText
primary="Auto-Lock Timeout"
secondary={`Automatically lock session after ${settings.autoLockTimeout} minutes of inactivity`}
/>
<Box sx={{ width: 120 }}>
<TextField
type="number"
size="small"
value={settings.autoLockTimeout}
onChange={(e) => {
const value = parseInt(e.target.value);
if (!isNaN(value) && value >= 1) {
handleSettingChange('autoLockTimeout', value);
}
}}
InputProps={{ inputProps: { min: 1, max: 60 } }}
/>
</Box>
</ListItem>
<Divider />
<ListItem>
<ListItemText
primary="Confirm Cryptographic Operations"
secondary="Always ask for confirmation before signing or encrypting"
/>
<Switch
edge="end"
checked={settings.confirmCryptoOperations}
onChange={(e) => handleSettingChange('confirmCryptoOperations', e.target.checked)}
/>
</ListItem>
<Divider />
<ListItem>
<ListItemText
primary="Script Notifications"
secondary="Show notifications when new scripts are received"
/>
<Switch
edge="end"
checked={settings.showScriptNotifications}
onChange={(e) => handleSettingChange('showScriptNotifications', e.target.checked)}
/>
</ListItem>
</List>
</Paper>
<Box sx={{ mt: 3 }}>
<Alert
severity="info"
icon={<InfoIcon />}
sx={{ mb: 2 }}
>
<Typography variant="body2">
The extension stores all cryptographic keys in encrypted form. Your password is never stored and is only kept in memory while the session is unlocked.
</Typography>
</Alert>
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={() => setClearDataDialogOpen(true)}
fullWidth
>
Clear All Data
</Button>
</Box>
{/* Clear Data Confirmation Dialog */}
<Dialog open={clearDataDialogOpen} onClose={() => setClearDataDialogOpen(false)}>
<DialogTitle>Clear All Extension Data</DialogTitle>
<DialogContent>
<Typography variant="body1" paragraph>
This will permanently delete all your keyspaces, keypairs, and settings. This action cannot be undone.
</Typography>
<Typography variant="body2" color="error" paragraph>
Type "CLEAR ALL DATA" to confirm:
</Typography>
<TextField
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
fullWidth
variant="outlined"
placeholder="CLEAR ALL DATA"
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setClearDataDialogOpen(false)}>
Cancel
</Button>
<Button
onClick={handleClearAllData}
color="error"
disabled={confirmText !== 'CLEAR ALL DATA'}
>
Clear All Data
</Button>
</DialogActions>
</Dialog>
{/* Snackbar for notifications */}
<Snackbar
open={snackbarOpen}
autoHideDuration={3000}
onClose={() => setSnackbarOpen(false)}
message={snackbarMessage}
/>
</Box>
);
};
export default SettingsPage;

View File

@@ -1,248 +0,0 @@
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
TextField,
Paper,
Alert,
CircularProgress,
List,
ListItem,
ListItemText,
Divider,
Chip
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { useSessionStore } from '../store/sessionStore';
interface ConnectionHistory {
id: string;
url: string;
timestamp: number;
status: 'connected' | 'disconnected';
}
const WebSocketPage = () => {
const navigate = useNavigate();
const {
isSessionUnlocked,
currentKeypair,
isWebSocketConnected,
webSocketUrl,
connectWebSocket,
disconnectWebSocket
} = useSessionStore();
const [serverUrl, setServerUrl] = useState('');
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [connectionHistory, setConnectionHistory] = useState<ConnectionHistory[]>([]);
// Redirect if not unlocked
useEffect(() => {
if (!isSessionUnlocked) {
navigate('/');
}
}, [isSessionUnlocked, navigate]);
// Load connection history from storage
useEffect(() => {
const loadConnectionHistory = async () => {
try {
const data = await chrome.storage.local.get('connectionHistory');
if (data.connectionHistory) {
setConnectionHistory(data.connectionHistory);
}
} catch (err) {
console.error('Failed to load connection history:', err);
}
};
if (isSessionUnlocked) {
loadConnectionHistory();
}
}, [isSessionUnlocked]);
const handleConnect = async () => {
if (!serverUrl.trim() || !currentKeypair) return;
setIsConnecting(true);
setError(null);
try {
const success = await connectWebSocket(serverUrl);
if (success) {
// Add to connection history
const newConnection: ConnectionHistory = {
id: `conn-${Date.now()}`,
url: serverUrl,
timestamp: Date.now(),
status: 'connected'
};
const updatedHistory = [newConnection, ...connectionHistory].slice(0, 10); // Keep last 10
setConnectionHistory(updatedHistory);
// Save to storage
await chrome.storage.local.set({ connectionHistory: updatedHistory });
} else {
throw new Error('Failed to connect to WebSocket server');
}
} catch (err) {
setError((err as Error).message || 'Failed to connect to WebSocket server');
} finally {
setIsConnecting(false);
}
};
const handleDisconnect = async () => {
try {
const success = await disconnectWebSocket();
if (success && webSocketUrl) {
// Update connection history
const updatedHistory = connectionHistory.map(conn =>
conn.url === webSocketUrl && conn.status === 'connected'
? { ...conn, status: 'disconnected' }
: conn
);
setConnectionHistory(updatedHistory);
// Save to storage
await chrome.storage.local.set({ connectionHistory: updatedHistory });
}
} catch (err) {
setError((err as Error).message || 'Failed to disconnect from WebSocket server');
}
};
const handleQuickConnect = (url: string) => {
setServerUrl(url);
// Don't auto-connect to avoid unexpected connections
};
if (!isSessionUnlocked) {
return null; // Will redirect via useEffect
}
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h6" gutterBottom>
WebSocket Connection
</Typography>
{!currentKeypair && (
<Alert severity="warning" sx={{ mb: 2 }}>
No keypair selected. Select a keypair before connecting to a WebSocket server.
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Paper variant="outlined" sx={{ p: 2, mb: 2 }}>
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Connection Status:
</Typography>
<Chip
label={isWebSocketConnected ? 'Connected' : 'Disconnected'}
color={isWebSocketConnected ? 'success' : 'default'}
variant="outlined"
/>
{isWebSocketConnected && webSocketUrl && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
Connected to: {webSocketUrl}
</Typography>
)}
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<TextField
label="WebSocket Server URL"
placeholder="wss://example.com/ws"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
fullWidth
disabled={isConnecting || isWebSocketConnected || !currentKeypair}
/>
{isWebSocketConnected ? (
<Button
variant="outlined"
color="error"
onClick={handleDisconnect}
>
Disconnect
</Button>
) : (
<Button
variant="contained"
color="primary"
onClick={handleConnect}
disabled={isConnecting || !serverUrl.trim() || !currentKeypair}
>
{isConnecting ? <CircularProgress size={24} /> : 'Connect'}
</Button>
)}
</Box>
</Paper>
<Typography variant="subtitle1" gutterBottom>
Connection History
</Typography>
{connectionHistory.length === 0 ? (
<Paper sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body1" color="text.secondary">
No connection history yet.
</Typography>
</Paper>
) : (
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
<List disablePadding>
{connectionHistory.map((conn, index) => (
<Box key={conn.id}>
{index > 0 && <Divider />}
<ListItem
button
onClick={() => handleQuickConnect(conn.url)}
disabled={isWebSocketConnected}
>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle2">
{conn.url}
</Typography>
<Chip
label={conn.status}
size="small"
color={conn.status === 'connected' ? 'success' : 'default'}
variant="outlined"
/>
</Box>
}
secondary={
<Typography variant="body2" color="text.secondary">
{new Date(conn.timestamp).toLocaleString()}
</Typography>
}
/>
</ListItem>
</Box>
))}
</List>
</Paper>
)}
</Box>
);
};
export default WebSocketPage;

View File

@@ -1,144 +0,0 @@
/**
* Crypto Store for Hero Vault Extension
*
* This store manages cryptographic operations such as:
* - Encryption/decryption using the keyspace's symmetric cipher
* - Signing/verification using the selected keypair
*/
import { create } from 'zustand';
import { getWasmModule, stringToUint8Array, uint8ArrayToString } from '../wasm/wasmHelper';
// Helper functions for Unicode-safe base64 encoding/decoding
function base64Encode(data: Uint8Array): string {
// Convert binary data to a string that only uses the low 8 bits of each character
const binaryString = Array.from(data)
.map(byte => String.fromCharCode(byte))
.join('');
// Use btoa on the binary string
return btoa(binaryString);
}
function base64Decode(base64: string): Uint8Array {
// Decode base64 to binary string
const binaryString = atob(base64);
// Convert binary string to Uint8Array
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
interface CryptoState {
// State
isEncrypting: boolean;
isDecrypting: boolean;
isSigning: boolean;
isVerifying: boolean;
error: string | null;
// Actions
encryptData: (data: string) => Promise<string>;
decryptData: (encrypted: string) => Promise<string>;
signMessage: (message: string) => Promise<string>;
verifySignature: (message: string, signature: string) => Promise<boolean>;
clearError: () => void;
}
export const useCryptoStore = create<CryptoState>()((set, get) => ({
isEncrypting: false,
isDecrypting: false,
isSigning: false,
isVerifying: false,
error: null,
encryptData: async (data: string) => {
try {
set({ isEncrypting: true, error: null });
const wasmModule = await getWasmModule();
// Convert input to Uint8Array
const dataBytes = stringToUint8Array(data);
// Encrypt the data
const encrypted = await wasmModule.encrypt_data(dataBytes);
// Convert result to base64 for storage/display using our Unicode-safe function
const encryptedBase64 = base64Encode(encrypted);
return encryptedBase64;
} catch (error) {
set({ error: (error as Error).message || 'Failed to encrypt data' });
throw error;
} finally {
set({ isEncrypting: false });
}
},
decryptData: async (encrypted: string) => {
try {
set({ isDecrypting: true, error: null });
const wasmModule = await getWasmModule();
// Convert input from base64 using our Unicode-safe function
const encryptedBytes = base64Decode(encrypted);
// Decrypt the data
const decrypted = await wasmModule.decrypt_data(encryptedBytes);
// Convert result to string
return uint8ArrayToString(decrypted);
} catch (error) {
set({ error: (error as Error).message || 'Failed to decrypt data' });
throw error;
} finally {
set({ isDecrypting: false });
}
},
signMessage: async (message: string) => {
try {
set({ isSigning: true, error: null });
const wasmModule = await getWasmModule();
// Convert message to Uint8Array
const messageBytes = stringToUint8Array(message);
// Sign the message
const signature = await wasmModule.sign(messageBytes);
return signature;
} catch (error) {
set({ error: (error as Error).message || 'Failed to sign message' });
throw error;
} finally {
set({ isSigning: false });
}
},
verifySignature: async (message: string, signature: string) => {
try {
set({ isVerifying: true, error: null });
const wasmModule = await getWasmModule();
// Convert inputs
const messageBytes = stringToUint8Array(message);
// Verify the signature
const isValid = await wasmModule.verify(messageBytes, signature);
return isValid;
} catch (error) {
set({ error: (error as Error).message || 'Failed to verify signature' });
throw error;
} finally {
set({ isVerifying: false });
}
},
clearError: () => set({ error: null })
}));

View File

@@ -1,416 +0,0 @@
import { create } from 'zustand';
import { getWasmModule, stringToUint8Array } from '../wasm/wasmHelper';
import { getChromeApi } from '../utils/chromeApi';
// Import Chrome types
/// <reference types="chrome" />
interface KeypairMetadata {
id: string;
type: string;
name?: string;
description?: string;
createdAt: number;
}
interface SessionState {
isSessionUnlocked: boolean;
currentKeyspace: string | null;
currentKeypair: KeypairMetadata | null;
availableKeypairs: KeypairMetadata[];
isWebSocketConnected: boolean;
webSocketUrl: string | null;
isWasmLoaded: boolean;
// Actions
initWasm: () => Promise<boolean>;
checkSessionStatus: () => Promise<boolean>;
unlockSession: (keyspace: string, password: string) => Promise<boolean>;
lockSession: () => Promise<boolean>;
createKeyspace: (keyspace: string, password: string) => Promise<boolean>;
listKeypairs: () => Promise<KeypairMetadata[]>;
selectKeypair: (keypairId: string) => Promise<boolean>;
createKeypair: (type: string, metadata?: Record<string, any>) => Promise<string>;
connectWebSocket: (url: string) => Promise<boolean>;
disconnectWebSocket: () => Promise<boolean>;
executeScript: (script: string) => Promise<string>;
signMessage: (message: string) => Promise<string>;
}
// Create the store
export const useSessionStore = create<SessionState>((set: any, get: any) => ({
isSessionUnlocked: false,
currentKeyspace: null,
currentKeypair: null,
availableKeypairs: [],
isWebSocketConnected: false,
webSocketUrl: null,
isWasmLoaded: false,
// Initialize WASM module
initWasm: async () => {
try {
set({ isWasmLoaded: true });
return true;
} catch (error) {
console.error('Failed to initialize WASM module:', error);
return false;
}
},
// Check if a session is currently active
checkSessionStatus: async () => {
try {
// First check with the background service worker
const chromeApi = getChromeApi();
const response = await chromeApi.runtime.sendMessage({ type: 'SESSION_STATUS' });
if (response && response.active) {
// If session is active in the background, check with WASM
try {
const wasmModule = await getWasmModule();
const isUnlocked = wasmModule.is_unlocked();
if (isUnlocked) {
// Get current keypair metadata if available
try {
const keypairMetadata = await wasmModule.current_keypair_metadata();
const parsedMetadata = JSON.parse(keypairMetadata);
set({
isSessionUnlocked: true,
currentKeypair: parsedMetadata
});
// Load keypairs
await get().listKeypairs();
} catch (e) {
// No keypair selected, but session is unlocked
set({ isSessionUnlocked: true });
}
return true;
}
} catch (wasmError) {
console.error('WASM error checking session status:', wasmError);
}
}
set({ isSessionUnlocked: false });
return false;
} catch (error) {
console.error('Failed to check session status:', error);
set({ isSessionUnlocked: false });
return false;
}
},
// Unlock a session with keyspace and password
unlockSession: async (keyspace: string, password: string) => {
try {
const wasmModule = await getWasmModule();
// Call the WASM init_session function
await wasmModule.init_session(keyspace, password);
// Initialize Rhai environment
wasmModule.init_rhai_env();
// Notify background service worker
const chromeApi = getChromeApi();
await chromeApi.runtime.sendMessage({ type: 'SESSION_UNLOCK' });
set({
isSessionUnlocked: true,
currentKeyspace: keyspace,
currentKeypair: null
});
// Load keypairs after unlocking
const keypairs = await get().listKeypairs();
set({ availableKeypairs: keypairs });
return true;
} catch (error) {
console.error('Failed to unlock session:', error);
return false;
}
},
// Lock the current session
lockSession: async () => {
try {
const wasmModule = await getWasmModule();
// Call the WASM lock_session function
wasmModule.lock_session();
// Notify background service worker
const chromeApi = getChromeApi();
await chromeApi.runtime.sendMessage({ type: 'SESSION_LOCK' });
set({
isSessionUnlocked: false,
currentKeyspace: null,
currentKeypair: null,
availableKeypairs: [],
isWebSocketConnected: false,
webSocketUrl: null
});
return true;
} catch (error) {
console.error('Failed to lock session:', error);
return false;
}
},
// Create a new keyspace
createKeyspace: async (keyspace: string, password: string) => {
try {
const wasmModule = await getWasmModule();
// Call the WASM create_keyspace function
await wasmModule.create_keyspace(keyspace, password);
// Initialize Rhai environment
wasmModule.init_rhai_env();
// Notify background service worker
const chromeApi = getChromeApi();
await chromeApi.runtime.sendMessage({ type: 'SESSION_UNLOCK' });
set({
isSessionUnlocked: true,
currentKeyspace: keyspace,
currentKeypair: null,
availableKeypairs: []
});
return true;
} catch (error) {
console.error('Failed to create keyspace:', error);
return false;
}
},
// List all keypairs in the current keyspace
listKeypairs: async () => {
try {
console.log('Listing keypairs from WASM module');
const wasmModule = await getWasmModule();
console.log('WASM module loaded, calling list_keypairs');
// Call the WASM list_keypairs function
let keypairsJson;
try {
keypairsJson = await wasmModule.list_keypairs();
console.log('Raw keypairs JSON from WASM:', keypairsJson);
} catch (listError) {
console.error('Error calling list_keypairs:', listError);
throw new Error(`Failed to list keypairs: ${listError.message || listError}`);
}
let keypairs;
try {
keypairs = JSON.parse(keypairsJson);
console.log('Parsed keypairs object:', keypairs);
} catch (parseError) {
console.error('Error parsing keypairs JSON:', parseError);
throw new Error(`Failed to parse keypairs JSON: ${parseError.message}`);
}
// Transform the keypairs to our expected format
const formattedKeypairs: KeypairMetadata[] = keypairs.map((keypair: any, index: number) => {
console.log(`Processing keypair at index ${index}:`, keypair);
return {
id: keypair.id, // Use the actual keypair ID from the WASM module
type: keypair.key_type || 'Unknown',
name: keypair.metadata?.name,
description: keypair.metadata?.description,
createdAt: keypair.metadata?.created_at || Date.now()
};
});
console.log('Formatted keypairs for UI:', formattedKeypairs);
set({ availableKeypairs: formattedKeypairs });
return formattedKeypairs;
} catch (error) {
console.error('Failed to list keypairs:', error);
return [];
}
},
// Select a keypair for use
selectKeypair: async (keypairId: string) => {
try {
console.log('Selecting keypair with ID:', keypairId);
// First, let's log the available keypairs to see what we have
const { availableKeypairs } = get();
console.log('Available keypairs:', JSON.stringify(availableKeypairs));
const wasmModule = await getWasmModule();
console.log('WASM module loaded, attempting to select keypair');
try {
// Call the WASM select_keypair function
await wasmModule.select_keypair(keypairId);
console.log('Successfully selected keypair in WASM');
} catch (selectError) {
console.error('Error in WASM select_keypair:', selectError);
throw new Error(`select_keypair error: ${selectError.message || selectError}`);
}
// Find the keypair in our availableKeypairs list
const selectedKeypair = availableKeypairs.find((kp: KeypairMetadata) => kp.id === keypairId);
if (selectedKeypair) {
console.log('Found keypair in available list, setting as current');
set({ currentKeypair: selectedKeypair });
} else {
console.log('Keypair not found in available list, creating new entry from available data');
// If not found in our list (rare case), create a new entry with what we know
// Since we can't get metadata from WASM, use what we have from the keypair list
const matchingKeypair = availableKeypairs.find(k => k.id === keypairId);
if (matchingKeypair) {
set({ currentKeypair: matchingKeypair });
} else {
// Last resort: create a minimal keypair entry
const newKeypair: KeypairMetadata = {
id: keypairId,
type: 'Unknown',
name: `Keypair ${keypairId.substring(0, 8)}...`,
createdAt: Date.now()
};
set({ currentKeypair: newKeypair });
}
}
return true;
} catch (error) {
console.error('Failed to select keypair:', error);
throw error; // Re-throw to show error in UI
}
},
// Create a new keypair
createKeypair: async (type: string, metadata?: Record<string, any>) => {
try {
const wasmModule = await getWasmModule();
// Format metadata for WASM
const metadataJson = metadata ? JSON.stringify({
name: metadata.name,
description: metadata.description,
created_at: Date.now()
}) : undefined;
// Call the WASM add_keypair function
const keypairId = await wasmModule.add_keypair(type, metadataJson);
// Refresh the keypair list
await get().listKeypairs();
return keypairId;
} catch (error) {
console.error('Failed to create keypair:', error);
throw error;
}
},
// Connect to a WebSocket server
connectWebSocket: async (url: string) => {
try {
const wasmModule = await getWasmModule();
const { currentKeypair } = get();
if (!currentKeypair) {
throw new Error('No keypair selected');
}
// Get the public key from WASM
const publicKeyArray = await wasmModule.current_keypair_public_key();
const publicKeyHex = Array.from(publicKeyArray)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// Connect to WebSocket via background service worker
const chromeApi = getChromeApi();
const response = await chromeApi.runtime.sendMessage({
type: 'CONNECT_WEBSOCKET',
serverUrl: url,
publicKey: publicKeyHex
});
if (response && response.success) {
set({
isWebSocketConnected: true,
webSocketUrl: url
});
return true;
} else {
throw new Error(response?.error || 'Failed to connect to WebSocket server');
}
} catch (error) {
console.error('Failed to connect to WebSocket:', error);
return false;
}
},
// Disconnect from WebSocket server
disconnectWebSocket: async () => {
try {
// Disconnect via background service worker
const chromeApi = getChromeApi();
const response = await chromeApi.runtime.sendMessage({
type: 'DISCONNECT_WEBSOCKET'
});
if (response && response.success) {
set({
isWebSocketConnected: false,
webSocketUrl: null
});
return true;
} else {
throw new Error(response?.error || 'Failed to disconnect from WebSocket server');
}
} catch (error) {
console.error('Failed to disconnect from WebSocket:', error);
return false;
}
},
// Execute a Rhai script
executeScript: async (script: string) => {
try {
const wasmModule = await getWasmModule();
// Call the WASM run_rhai function
const result = await wasmModule.run_rhai(script);
return result;
} catch (error) {
console.error('Failed to execute script:', error);
throw error;
}
},
// Sign a message with the current keypair
signMessage: async (message: string) => {
try {
const wasmModule = await getWasmModule();
// Convert message to Uint8Array
const messageBytes = stringToUint8Array(message);
// Call the WASM sign function
const signature = await wasmModule.sign(messageBytes);
return signature;
} catch (error) {
console.error('Failed to sign message:', error);
throw error;
}
}
}));

View File

@@ -1,45 +0,0 @@
/**
* Common TypeScript types for the Hero Vault Extension
*/
// React types
export type SyntheticEvent<T = Element, E = Event> = React.BaseSyntheticEvent<E, EventTarget & T, EventTarget>;
// Session types
export interface SessionActivity {
timestamp: number;
action: string;
details?: string;
}
// Script types
export interface ScriptResult {
id: string;
script: string;
result: string;
timestamp: number;
success: boolean;
}
export interface PendingScript {
id: string;
name: string;
script: string;
}
// WebSocket types
export interface ConnectionHistory {
id: string;
url: string;
timestamp: number;
status: 'connected' | 'disconnected' | 'error';
message?: string;
}
// Settings types
export interface Settings {
darkMode: boolean;
autoLockTimeout: number;
defaultKeyType: string;
showScriptNotifications: boolean;
}

View File

@@ -1,5 +0,0 @@
/// <reference types="chrome" />
// This file provides type declarations for Chrome extension APIs
// It's needed because we're using the Chrome extension API in a TypeScript project
// The actual implementation is provided by the browser at runtime

View File

@@ -1,14 +0,0 @@
// Type declarations for modules without type definitions
// React and Material UI
declare module 'react';
declare module 'react-dom';
declare module 'react-router-dom';
declare module '@mui/material';
declare module '@mui/material/*';
declare module '@mui/icons-material/*';
// Project modules
declare module './pages/*';
declare module './components/*';
declare module './store/*';

View File

@@ -1,16 +0,0 @@
declare module '*/wasm_app.js' {
export default function init(): Promise<void>;
export function init_session(keyspace: string, password: string): Promise<void>;
export function create_keyspace(keyspace: string, password: string): Promise<void>;
export function lock_session(): void;
export function is_unlocked(): boolean;
export function add_keypair(key_type: string | undefined, metadata: string | undefined): Promise<string>;
export function list_keypairs(): Promise<string>;
export function select_keypair(key_id: string): Promise<void>;
export function current_keypair_metadata(): Promise<any>;
export function current_keypair_public_key(): Promise<Uint8Array>;
export function sign(message: Uint8Array): Promise<string>;
export function verify(signature: string, message: Uint8Array): Promise<boolean>;
export function init_rhai_env(): void;
export function run_rhai(script: string): Promise<string>;
}

View File

@@ -1,103 +0,0 @@
/**
* Chrome API utilities for Hero Vault Extension
*
* This module provides Chrome API detection and mocks for development mode
*/
// Check if we're running in a Chrome extension environment
export const isExtensionEnvironment = (): boolean => {
return typeof chrome !== 'undefined' && !!chrome.runtime && !!chrome.runtime.id;
};
// Mock storage for development mode
const mockStorage: Record<string, any> = {
// Initialize with some default values for script storage
pendingScripts: [],
scriptResults: []
};
// Mock Chrome API for development mode
export const getChromeApi = () => {
// If we're in a Chrome extension environment, return the real Chrome API
if (isExtensionEnvironment()) {
return chrome;
}
// Otherwise, return a mock implementation
return {
runtime: {
sendMessage: (message: any): Promise<any> => {
console.log('Mock sendMessage called with:', message);
// Mock responses based on message type
if (message.type === 'SESSION_STATUS') {
return Promise.resolve({ active: false });
}
if (message.type === 'CREATE_KEYSPACE') {
mockStorage['currentKeyspace'] = message.keyspace;
return Promise.resolve({ success: true });
}
if (message.type === 'UNLOCK_SESSION') {
mockStorage['currentKeyspace'] = message.keyspace;
return Promise.resolve({ success: true });
}
if (message.type === 'LOCK_SESSION') {
delete mockStorage['currentKeyspace'];
return Promise.resolve({ success: true });
}
return Promise.resolve({ success: false });
},
getURL: (path: string): string => {
return path;
}
},
storage: {
local: {
get: (keys: string | string[] | object): Promise<Record<string, any>> => {
console.log('Mock storage.local.get called with:', keys);
if (typeof keys === 'string') {
// Handle specific script storage keys
if (keys === 'pendingScripts' && !mockStorage[keys]) {
mockStorage[keys] = [];
}
if (keys === 'scriptResults' && !mockStorage[keys]) {
mockStorage[keys] = [];
}
return Promise.resolve({ [keys]: mockStorage[keys] });
}
if (Array.isArray(keys)) {
const result: Record<string, any> = {};
keys.forEach(key => {
// Handle specific script storage keys
if (key === 'pendingScripts' && !mockStorage[key]) {
mockStorage[key] = [];
}
if (key === 'scriptResults' && !mockStorage[key]) {
mockStorage[key] = [];
}
result[key] = mockStorage[key];
});
return Promise.resolve(result);
}
return Promise.resolve(mockStorage);
},
set: (items: Record<string, any>): Promise<void> => {
console.log('Mock storage.local.set called with:', items);
Object.keys(items).forEach(key => {
mockStorage[key] = items[key];
});
return Promise.resolve();
}
}
}
} as typeof chrome;
};

View File

@@ -1,139 +0,0 @@
/**
* WASM Helper for Hero Vault Extension
*
* This module handles loading and initializing the WASM module,
* and provides a typed interface to the WASM functions.
*/
// Import types for TypeScript
interface WasmModule {
// Session management
init_session: (keyspace: string, password: string) => Promise<void>;
create_keyspace: (keyspace: string, password: string) => Promise<void>;
lock_session: () => void;
is_unlocked: () => boolean;
// Keypair management
add_keypair: (key_type: string | undefined, metadata: string | undefined) => Promise<string>;
list_keypairs: () => Promise<string>;
select_keypair: (key_id: string) => Promise<void>;
current_keypair_metadata: () => Promise<any>;
current_keypair_public_key: () => Promise<Uint8Array>;
// Cryptographic operations
sign: (message: Uint8Array) => Promise<string>;
verify: (message: Uint8Array, signature: string) => Promise<boolean>;
encrypt_data: (data: Uint8Array) => Promise<Uint8Array>;
decrypt_data: (encrypted: Uint8Array) => Promise<Uint8Array>;
// Rhai scripting
init_rhai_env: () => void;
run_rhai: (script: string) => Promise<string>;
}
// Global reference to the WASM module
let wasmModule: WasmModule | null = null;
let isInitializing = false;
let initPromise: Promise<void> | null = null;
/**
* Initialize the WASM module
* This should be called before any other WASM functions
*/
export const initWasm = async (): Promise<void> => {
if (wasmModule) {
return Promise.resolve(); // Already initialized
}
if (isInitializing && initPromise) {
return initPromise; // Already initializing
}
isInitializing = true;
initPromise = new Promise<void>(async (resolve, reject) => {
try {
try {
// Import the WASM module
// Use a relative path that will be resolved by Vite during build
const wasmImport = await import('../../public/wasm/wasm_app.js');
// Initialize the WASM module
await wasmImport.default();
// Store the WASM module globally
wasmModule = wasmImport as unknown as WasmModule;
console.log('WASM module initialized successfully');
resolve();
} catch (error) {
console.error('Failed to initialize WASM module:', error);
reject(error);
}
} finally {
isInitializing = false;
}
});
return initPromise;
};
/**
* Get the WASM module
* This will initialize the module if it hasn't been initialized yet
*/
export const getWasmModule = async (): Promise<WasmModule> => {
if (!wasmModule) {
await initWasm();
}
if (!wasmModule) {
throw new Error('WASM module failed to initialize');
}
return wasmModule;
};
/**
* Check if the WASM module is initialized
*/
export const isWasmInitialized = (): boolean => {
return wasmModule !== null;
};
/**
* Helper to convert string to Uint8Array
*/
export const stringToUint8Array = (str: string): Uint8Array => {
const encoder = new TextEncoder();
return encoder.encode(str);
};
/**
* Helper to convert Uint8Array to string
*/
export const uint8ArrayToString = (array: Uint8Array): string => {
const decoder = new TextDecoder();
return decoder.decode(array);
};
/**
* Helper to convert hex string to Uint8Array
*/
export const hexToUint8Array = (hex: string): Uint8Array => {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
};
/**
* Helper to convert Uint8Array to hex string
*/
export const uint8ArrayToHex = (array: Uint8Array): string => {
return Array.from(array)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
};

View File

@@ -1,30 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": false,
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"typeRoots": ["./node_modules/@types", "./src/types"],
"jsxImportSource": "react"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,33 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { crx } from '@crxjs/vite-plugin';
import { resolve } from 'path';
import { readFileSync } from 'fs';
import fs from 'fs';
const manifest = JSON.parse(
readFileSync('public/manifest.json', 'utf-8')
);
export default defineConfig({
plugins: [
react(),
crx({ manifest }),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
outDir: 'dist',
emptyOutDir: true,
rollupOptions: {
input: {
index: resolve(__dirname, 'index.html'),
},
},
},
// Copy WASM files to the dist directory
publicDir: 'public',
});