Compare commits
2 Commits
c641d0ae2e
...
main_brows
Author | SHA1 | Date | |
---|---|---|---|
|
1d3d0a4fa4 | ||
|
4f3f98a954 |
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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 [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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) {
|
||||||
|
1
hero_vault_extension/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
dist
|
|
@@ -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)
|
|
@@ -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)}
|
|
@@ -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()});
|
|
61
hero_vault_extension/dist/background.js
vendored
@@ -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();
|
|
||||||
});
|
|
||||||
|
|
BIN
hero_vault_extension/dist/icons/icon-128.png
vendored
Before Width: | Height: | Size: 1.9 KiB |
BIN
hero_vault_extension/dist/icons/icon-16.png
vendored
Before Width: | Height: | Size: 454 B |
BIN
hero_vault_extension/dist/icons/icon-32.png
vendored
Before Width: | Height: | Size: 712 B |
BIN
hero_vault_extension/dist/icons/icon-48.png
vendored
Before Width: | Height: | Size: 1.1 KiB |
14
hero_vault_extension/dist/index.html
vendored
@@ -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>
|
|
26
hero_vault_extension/dist/manifest.json
vendored
@@ -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'"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
import './assets/simple-background.ts-e63275e1.js';
|
|
@@ -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>
|
|
4862
hero_vault_extension/package-lock.json
generated
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 454 B |
Before Width: | Height: | Size: 712 B |
Before Width: | Height: | Size: 1.1 KiB |
@@ -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'"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
|
@@ -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);
|
|
||||||
}
|
|
@@ -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;
|
|
@@ -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');
|
|
@@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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);
|
|
||||||
}
|
|
@@ -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>
|
|
||||||
);
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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 })
|
|
||||||
}));
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
@@ -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;
|
|
||||||
}
|
|
5
hero_vault_extension/src/types/chrome.d.ts
vendored
@@ -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
|
|
14
hero_vault_extension/src/types/declarations.d.ts
vendored
@@ -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/*';
|
|
16
hero_vault_extension/src/types/wasm.d.ts
vendored
@@ -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>;
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
};
|
|
@@ -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('');
|
|
||||||
};
|
|
@@ -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" }]
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
@@ -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',
|
|
||||||
});
|
|