diff --git a/crypto_vault_extension/background.js b/crypto_vault_extension/background.js index 191cd1a..6796f38 100644 --- a/crypto_vault_extension/background.js +++ b/crypto_vault_extension/background.js @@ -6,6 +6,9 @@ let sessionTimeoutDuration = 15; // Default 15 seconds let sessionTimeoutId = null; // Background timer let popupPort = null; // Track popup connection +// SigSocket service instance +let sigSocketService = null; + // Utility function to convert Uint8Array to hex function toHex(uint8Array) { return Array.from(uint8Array) @@ -135,8 +138,9 @@ async function restoreSession() { return null; } -// Import WASM module functions +// Import WASM module functions and SigSocket service import init, * as wasmFunctions from './wasm/wasm_app.js'; +import SigSocketService from './background/sigsocket.js'; // Initialize WASM module async function initVault() { @@ -151,6 +155,13 @@ async function initVault() { vault = wasmFunctions; isInitialized = true; + // Initialize SigSocket service + if (!sigSocketService) { + sigSocketService = new SigSocketService(); + await sigSocketService.initialize(vault); + console.log('🔌 SigSocket service initialized'); + } + // Try to restore previous session await restoreSession(); @@ -172,6 +183,20 @@ const messageHandlers = { initSession: async (request) => { await vault.init_session(request.keyspace, request.password); await sessionManager.save(request.keyspace); + + // Smart auto-connect to SigSocket when session is initialized + if (sigSocketService) { + try { + // This will reuse existing connection if same workspace, or switch if different + const connected = await sigSocketService.connectToServer(request.keyspace); + if (connected) { + console.log(`🔗 SigSocket ready for workspace: ${request.keyspace}`); + } + } catch (error) { + console.warn('Failed to auto-connect to SigSocket:', error); + } + } + return { success: true }; }, @@ -261,6 +286,62 @@ const messageHandlers = { await chrome.storage.local.set({ sessionTimeout: request.timeout }); resetSessionTimeout(); // Restart with new duration return { success: true }; + }, + + // SigSocket handlers + connectSigSocket: async (request) => { + if (!sigSocketService) { + return { success: false, error: 'SigSocket service not initialized' }; + } + const connected = await sigSocketService.connectToServer(request.workspace); + return { success: connected }; + }, + + disconnectSigSocket: async () => { + if (!sigSocketService) { + return { success: false, error: 'SigSocket service not initialized' }; + } + await sigSocketService.disconnect(); + return { success: true }; + }, + + getSigSocketStatus: async () => { + if (!sigSocketService) { + return { success: false, error: 'SigSocket service not initialized' }; + } + const status = await sigSocketService.getStatus(); + return { success: true, status }; + }, + + getPendingSignRequests: async () => { + if (!sigSocketService) { + return { success: false, error: 'SigSocket service not initialized' }; + } + + try { + // Use WASM filtered requests which handles workspace filtering + const requests = await sigSocketService.getFilteredRequests(); + return { success: true, requests }; + } catch (error) { + console.error('Failed to get pending requests:', error); + return { success: false, error: error.message }; + } + }, + + approveSignRequest: async (request) => { + if (!sigSocketService) { + return { success: false, error: 'SigSocket service not initialized' }; + } + const approved = await sigSocketService.approveSignRequest(request.requestId); + return { success: approved }; + }, + + rejectSignRequest: async (request) => { + if (!sigSocketService) { + return { success: false, error: 'SigSocket service not initialized' }; + } + const rejected = await sigSocketService.rejectSignRequest(request.requestId, request.reason); + return { success: rejected }; } }; @@ -302,6 +383,11 @@ chrome.runtime.onConnect.addListener((port) => { // Track popup connection popupPort = port; + // Connect SigSocket service to popup + if (sigSocketService) { + sigSocketService.setPopupPort(port); + } + // If we have an active session, ensure keep-alive is running if (currentSession) { startKeepAlive(); @@ -311,6 +397,11 @@ chrome.runtime.onConnect.addListener((port) => { // Popup closed, clear reference and stop keep-alive popupPort = null; stopKeepAlive(); + + // Disconnect SigSocket service from popup + if (sigSocketService) { + sigSocketService.setPopupPort(null); + } }); } }); \ No newline at end of file diff --git a/crypto_vault_extension/background/sigsocket.js b/crypto_vault_extension/background/sigsocket.js index 3f8d687..b2ec336 100644 --- a/crypto_vault_extension/background/sigsocket.js +++ b/crypto_vault_extension/background/sigsocket.js @@ -1,31 +1,35 @@ /** - * SigSocket Service for Browser Extension - * - * Handles SigSocket client functionality including: - * - Auto-connecting to SigSocket server when workspace is created - * - Managing pending sign requests - * - Handling user approval/rejection flow - * - Validating keyspace matches before showing approval UI + * SigSocket Service - Clean Implementation with New WASM APIs + * + * This service provides a clean interface for SigSocket functionality using + * the new WASM-based APIs that handle all WebSocket management, request storage, + * and security validation internally. + * + * Architecture: + * - WASM handles: WebSocket connection, message parsing, request storage, security + * - Extension handles: UI notifications, badge updates, user interactions */ class SigSocketService { constructor() { - this.connection = null; - this.pendingRequests = new Map(); // requestId -> SignRequestData - this.connectedPublicKey = null; + // Connection state this.isConnected = false; + this.currentWorkspace = null; + this.connectedPublicKey = null; + + // Configuration this.defaultServerUrl = "ws://localhost:8080/ws"; - // Initialize WASM module reference + // WASM module reference this.wasmModule = null; - // Reference to popup port for communication + // UI communication this.popupPort = null; } /** * Initialize the service with WASM module - * @param {Object} wasmModule - The loaded WASM module + * @param {Object} wasmModule - The loaded WASM module with SigSocketManager */ async initialize(wasmModule) { this.wasmModule = wasmModule; @@ -40,427 +44,333 @@ class SigSocketService { console.warn('Failed to load SigSocket URL from storage:', error); } - // Set up global callbacks for WASM - globalThis.onSignRequestReceived = this.handleIncomingRequest.bind(this); - globalThis.onConnectionStateChanged = this.handleConnectionStateChange.bind(this); + console.log('🔌 SigSocket service initialized with WASM APIs'); } /** - * Connect to SigSocket server for a workspace + * Connect to SigSocket server using WASM APIs + * WASM handles all connection logic (reuse, switching, etc.) * @param {string} workspaceId - The workspace/keyspace identifier * @returns {Promise} - True if connected successfully */ async connectToServer(workspaceId) { try { - if (!this.wasmModule) { - throw new Error('WASM module not initialized'); + if (!this.wasmModule?.SigSocketManager) { + throw new Error('WASM SigSocketManager not available'); } - // Check if already connected to this workspace - if (this.isConnected && this.connection) { - console.log(`Already connected to SigSocket server for workspace: ${workspaceId}`); - return true; - } + console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId}`); - // Disconnect any existing connection first - if (this.connection) { - this.disconnect(); - } + // 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) + ); - // Get the workspace default public key - const publicKeyHex = await this.wasmModule.get_workspace_default_public_key(workspaceId); - if (!publicKeyHex) { - throw new Error('No public key found for workspace'); - } + // Parse connection info + const info = JSON.parse(connectionInfo); + this.currentWorkspace = info.workspace; + this.connectedPublicKey = info.public_key; + this.isConnected = info.is_connected; - console.log(`Connecting to SigSocket server for workspace: ${workspaceId} with key: ${publicKeyHex.substring(0, 16)}...`); + console.log(`✅ SigSocket connection result:`, { + workspace: this.currentWorkspace, + publicKey: this.connectedPublicKey?.substring(0, 16) + '...', + connected: this.isConnected + }); - // Create new SigSocket connection - console.log('Creating new SigSocketConnection instance'); - this.connection = new this.wasmModule.SigSocketConnection(); - console.log('SigSocketConnection instance created'); + // Update badge to show current state + this.updateBadge(); - // Connect to server - await this.connection.connect(this.defaultServerUrl, publicKeyHex); - - this.connectedPublicKey = publicKeyHex; - - // Clear pending requests if switching to a different workspace - if (this.currentWorkspace && this.currentWorkspace !== workspaceId) { - console.log(`Switching workspace from ${this.currentWorkspace} to ${workspaceId}, clearing pending requests`); - this.pendingRequests.clear(); - this.updateBadge(); - } - - this.currentWorkspace = workspaceId; - this.isConnected = true; - - console.log(`Successfully connected to SigSocket server for workspace: ${workspaceId}`); - return true; + return this.isConnected; } catch (error) { - console.error('Failed to connect to SigSocket server:', error); + console.error('❌ SigSocket connection failed:', error); this.isConnected = false; - this.connectedPublicKey = null; this.currentWorkspace = null; - if (this.connection) { - this.connection.disconnect(); - this.connection = null; - } + this.connectedPublicKey = null; return false; } } /** - * Handle incoming sign request from server - * @param {string} requestId - Unique request identifier - * @param {string} messageBase64 - Message to be signed (base64-encoded) + * Handle events from the WASM SigSocket client + * This is called automatically when requests arrive + * @param {Object} event - Event from WASM layer */ - handleIncomingRequest(requestId, messageBase64) { - console.log(`Received sign request: ${requestId}`); + handleSigSocketEvent(event) { + console.log('📨 Received SigSocket event:', event); - // Security check: Only accept requests if we have an active connection - if (!this.isConnected || !this.connectedPublicKey || !this.currentWorkspace) { - console.warn(`Rejecting sign request ${requestId}: No active workspace connection`); - return; - } + if (event.type === 'sign_request') { + console.log(`🔐 New sign request: ${event.request_id}`); - // Store the request with workspace info - const requestData = { - id: requestId, - message: messageBase64, - timestamp: Date.now(), - status: 'pending', - workspace: this.currentWorkspace, - connectedPublicKey: this.connectedPublicKey - }; - - this.pendingRequests.set(requestId, requestData); - - console.log(`Stored sign request for workspace: ${this.currentWorkspace}`); - - // Show notification to user - this.showSignRequestNotification(); - - // Update extension badge - this.updateBadge(); - - // Notify popup about new request if it's open and keyspace is unlocked - this.notifyPopupOfNewRequest(); - } - - /** - * Handle connection state changes - * @param {boolean} connected - True if connected, false if disconnected - */ - handleConnectionStateChange(connected) { - this.isConnected = connected; - console.log(`SigSocket connection state changed: ${connected ? 'connected' : 'disconnected'}`); - - if (!connected) { - this.connectedPublicKey = null; - this.currentWorkspace = null; - // Optionally attempt reconnection here + // The request is automatically stored by WASM + // We just handle UI updates + this.showSignRequestNotification(); + this.updateBadge(); + this.notifyPopupOfNewRequest(); } } /** - * Called when keyspace is unlocked - validate and show/hide approval UI - */ - async onKeypaceUnlocked() { - try { - if (!this.wasmModule) { - return; - } - - // Only check keyspace match if we have a connection - if (!this.isConnected || !this.connectedPublicKey) { - console.log('No SigSocket connection to validate against'); - return; - } - - // Get the currently unlocked workspace name - const unlockedWorkspaceName = this.wasmModule.get_current_keyspace_name(); - - // Get workspace default public key for the UNLOCKED workspace (not connected workspace) - const unlockedWorkspacePublicKey = await this.wasmModule.get_workspace_default_public_key(unlockedWorkspaceName); - - // Check if the unlocked workspace matches the connected workspace - const workspaceMatches = unlockedWorkspaceName === this.currentWorkspace; - const publicKeyMatches = unlockedWorkspacePublicKey === this.connectedPublicKey; - const keypaceMatches = workspaceMatches && publicKeyMatches; - - console.log(`Keyspace unlock validation:`); - console.log(` Connected workspace: ${this.currentWorkspace}`); - console.log(` Unlocked workspace: ${unlockedWorkspaceName}`); - console.log(` Connected public key: ${this.connectedPublicKey}`); - console.log(` Unlocked public key: ${unlockedWorkspacePublicKey}`); - console.log(` Workspace matches: ${workspaceMatches}`); - console.log(` Public key matches: ${publicKeyMatches}`); - console.log(` Overall match: ${keypaceMatches}`); - - // Always get current pending requests (filtered by connected workspace) - const currentPendingRequests = this.getPendingRequests(); - - // Notify popup about keyspace state - console.log(`Sending KEYSPACE_UNLOCKED message to popup: keypaceMatches=${keypaceMatches}, pendingRequests=${currentPendingRequests.length}`); - if (this.popupPort) { - this.popupPort.postMessage({ - type: 'KEYSPACE_UNLOCKED', - keypaceMatches, - pendingRequests: currentPendingRequests - }); - console.log('KEYSPACE_UNLOCKED message sent to popup'); - } else { - console.log('No popup port available to send KEYSPACE_UNLOCKED message'); - } - - } catch (error) { - if (error.message && error.message.includes('Workspace not found')) { - console.log(`Keyspace unlock: Different workspace unlocked (connected to: ${this.currentWorkspace})`); - - // Send message with no match and empty requests - if (this.popupPort) { - this.popupPort.postMessage({ - type: 'KEYSPACE_UNLOCKED', - keypaceMatches: false, - pendingRequests: [] - }); - } - } else { - console.error('Error handling keyspace unlock:', error); - } - } - } - - /** - * Approve a sign request + * Approve a sign request using WASM APIs * @param {string} requestId - Request to approve * @returns {Promise} - True if approved successfully */ async approveSignRequest(requestId) { try { - const request = this.pendingRequests.get(requestId); - if (!request) { - throw new Error('Request not found'); + if (!this.wasmModule?.SigSocketManager) { + throw new Error('WASM SigSocketManager not available'); } - // Validate request is for current workspace - if (request.workspace !== this.currentWorkspace) { - throw new Error(`Request is for workspace '${request.workspace}', but current workspace is '${this.currentWorkspace}'`); - } + console.log(`✅ Approving request: ${requestId}`); - if (request.connectedPublicKey !== this.connectedPublicKey) { - throw new Error('Request public key does not match current connection'); - } + // WASM handles all validation, signing, and server communication + await this.wasmModule.SigSocketManager.approve_request(requestId); - // Validate keyspace is still unlocked and matches - if (!this.wasmModule.is_unlocked()) { - throw new Error('Keyspace is locked'); - } + console.log(`🎉 Request approved successfully: ${requestId}`); - const currentPublicKey = await this.wasmModule.get_workspace_default_public_key(this.currentWorkspace); - if (currentPublicKey !== this.connectedPublicKey) { - throw new Error('Keyspace mismatch'); - } - - // Decode message from base64 - const messageBytes = atob(request.message).split('').map(c => c.charCodeAt(0)); - - // Sign the message with default keypair (doesn't require selected keypair) - const signatureHex = await this.wasmModule.sign_with_default_keypair(new Uint8Array(messageBytes)); - - // Send response to server - await this.connection.send_response(requestId, request.message, signatureHex); - - // Update request status - request.status = 'approved'; - request.signature = signatureHex; - - // Remove from pending requests - this.pendingRequests.delete(requestId); - - // Update badge + // Update UI this.updateBadge(); - - console.log(`Approved sign request: ${requestId}`); + this.notifyPopupOfRequestUpdate(); + return true; - + } catch (error) { - console.error('Failed to approve sign request:', error); + console.error(`❌ Failed to approve request ${requestId}:`, error); return false; } } /** - * Reject a sign request + * Reject a sign request using WASM APIs * @param {string} requestId - Request to reject - * @param {string} reason - Reason for rejection (optional) + * @param {string} reason - Reason for rejection * @returns {Promise} - True if rejected successfully */ async rejectSignRequest(requestId, reason = 'User rejected') { try { - const request = this.pendingRequests.get(requestId); - if (!request) { - throw new Error('Request not found'); + if (!this.wasmModule?.SigSocketManager) { + throw new Error('WASM SigSocketManager not available'); } - // Send rejection to server - await this.connection.send_rejection(requestId, reason); - - // Update request status - request.status = 'rejected'; - request.reason = reason; - - // Remove from pending requests - this.pendingRequests.delete(requestId); - - // Update badge + console.log(`❌ Rejecting request: ${requestId}, reason: ${reason}`); + + // WASM handles rejection and server communication + await this.wasmModule.SigSocketManager.reject_request(requestId, reason); + + console.log(`✅ Request rejected successfully: ${requestId}`); + + // Update UI this.updateBadge(); - - console.log(`Rejected sign request: ${requestId}, reason: ${reason}`); + this.notifyPopupOfRequestUpdate(); + return true; - + } catch (error) { - console.error('Failed to reject sign request:', error); + console.error(`❌ Failed to reject request ${requestId}:`, error); return false; } } /** - * Get all pending requests for the current workspace - * @returns {Array} - Array of pending request data for current workspace + * Get pending requests from WASM (filtered by current workspace) + * @returns {Promise} - Array of pending requests for current workspace */ - getPendingRequests() { - const allRequests = Array.from(this.pendingRequests.values()); + async getPendingRequests() { + return this.getFilteredRequests(); + } - // Filter requests to only include those for the current workspace - const filteredRequests = allRequests.filter(request => { - const isCurrentWorkspace = request.workspace === this.currentWorkspace; - const isCurrentPublicKey = request.connectedPublicKey === this.connectedPublicKey; - - if (!isCurrentWorkspace || !isCurrentPublicKey) { - console.log(`Filtering out request ${request.id}: workspace=${request.workspace} (current=${this.currentWorkspace}), publicKey match=${isCurrentPublicKey}`); + /** + * Get filtered requests from WASM (workspace-aware) + * @returns {Promise} - Array of filtered requests + */ + async getFilteredRequests() { + try { + if (!this.wasmModule?.SigSocketManager) { + return []; } - return isCurrentWorkspace && isCurrentPublicKey; - }); + const requestsJson = await this.wasmModule.SigSocketManager.get_filtered_requests(); + const requests = JSON.parse(requestsJson); - console.log(`getPendingRequests: ${allRequests.length} total, ${filteredRequests.length} for current workspace`); - return filteredRequests; + console.log(`📋 Retrieved ${requests.length} filtered requests for current workspace`); + return requests; + + } catch (error) { + console.error('Failed to get filtered requests:', error); + return []; + } + } + + /** + * Check if a request can be approved (keyspace validation) + * @param {string} requestId - Request ID to check + * @returns {Promise} - True if can be approved + */ + async canApproveRequest(requestId) { + try { + if (!this.wasmModule?.SigSocketManager) { + return false; + } + + return await this.wasmModule.SigSocketManager.can_approve_request(requestId); + + } catch (error) { + console.error('Failed to check request approval status:', error); + return false; + } } /** * Show notification for new sign request */ showSignRequestNotification() { - // Create notification - chrome.notifications.create({ - type: 'basic', - iconUrl: 'icons/icon48.png', - title: 'Sign Request', - message: 'New signature request received. Click to review.' - }); + try { + if (chrome.notifications && chrome.notifications.create) { + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icons/icon48.png', + title: 'SigSocket Sign Request', + message: 'New signature request received. Click to review.' + }); + console.log('📢 Notification shown for sign request'); + } else { + console.log('📢 Notifications not available, skipping notification'); + } + } catch (error) { + console.warn('Failed to show notification:', error); + } } /** - * Notify popup about new request if popup is open and keyspace is unlocked + * Update extension badge with pending request count + */ + async updateBadge() { + try { + const requests = await this.getPendingRequests(); + const count = requests.length; + const badgeText = count > 0 ? count.toString() : ''; + + console.log(`🔢 Updating badge: ${count} pending requests`); + + chrome.action.setBadgeText({ text: badgeText }); + chrome.action.setBadgeBackgroundColor({ color: '#ff6b6b' }); + + } catch (error) { + console.error('Failed to update badge:', error); + } + } + + /** + * Notify popup about new request */ async notifyPopupOfNewRequest() { - // Only notify if popup is connected if (!this.popupPort) { - console.log('No popup port available, skipping new request notification'); - return; - } - - // Check if we have WASM module and can validate keyspace - if (!this.wasmModule) { - console.log('WASM module not available, skipping new request notification'); + console.log('No popup connected, skipping notification'); return; } try { - // Check if keyspace is unlocked - if (!this.wasmModule.is_unlocked()) { - console.log('Keyspace is locked, skipping new request notification'); - return; - } + const requests = await this.getPendingRequests(); + const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false; - // Get the currently unlocked workspace name - const unlockedWorkspaceName = this.wasmModule.get_current_keyspace_name(); - - // Get workspace default public key for the UNLOCKED workspace - const unlockedWorkspacePublicKey = await this.wasmModule.get_workspace_default_public_key(unlockedWorkspaceName); - - // Check if the unlocked workspace matches the connected workspace - const workspaceMatches = unlockedWorkspaceName === this.currentWorkspace; - const publicKeyMatches = unlockedWorkspacePublicKey === this.connectedPublicKey; - const keypaceMatches = workspaceMatches && publicKeyMatches; - - console.log(`New request notification check: keypaceMatches=${keypaceMatches}, workspace=${unlockedWorkspaceName}, connected=${this.currentWorkspace}`); - - // Get current pending requests (filtered by connected workspace) - const currentPendingRequests = this.getPendingRequests(); - - // SECURITY: Only send requests if workspace matches, otherwise send empty array - const requestsToSend = keypaceMatches ? currentPendingRequests : []; - - // Send update to popup this.popupPort.postMessage({ type: 'NEW_SIGN_REQUEST', - keypaceMatches, - pendingRequests: requestsToSend + canApprove, + pendingRequests: requests }); - console.log(`Sent NEW_SIGN_REQUEST message to popup: keypaceMatches=${keypaceMatches}, ${requestsToSend.length} requests (${currentPendingRequests.length} total for connected workspace)`); + console.log(`📤 Notified popup: ${requests.length} requests, canApprove: ${canApprove}`); } catch (error) { - console.log('Error in notifyPopupOfNewRequest:', error); + console.error('Failed to notify popup:', error); } } /** - * Update extension badge with pending request count for current workspace + * Notify popup about request updates */ - updateBadge() { - // Only count requests for the current workspace - const currentWorkspaceRequests = this.getPendingRequests(); - const count = currentWorkspaceRequests.length; - const badgeText = count > 0 ? count.toString() : ''; + async notifyPopupOfRequestUpdate() { + if (!this.popupPort) return; - console.log(`Updating badge: ${this.pendingRequests.size} total requests, ${count} for current workspace, badge text: "${badgeText}"`); + try { + const requests = await this.getPendingRequests(); - chrome.action.setBadgeText({ text: badgeText }); - chrome.action.setBadgeBackgroundColor({ color: '#ff6b6b' }); + this.popupPort.postMessage({ + type: 'REQUESTS_UPDATED', + pendingRequests: requests + }); + + } catch (error) { + console.error('Failed to notify popup of update:', error); + } } /** * Disconnect from SigSocket server + * WASM handles all disconnection logic */ - disconnect() { - if (this.connection) { - this.connection.disconnect(); - this.connection = null; + async disconnect() { + try { + if (this.wasmModule?.SigSocketManager) { + await this.wasmModule.SigSocketManager.disconnect(); + } + + // Clear local state + this.isConnected = false; + this.currentWorkspace = null; + this.connectedPublicKey = null; + + this.updateBadge(); + + console.log('🔌 SigSocket disconnection requested'); + + } catch (error) { + console.error('Failed to disconnect:', error); } - - this.isConnected = false; - this.connectedPublicKey = null; - this.currentWorkspace = null; - this.pendingRequests.clear(); - this.updateBadge(); } /** - * Get connection status - * @returns {Object} - Connection status information + * Get connection status from WASM + * @returns {Promise} - Connection status information */ - getStatus() { - return { - isConnected: this.isConnected, - connectedPublicKey: this.connectedPublicKey, - pendingRequestCount: this.getPendingRequests().length, - serverUrl: this.defaultServerUrl - }; + async getStatus() { + try { + if (!this.wasmModule?.SigSocketManager) { + return { + isConnected: false, + workspace: null, + publicKey: null, + pendingRequestCount: 0, + serverUrl: this.defaultServerUrl + }; + } + + // Let WASM provide the authoritative status + const statusJson = await this.wasmModule.SigSocketManager.get_connection_status(); + const status = JSON.parse(statusJson); + const requests = await this.getPendingRequests(); + + return { + isConnected: status.is_connected, + workspace: status.workspace, + publicKey: status.public_key, + pendingRequestCount: requests.length, + serverUrl: this.defaultServerUrl + }; + + } catch (error) { + console.error('Failed to get status:', error); + return { + isConnected: false, + workspace: null, + publicKey: null, + pendingRequestCount: 0, + serverUrl: this.defaultServerUrl + }; + } } /** @@ -469,8 +379,32 @@ class SigSocketService { */ setPopupPort(port) { this.popupPort = port; + console.log('📱 Popup connected to SigSocket service'); + } + + /** + * Called when keyspace is unlocked - notify popup of current state + */ + async onKeypaceUnlocked() { + if (!this.popupPort) return; + + try { + const requests = await this.getPendingRequests(); + const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false; + + this.popupPort.postMessage({ + type: 'KEYSPACE_UNLOCKED', + canApprove, + pendingRequests: requests + }); + + console.log(`🔓 Keyspace unlocked notification sent: ${requests.length} requests, canApprove: ${canApprove}`); + + } catch (error) { + console.error('Failed to handle keyspace unlock:', error); + } } } // Export for use in background script -export default SigSocketService; +export default SigSocketService; \ No newline at end of file diff --git a/crypto_vault_extension/manifest.json b/crypto_vault_extension/manifest.json index 814bf08..cda239d 100644 --- a/crypto_vault_extension/manifest.json +++ b/crypto_vault_extension/manifest.json @@ -6,7 +6,8 @@ "permissions": [ "storage", - "activeTab" + "activeTab", + "notifications" ], "icons": { diff --git a/crypto_vault_extension/popup.html b/crypto_vault_extension/popup.html index 551e4ee..71c4784 100644 --- a/crypto_vault_extension/popup.html +++ b/crypto_vault_extension/popup.html @@ -73,6 +73,50 @@ + +
+
+

🔌 SigSocket Requests

+
+ + Disconnected +
+
+ +
+
+
+
📝
+

No pending sign requests

+ Requests will appear here when received from SigSocket server +
+
+ + +
+ +
+ + +
+
+

Your Keypairs

`; } -}); \ No newline at end of file +}); + +// SigSocket functionality +let sigSocketRequests = []; +let sigSocketStatus = { isConnected: false, workspace: null }; + +// Initialize SigSocket UI elements +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 +if (backgroundPort) { + backgroundPort.onMessage.addListener((message) => { + if (message.type === 'NEW_SIGN_REQUEST') { + handleNewSignRequest(message); + } else if (message.type === 'REQUESTS_UPDATED') { + updateRequestsList(message.pendingRequests); + } else if (message.type === 'KEYSPACE_UNLOCKED') { + handleKeypaceUnlocked(message); + } + }); +} + +// Load SigSocket state when popup opens +async function loadSigSocketState() { + try { + // Get SigSocket status + const statusResponse = await sendMessage('getSigSocketStatus'); + if (statusResponse?.success) { + updateConnectionStatus(statusResponse.status); + } + + // Get pending requests + const requestsResponse = await sendMessage('getPendingSignRequests'); + if (requestsResponse?.success) { + updateRequestsList(requestsResponse.requests); + } + } catch (error) { + console.warn('Failed to load SigSocket state:', error); + } +} + +// Update connection status display +function updateConnectionStatus(status) { + sigSocketStatus = status; + + if (sigSocketElements.connectionDot && sigSocketElements.connectionText) { + if (status.isConnected) { + sigSocketElements.connectionDot.classList.add('connected'); + sigSocketElements.connectionText.textContent = `Connected (${status.workspace || 'Unknown'})`; + } else { + sigSocketElements.connectionDot.classList.remove('connected'); + sigSocketElements.connectionText.textContent = 'Disconnected'; + } + } +} + +// Update requests list display +function updateRequestsList(requests) { + sigSocketRequests = requests || []; + + if (!sigSocketElements.requestsContainer) return; + + if (sigSocketRequests.length === 0) { + sigSocketElements.noRequestsMessage?.classList.remove('hidden'); + sigSocketElements.requestsList?.classList.add('hidden'); + } else { + sigSocketElements.noRequestsMessage?.classList.add('hidden'); + sigSocketElements.requestsList?.classList.remove('hidden'); + + if (sigSocketElements.requestsList) { + sigSocketElements.requestsList.innerHTML = sigSocketRequests.map(request => + createRequestItem(request) + ).join(''); + + // Add event listeners to approve/reject buttons + addRequestEventListeners(); + } + } +} + +// Create HTML for a single request item +function createRequestItem(request) { + const requestTime = new Date(request.timestamp || Date.now()).toLocaleTimeString(); + const shortId = request.id.substring(0, 8) + '...'; + const decodedMessage = request.message ? atob(request.message) : 'No message'; + + return ` +
+
+
${shortId}
+
${requestTime}
+
+ +
+ ${decodedMessage.length > 100 ? decodedMessage.substring(0, 100) + '...' : decodedMessage} +
+ +
+ + +
+
+ `; +} + +// Add event listeners to request action buttons +function addRequestEventListeners() { + // Approve buttons + document.querySelectorAll('.btn-approve').forEach(btn => { + btn.addEventListener('click', async (e) => { + const requestId = e.target.getAttribute('data-request-id'); + await approveSignRequest(requestId); + }); + }); + + // Reject buttons + document.querySelectorAll('.btn-reject').forEach(btn => { + btn.addEventListener('click', async (e) => { + const requestId = e.target.getAttribute('data-request-id'); + await rejectSignRequest(requestId); + }); + }); +} + +// Handle new sign request notification +function handleNewSignRequest(message) { + // Update requests list + if (message.pendingRequests) { + updateRequestsList(message.pendingRequests); + } + + // Show notification if workspace doesn't match + if (!message.canApprove) { + showWorkspaceMismatchWarning(); + } +} + +// Handle keyspace unlocked event +function handleKeypaceUnlocked(message) { + // Update requests list + if (message.pendingRequests) { + updateRequestsList(message.pendingRequests); + } + + // Update button states based on whether requests can be approved + updateRequestButtonStates(message.canApprove); +} + +// Show workspace mismatch warning +function showWorkspaceMismatchWarning() { + const existingWarning = document.querySelector('.workspace-mismatch'); + if (existingWarning) return; // Don't show multiple warnings + + const warning = document.createElement('div'); + warning.className = 'workspace-mismatch'; + warning.innerHTML = ` + ⚠️ Sign requests received for a different workspace. + Switch to the correct workspace to approve requests. + `; + + if (sigSocketElements.requestsContainer) { + sigSocketElements.requestsContainer.insertBefore(warning, sigSocketElements.requestsContainer.firstChild); + } + + // Auto-remove warning after 10 seconds + setTimeout(() => { + warning.remove(); + }, 10000); +} + +// Update request button states +function updateRequestButtonStates(canApprove) { + document.querySelectorAll('.btn-approve, .btn-reject').forEach(btn => { + btn.disabled = !canApprove; + }); +} + +// Approve a sign request +async function approveSignRequest(requestId) { + try { + const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); + setButtonLoading(button, true); + + const response = await sendMessage('approveSignRequest', { requestId }); + + if (response?.success) { + showToast('Request approved and signed!', 'success'); + await refreshSigSocketRequests(); + } else { + throw new Error(getResponseError(response, 'approve request')); + } + } catch (error) { + showToast(`Failed to approve request: ${error.message}`, 'error'); + } finally { + const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); + setButtonLoading(button, false); + } +} + +// Reject a sign request +async function rejectSignRequest(requestId) { + try { + const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); + setButtonLoading(button, true); + + const response = await sendMessage('rejectSignRequest', { + requestId, + reason: 'User rejected via extension' + }); + + if (response?.success) { + showToast('Request rejected', 'info'); + await refreshSigSocketRequests(); + } else { + throw new Error(getResponseError(response, 'reject request')); + } + } catch (error) { + showToast(`Failed to reject request: ${error.message}`, 'error'); + } finally { + const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); + setButtonLoading(button, false); + } +} + +// Refresh SigSocket requests +async function refreshSigSocketRequests() { + try { + setButtonLoading(sigSocketElements.refreshRequestsBtn, true); + + const response = await sendMessage('getPendingSignRequests'); + if (response?.success) { + updateRequestsList(response.requests); + showToast('Requests refreshed', 'success'); + } else { + throw new Error(getResponseError(response, 'refresh requests')); + } + } catch (error) { + showToast(`Failed to refresh requests: ${error.message}`, 'error'); + } finally { + 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'); + } +} \ No newline at end of file diff --git a/crypto_vault_extension/styles/popup.css b/crypto_vault_extension/styles/popup.css index 9f4f4fb..bf242f6 100644 --- a/crypto_vault_extension/styles/popup.css +++ b/crypto_vault_extension/styles/popup.css @@ -1069,4 +1069,183 @@ input::placeholder, textarea::placeholder { .verification-icon svg { width: 20px; height: 20px; +} + +/* SigSocket Requests Styles */ +.sigsocket-section { + margin-bottom: 20px; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.section-header h3 { + margin: 0; + font-size: 16px; + font-weight: 600; +} + +.connection-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-secondary); +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent-error); + transition: background-color 0.3s ease; +} + +.status-dot.connected { + background: var(--accent-success); +} + +.requests-container { + min-height: 80px; +} + +.empty-state { + text-align: center; + padding: 20px; + color: var(--text-secondary); +} + +.empty-icon { + font-size: 24px; + margin-bottom: 8px; +} + +.empty-state p { + margin: 0 0 4px 0; + font-weight: 500; +} + +.empty-state small { + font-size: 11px; + opacity: 0.8; +} + +.request-item { + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 12px; + margin-bottom: 8px; + background: var(--bg-secondary); + transition: all 0.2s ease; +} + +.request-item:hover { + border-color: var(--border-focus); + box-shadow: 0 2px 8px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.1); +} + +.request-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 8px; +} + +.request-id { + font-family: 'Courier New', monospace; + font-size: 11px; + color: var(--text-secondary); + background: var(--bg-input); + padding: 2px 6px; + border-radius: 4px; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; +} + +.request-time { + font-size: 11px; + color: var(--text-secondary); +} + +.request-message { + margin: 8px 0; + padding: 8px; + background: var(--bg-input); + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 11px; + word-break: break-all; + max-height: 60px; + overflow-y: auto; +} + +.request-actions { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.btn-approve { + background: var(--accent-success); + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.btn-approve:hover { + background: hsl(var(--accent-hue), 65%, 40%); +} + +.btn-reject { + background: var(--accent-error); + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.btn-reject:hover { + background: hsl(0, 70%, 50%); +} + +.btn-approve:disabled, +.btn-reject:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.sigsocket-actions { + display: flex; + gap: 8px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border-color); +} + +.workspace-mismatch { + background: hsla(35, 85%, 85%, 0.8); + border: 1px solid hsla(35, 85%, 70%, 0.5); + color: hsl(35, 70%, 30%); + padding: 8px; + border-radius: 4px; + font-size: 12px; + margin-bottom: 8px; +} + +[data-theme="dark"] .workspace-mismatch { + background: hsla(35, 60%, 15%, 0.8); + border-color: hsla(35, 60%, 30%, 0.5); + color: hsl(35, 70%, 70%); } \ No newline at end of file diff --git a/crypto_vault_extension/wasm/wasm_app.js b/crypto_vault_extension/wasm/wasm_app.js index 57c87b8..ce95c45 100644 --- a/crypto_vault_extension/wasm/wasm_app.js +++ b/crypto_vault_extension/wasm/wasm_app.js @@ -277,6 +277,42 @@ export function is_unlocked() { return ret !== 0; } +/** + * Get the default public key for a workspace (keyspace) + * This returns the public key of the first keypair in the keyspace + * @param {string} workspace_id + * @returns {Promise} + */ +export function get_workspace_default_public_key(workspace_id) { + const ptr0 = passStringToWasm0(workspace_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.get_workspace_default_public_key(ptr0, len0); + return ret; +} + +/** + * Get the current unlocked public key as hex string + * @returns {string} + */ +export function get_current_unlocked_public_key() { + let deferred2_0; + let deferred2_1; + try { + const ret = wasm.get_current_unlocked_public_key(); + var ptr1 = ret[0]; + var len1 = ret[1]; + if (ret[3]) { + ptr1 = 0; len1 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred2_0 = ptr1; + deferred2_1 = len1; + return getStringFromWasm0(ptr1, len1); + } finally { + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } +} + /** * Get all keypairs from the current session * Returns an array of keypair objects with id, type, and metadata @@ -323,7 +359,7 @@ function passArray8ToWasm0(arg, malloc) { return ptr; } /** - * Sign message with current session + * Sign message with current session (requires selected keypair) * @param {Uint8Array} message * @returns {Promise} */ @@ -334,6 +370,41 @@ export function sign(message) { return ret; } +/** + * Get the current keyspace name + * @returns {string} + */ +export function get_current_keyspace_name() { + let deferred2_0; + let deferred2_1; + try { + const ret = wasm.get_current_keyspace_name(); + var ptr1 = ret[0]; + var len1 = ret[1]; + if (ret[3]) { + ptr1 = 0; len1 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred2_0 = ptr1; + deferred2_1 = len1; + return getStringFromWasm0(ptr1, len1); + } finally { + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } +} + +/** + * Sign message with default keypair (first keypair in keyspace) without changing session state + * @param {Uint8Array} message + * @returns {Promise} + */ +export function sign_with_default_keypair(message) { + const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.sign_with_default_keypair(ptr0, len0); + return ret; +} + /** * Verify a signature with the current session's selected keypair * @param {Uint8Array} message @@ -395,24 +466,391 @@ export function run_rhai(script) { return takeFromExternrefTable0(ret[0]); } -function __wbg_adapter_32(arg0, arg1, arg2) { - wasm.closure121_externref_shim(arg0, arg1, arg2); +function __wbg_adapter_34(arg0, arg1, arg2) { + wasm.closure174_externref_shim(arg0, arg1, arg2); } -function __wbg_adapter_35(arg0, arg1, arg2) { - wasm.closure150_externref_shim(arg0, arg1, arg2); +function __wbg_adapter_39(arg0, arg1) { + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha4436a3f79fb1a0f(arg0, arg1); } -function __wbg_adapter_38(arg0, arg1, arg2) { - wasm.closure227_externref_shim(arg0, arg1, arg2); +function __wbg_adapter_44(arg0, arg1, arg2) { + wasm.closure237_externref_shim(arg0, arg1, arg2); } -function __wbg_adapter_138(arg0, arg1, arg2, arg3) { - wasm.closure1879_externref_shim(arg0, arg1, arg2, arg3); +function __wbg_adapter_49(arg0, arg1) { + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf148c54a4a246cea(arg0, arg1); } +function __wbg_adapter_52(arg0, arg1, arg2) { + wasm.closure308_externref_shim(arg0, arg1, arg2); +} + +function __wbg_adapter_55(arg0, arg1, arg2) { + wasm.closure392_externref_shim(arg0, arg1, arg2); +} + +function __wbg_adapter_207(arg0, arg1, arg2, arg3) { + wasm.closure2046_externref_shim(arg0, arg1, arg2, arg3); +} + +const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"]; + const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"]; +const SigSocketConnectionFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_sigsocketconnection_free(ptr >>> 0, 1)); +/** + * WASM-bindgen wrapper for SigSocket client + * + * This provides a clean JavaScript API for the browser extension to: + * - Connect to SigSocket servers + * - Send responses to sign requests + * - Manage connection state + */ +export class SigSocketConnection { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + SigSocketConnectionFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_sigsocketconnection_free(ptr, 0); + } + /** + * Create a new SigSocket connection + */ + constructor() { + const ret = wasm.sigsocketconnection_new(); + this.__wbg_ptr = ret >>> 0; + SigSocketConnectionFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * Connect to a SigSocket server + * + * # Arguments + * * `server_url` - WebSocket server URL (e.g., "ws://localhost:8080/ws") + * * `public_key_hex` - Client's public key as hex string + * + * # Returns + * * `Ok(())` - Successfully connected + * * `Err(error)` - Connection failed + * @param {string} server_url + * @param {string} public_key_hex + * @returns {Promise} + */ + connect(server_url, public_key_hex) { + const ptr0 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(public_key_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.sigsocketconnection_connect(this.__wbg_ptr, ptr0, len0, ptr1, len1); + return ret; + } + /** + * Send a response to a sign request + * + * This should be called by the extension after the user has approved + * a sign request and the message has been signed. + * + * # Arguments + * * `request_id` - ID of the original request + * * `message_base64` - Original message (base64-encoded) + * * `signature_hex` - Signature as hex string + * + * # Returns + * * `Ok(())` - Response sent successfully + * * `Err(error)` - Failed to send response + * @param {string} request_id + * @param {string} message_base64 + * @param {string} signature_hex + * @returns {Promise} + */ + send_response(request_id, message_base64, signature_hex) { + const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(message_base64, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passStringToWasm0(signature_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len2 = WASM_VECTOR_LEN; + const ret = wasm.sigsocketconnection_send_response(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2); + return ret; + } + /** + * Send a rejection for a sign request + * + * This should be called when the user rejects a sign request. + * + * # Arguments + * * `request_id` - ID of the request to reject + * * `reason` - Reason for rejection (optional) + * + * # Returns + * * `Ok(())` - Rejection sent successfully + * * `Err(error)` - Failed to send rejection + * @param {string} request_id + * @param {string} reason + * @returns {Promise} + */ + send_rejection(request_id, reason) { + const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(reason, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.sigsocketconnection_send_rejection(this.__wbg_ptr, ptr0, len0, ptr1, len1); + return ret; + } + /** + * Disconnect from the SigSocket server + */ + disconnect() { + wasm.sigsocketconnection_disconnect(this.__wbg_ptr); + } + /** + * Check if connected to the server + * @returns {boolean} + */ + is_connected() { + const ret = wasm.sigsocketconnection_is_connected(this.__wbg_ptr); + return ret !== 0; + } +} + +const SigSocketManagerFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_sigsocketmanager_free(ptr >>> 0, 1)); +/** + * SigSocket manager for high-level operations + */ +export class SigSocketManager { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + SigSocketManagerFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_sigsocketmanager_free(ptr, 0); + } + /** + * Connect to SigSocket server with smart connection management + * + * This handles all connection logic: + * - Reuses existing connection if same workspace + * - Switches connection if different workspace + * - Creates new connection if none exists + * + * # Arguments + * * `workspace` - The workspace name to connect with + * * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws") + * * `event_callback` - JavaScript function to call when events occur + * + * # Returns + * * `Ok(connection_info)` - JSON string with connection details + * * `Err(error)` - If connection failed or workspace is invalid + * @param {string} workspace + * @param {string} server_url + * @param {Function} event_callback + * @returns {Promise} + */ + static connect_workspace_with_events(workspace, server_url, event_callback) { + const ptr0 = passStringToWasm0(workspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.sigsocketmanager_connect_workspace_with_events(ptr0, len0, ptr1, len1, event_callback); + return ret; + } + /** + * Connect to SigSocket server with a specific workspace (backward compatibility) + * + * This is a simpler version that doesn't set up event callbacks. + * Use connect_workspace_with_events for full functionality. + * + * # Arguments + * * `workspace` - The workspace name to connect with + * * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws") + * + * # Returns + * * `Ok(connection_info)` - JSON string with connection details + * * `Err(error)` - If connection failed or workspace is invalid + * @param {string} workspace + * @param {string} server_url + * @returns {Promise} + */ + static connect_workspace(workspace, server_url) { + const ptr0 = passStringToWasm0(workspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.sigsocketmanager_connect_workspace(ptr0, len0, ptr1, len1); + return ret; + } + /** + * Disconnect from SigSocket server + * + * # Returns + * * `Ok(())` - Successfully disconnected + * * `Err(error)` - If disconnect failed + * @returns {Promise} + */ + static disconnect() { + const ret = wasm.sigsocketmanager_disconnect(); + return ret; + } + /** + * Check if we can approve a specific sign request + * + * This validates that: + * 1. The request exists + * 2. The vault session is unlocked + * 3. The current workspace matches the request's target + * + * # Arguments + * * `request_id` - The ID of the request to validate + * + * # Returns + * * `Ok(true)` - Request can be approved + * * `Ok(false)` - Request cannot be approved + * * `Err(error)` - Validation error + * @param {string} request_id + * @returns {Promise} + */ + static can_approve_request(request_id) { + const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.sigsocketmanager_can_approve_request(ptr0, len0); + return ret; + } + /** + * Approve a sign request and send the signature to the server + * + * This performs the complete approval flow: + * 1. Validates the request can be approved + * 2. Signs the message using the vault + * 3. Sends the signature to the SigSocket server + * 4. Removes the request from pending list + * + * # Arguments + * * `request_id` - The ID of the request to approve + * + * # Returns + * * `Ok(signature)` - Base64-encoded signature that was sent + * * `Err(error)` - If approval failed + * @param {string} request_id + * @returns {Promise} + */ + static approve_request(request_id) { + const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.sigsocketmanager_approve_request(ptr0, len0); + return ret; + } + /** + * Reject a sign request + * + * # Arguments + * * `request_id` - The ID of the request to reject + * * `reason` - The reason for rejection + * + * # Returns + * * `Ok(())` - Request rejected successfully + * * `Err(error)` - If rejection failed + * @param {string} request_id + * @param {string} reason + * @returns {Promise} + */ + static reject_request(request_id, reason) { + const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(reason, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.sigsocketmanager_reject_request(ptr0, len0, ptr1, len1); + return ret; + } + /** + * Get pending requests filtered by current workspace + * + * This returns only the requests that the current vault session can handle, + * based on the unlocked workspace and its public key. + * + * # Returns + * * `Ok(requests_json)` - JSON array of filtered requests + * * `Err(error)` - If filtering failed + * @returns {Promise} + */ + static get_filtered_requests() { + const ret = wasm.sigsocketmanager_get_filtered_requests(); + return ret; + } + /** + * Add a pending sign request (called when request arrives from server) + * + * # Arguments + * * `request_json` - JSON string containing the sign request + * + * # Returns + * * `Ok(())` - Request added successfully + * * `Err(error)` - If adding failed + * @param {string} request_json + */ + static add_pending_request(request_json) { + const ptr0 = passStringToWasm0(request_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.sigsocketmanager_add_pending_request(ptr0, len0); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } + /** + * Get connection status + * + * # Returns + * * `Ok(status_json)` - JSON object with connection status + * * `Err(error)` - If getting status failed + * @returns {string} + */ + static get_connection_status() { + let deferred2_0; + let deferred2_1; + try { + const ret = wasm.sigsocketmanager_get_connection_status(); + var ptr1 = ret[0]; + var len1 = ret[1]; + if (ret[3]) { + ptr1 = 0; len1 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred2_0 = ptr1; + deferred2_1 = len1; + return getStringFromWasm0(ptr1, len1); + } finally { + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } + } + /** + * Clear all pending requests + * + * # Returns + * * `Ok(())` - Requests cleared successfully + */ + static clear_pending_requests() { + const ret = wasm.sigsocketmanager_clear_pending_requests(); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } + } +} + async function __wbg_load(module, imports) { if (typeof Response === 'function' && module instanceof Response) { if (typeof WebAssembly.instantiateStreaming === 'function') { @@ -459,6 +897,9 @@ function __wbg_get_imports() { const ret = arg0.call(arg1, arg2); return ret; }, arguments) }; + imports.wbg.__wbg_close_2893b7d056a0627d = function() { return handleError(function (arg0) { + arg0.close(); + }, arguments) }; imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) { const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3); return ret; @@ -467,6 +908,10 @@ function __wbg_get_imports() { const ret = arg0.crypto; return ret; }; + imports.wbg.__wbg_data_432d9c3df2630942 = function(arg0) { + const ret = arg0.data; + return ret; + }; imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) { console.error(arg0); }; @@ -539,10 +984,23 @@ function __wbg_get_imports() { const ret = result; return ret; }; + imports.wbg.__wbg_instanceof_Window_def73ea0955fc569 = function(arg0) { + let result; + try { + result = arg0 instanceof Window; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) { const ret = arg0.length; return ret; }; + imports.wbg.__wbg_log_c222819a41e063d3 = function(arg0) { + console.log(arg0); + }; imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) { const ret = arg0.msCrypto; return ret; @@ -558,7 +1016,7 @@ function __wbg_get_imports() { const a = state0.a; state0.a = 0; try { - return __wbg_adapter_138(a, state0.b, arg0, arg1); + return __wbg_adapter_207(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -577,6 +1035,10 @@ function __wbg_get_imports() { const ret = new Array(); return ret; }; + imports.wbg.__wbg_new_92c54fc74574ef55 = function() { return handleError(function (arg0, arg1) { + const ret = new WebSocket(getStringFromWasm0(arg0, arg1)); + return ret; + }, arguments) }; imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) { const ret = new Uint8Array(arg0); return ret; @@ -609,6 +1071,12 @@ function __wbg_get_imports() { const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2)); return ret; }, arguments) }; + imports.wbg.__wbg_onConnectionStateChanged_b0dc098522afadba = function(arg0) { + onConnectionStateChanged(arg0 !== 0); + }; + imports.wbg.__wbg_onSignRequestReceived_93232ba7a0919705 = function(arg0, arg1, arg2, arg3) { + onSignRequestReceived(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3)); + }; imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) { const ret = arg0.open(getStringFromWasm0(arg1, arg2)); return ret; @@ -643,6 +1111,10 @@ function __wbg_get_imports() { imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) { arg0.randomFillSync(arg1); }, arguments) }; + imports.wbg.__wbg_readyState_7ef6e63c349899ed = function(arg0) { + const ret = arg0.readyState; + return ret; + }; imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () { const ret = module.require; return ret; @@ -655,12 +1127,38 @@ function __wbg_get_imports() { const ret = arg0.result; return ret; }, arguments) }; + imports.wbg.__wbg_send_0293179ba074ffb4 = function() { return handleError(function (arg0, arg1, arg2) { + arg0.send(getStringFromWasm0(arg1, arg2)); + }, arguments) }; + imports.wbg.__wbg_setTimeout_f2fe5af8e3debeb3 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.setTimeout(arg1, arg2); + return ret; + }, arguments) }; imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) { arg0.set(arg1, arg2 >>> 0); }; + imports.wbg.__wbg_set_bb8cecf6a62b9f46 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.set(arg0, arg1, arg2); + return ret; + }, arguments) }; + imports.wbg.__wbg_setbinaryType_92fa1ffd873b327c = function(arg0, arg1) { + arg0.binaryType = __wbindgen_enum_BinaryType[arg1]; + }; + imports.wbg.__wbg_setonclose_14fc475a49d488fc = function(arg0, arg1) { + arg0.onclose = arg1; + }; + imports.wbg.__wbg_setonerror_8639efe354b947cd = function(arg0, arg1) { + arg0.onerror = arg1; + }; imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) { arg0.onerror = arg1; }; + imports.wbg.__wbg_setonmessage_6eccab530a8fb4c7 = function(arg0, arg1) { + arg0.onmessage = arg1; + }; + imports.wbg.__wbg_setonopen_2da654e1f39745d5 = function(arg0, arg1) { + arg0.onopen = arg1; + }; imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) { arg0.onsuccess = arg1; }; @@ -695,6 +1193,10 @@ function __wbg_get_imports() { const ret = arg0.then(arg1); return ret; }; + imports.wbg.__wbg_then_48b406749878a531 = function(arg0, arg1, arg2) { + const ret = arg0.then(arg1, arg2); + return ret; + }; imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) { const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]); return ret; @@ -703,6 +1205,9 @@ function __wbg_get_imports() { const ret = arg0.versions; return ret; }; + imports.wbg.__wbg_warn_4ca3906c248c47c4 = function(arg0) { + console.warn(arg0); + }; imports.wbg.__wbindgen_cb_drop = function(arg0) { const obj = arg0.original; if (obj.cnt-- == 1) { @@ -712,16 +1217,40 @@ function __wbg_get_imports() { const ret = false; return ret; }; - imports.wbg.__wbindgen_closure_wrapper378 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 122, __wbg_adapter_32); + imports.wbg.__wbindgen_closure_wrapper1015 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 309, __wbg_adapter_52); return ret; }; - imports.wbg.__wbindgen_closure_wrapper549 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 151, __wbg_adapter_35); + imports.wbg.__wbindgen_closure_wrapper1320 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 393, __wbg_adapter_55); return ret; }; - imports.wbg.__wbindgen_closure_wrapper857 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 228, __wbg_adapter_38); + imports.wbg.__wbindgen_closure_wrapper423 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper424 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper425 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_39); + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper428 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper767 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper770 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_49); return ret; }; imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { @@ -778,6 +1307,14 @@ function __wbg_get_imports() { const ret = wasm.memory; return ret; }; + imports.wbg.__wbindgen_string_get = function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; imports.wbg.__wbindgen_string_new = function(arg0, arg1) { const ret = getStringFromWasm0(arg0, arg1); return ret; diff --git a/crypto_vault_extension/wasm/wasm_app_bg.wasm b/crypto_vault_extension/wasm/wasm_app_bg.wasm index 6ededfd..2c5ffe3 100644 Binary files a/crypto_vault_extension/wasm/wasm_app_bg.wasm and b/crypto_vault_extension/wasm/wasm_app_bg.wasm differ diff --git a/sigsocket_client/src/wasm.rs b/sigsocket_client/src/wasm.rs index ff6c43c..b40e2d9 100644 --- a/sigsocket_client/src/wasm.rs +++ b/sigsocket_client/src/wasm.rs @@ -525,7 +525,13 @@ impl WasmClient { impl Drop for WasmClient { fn drop(&mut self) { - // Cleanup will be handled by the WebSocket close + // Close WebSocket connection if it exists + if let Some(ws) = self.websocket.take() { + ws.close().unwrap_or_else(|e| { + web_sys::console::warn_1(&format!("Failed to close WebSocket: {:?}", e).into()); + }); + web_sys::console::log_1(&"🔌 WebSocket connection closed on drop".into()); + } } } diff --git a/wasm_app/src/sigsocket_bindings.rs b/wasm_app/src/sigsocket_bindings.rs index b896424..c681f8e 100644 --- a/wasm_app/src/sigsocket_bindings.rs +++ b/wasm_app/src/sigsocket_bindings.rs @@ -5,6 +5,7 @@ use wasm_bindgen::prelude::*; use serde::{Deserialize, Serialize}; use sigsocket_client::{SigSocketClient, SignRequest, SignRequestHandler, Result as SigSocketResult, SigSocketError}; use web_sys::console; +use base64::prelude::*; use crate::vault_bindings::{get_workspace_default_public_key, get_current_keyspace_name, is_unlocked, sign_with_default_keypair}; @@ -31,7 +32,32 @@ impl ExtensionNotificationHandler { impl SignRequestHandler for ExtensionNotificationHandler { fn handle_sign_request(&self, request: &SignRequest) -> SigSocketResult> { - // Create event object for JavaScript + console_log!("📨 WASM: Handling sign request: {}", request.id); + + // First, store the request in the WASM client + let store_result = SIGSOCKET_CLIENT.with(|c| { + let mut client_opt = c.borrow_mut(); + if let Some(client) = client_opt.as_mut() { + // Get the connected public key as the target + if let Some(target_public_key) = client.connected_public_key() { + client.add_pending_request(request.clone(), target_public_key.to_string()); + console_log!("✅ WASM: Stored sign request: {}", request.id); + Ok(()) + } else { + Err(SigSocketError::Other("No connected public key".to_string())) + } + } else { + Err(SigSocketError::Other("No SigSocket client available".to_string())) + } + }); + + // If storage failed, return error + if let Err(e) = store_result { + console_log!("❌ WASM: Failed to store request: {:?}", e); + return Err(e); + } + + // Create event object for JavaScript notification let event = js_sys::Object::new(); js_sys::Reflect::set(&event, &"type".into(), &"sign_request".into()) .map_err(|_| SigSocketError::Other("Failed to set event type".to_string()))?; @@ -40,17 +66,16 @@ impl SignRequestHandler for ExtensionNotificationHandler { js_sys::Reflect::set(&event, &"message".into(), &request.message.clone().into()) .map_err(|_| SigSocketError::Other("Failed to set message".to_string()))?; - // Store the request in our pending requests (this will be done by the client) - // and notify the extension + // Notify the extension match self.callback.call1(&wasm_bindgen::JsValue::NULL, &event) { Ok(_) => { - console_log!("Notified extension about sign request: {}", request.id); + console_log!("✅ WASM: Notified extension about sign request: {}", request.id); // Return an error to indicate this request should not be auto-signed // The extension will handle the approval flow Err(SigSocketError::Other("Request forwarded to extension for approval".to_string())) } Err(e) => { - console_log!("Failed to notify extension: {:?}", e); + console_log!("❌ WASM: Failed to notify extension: {:?}", e); Err(SigSocketError::Other("Extension notification failed".to_string())) } } @@ -72,10 +97,12 @@ pub struct SigSocketManager; #[wasm_bindgen] impl SigSocketManager { - /// Connect to SigSocket server with a specific workspace and event callback + /// Connect to SigSocket server with smart connection management /// - /// This establishes a real WebSocket connection using the workspace's default public key - /// and integrates with the vault system for security validation. + /// This handles all connection logic: + /// - Reuses existing connection if same workspace + /// - Switches connection if different workspace + /// - Creates new connection if none exists /// /// # Arguments /// * `workspace` - The workspace name to connect with @@ -98,25 +125,56 @@ impl SigSocketManager { let public_key_bytes = hex::decode(&public_key_hex) .map_err(|e| JsValue::from_str(&format!("Invalid public key format: {}", e)))?; - // 3. Create SigSocket client with extension notification handler - let mut client = SigSocketClient::new(server_url, public_key_bytes) - .map_err(|e| JsValue::from_str(&format!("Failed to create client: {:?}", e)))?; + // 3. Check if already connected to same workspace and handle disconnection + let should_connect = SIGSOCKET_CLIENT.with(|c| { + let mut client_opt = c.borrow_mut(); - // Set up extension notification handler using existing API - let handler = ExtensionNotificationHandler::new(event_callback.clone()); - client.set_sign_handler(handler); + // Check if we already have a client for this workspace + if let Some(existing_client) = client_opt.as_ref() { + if let Some(existing_key) = existing_client.connected_public_key() { + if existing_key == hex::encode(&public_key_bytes) && existing_client.is_connected() { + console_log!("🔄 WASM: Already connected to workspace: {}", workspace); + return false; // Reuse existing connection + } else { + console_log!("🔄 WASM: Switching workspace from {} to {}", + existing_key, hex::encode(&public_key_bytes)); - // 4. Connect to the WebSocket server - client.connect().await - .map_err(|e| JsValue::from_str(&format!("Connection failed: {:?}", e)))?; + // Disconnect the old client + *client_opt = None; // This will drop the old client and close WebSocket + console_log!("🔌 WASM: Disconnected from old workspace"); - console_log!("SigSocket connected successfully to {}", server_url); + return true; // Need new connection + } + } + } - // 5. Store the connected client - SIGSOCKET_CLIENT.with(|c| { - *c.borrow_mut() = Some(client); + true // Need new connection, no old one to disconnect }); + // 4. Create and connect if needed + if should_connect { + console_log!("🔗 WASM: Creating new connection for workspace: {}", workspace); + + // Create new client + let mut client = SigSocketClient::new(server_url, public_key_bytes.clone()) + .map_err(|e| JsValue::from_str(&format!("Failed to create client: {:?}", e)))?; + + // Set up extension notification handler + let handler = ExtensionNotificationHandler::new(event_callback.clone()); + client.set_sign_handler(handler); + + // Connect to the WebSocket server + client.connect().await + .map_err(|e| JsValue::from_str(&format!("Connection failed: {:?}", e)))?; + + console_log!("✅ WASM: Connected to SigSocket server for workspace: {}", workspace); + + // Store the connected client + SIGSOCKET_CLIENT.with(|c| { + *c.borrow_mut() = Some(client); + }); + } + // 6. Return connection info let connection_info = SigSocketConnectionInfo { workspace: workspace.to_string(), @@ -150,7 +208,7 @@ impl SigSocketManager { } /// Disconnect from SigSocket server - /// + /// /// # Returns /// * `Ok(())` - Successfully disconnected /// * `Err(error)` - If disconnect failed @@ -158,9 +216,16 @@ impl SigSocketManager { pub async fn disconnect() -> Result<(), JsValue> { SIGSOCKET_CLIENT.with(|c| { let mut client_opt = c.borrow_mut(); - if let Some(_client) = client_opt.take() { - // client.disconnect().await?; // Will be async in real implementation - console_log!("SigSocket client disconnected"); + if let Some(client) = client_opt.take() { + let workspace_info = client.connected_public_key() + .map(|key| key[..16].to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + // Dropping the client will close the WebSocket connection + drop(client); + console_log!("🔌 WASM: Disconnected SigSocket client (was: {}...)", workspace_info); + } else { + console_log!("🔌 WASM: No SigSocket client to disconnect"); } Ok(()) }) @@ -254,27 +319,51 @@ impl SigSocketManager { // 3. Sign with vault let signature_result = sign_with_default_keypair(&message_bytes).await?; - let signature_obj: serde_json::Value = serde_json::from_str(&signature_result.as_string().unwrap()) - .map_err(|e| JsValue::from_str(&format!("Failed to parse signature: {}", e)))?; + let signature_hex = signature_result.as_string() + .ok_or_else(|| JsValue::from_str("Signature result is not a string"))?; + + // Convert hex signature to base64 for SigSocket protocol + let signature_bytes = hex::decode(&signature_hex) + .map_err(|e| JsValue::from_str(&format!("Invalid hex signature: {}", e)))?; + let signature_base64 = base64::prelude::BASE64_STANDARD.encode(&signature_bytes); - let signature_base64 = signature_obj["signature"].as_str() - .ok_or_else(|| JsValue::from_str("Invalid signature format"))?; - - // 4. Send response to server and remove request + // 4. Get original message for response + let original_message = SIGSOCKET_CLIENT.with(|c| { + let client = c.borrow(); + let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected"))?; + + let request = client.get_pending_request(request_id) + .ok_or_else(|| JsValue::from_str("Request not found"))?; + + Ok::(request.request.message.clone()) + })?; + + // 5. Send response to server (create a new scope to avoid borrowing issues) + { + let client_ref = SIGSOCKET_CLIENT.with(|c| { + c.borrow().as_ref().map(|client| client as *const SigSocketClient) + }).ok_or_else(|| JsValue::from_str("Not connected"))?; + + // SAFETY: We know the client exists and we're using it synchronously + let client = unsafe { &*client_ref }; + + client.send_response(request_id, &original_message, &signature_base64).await + .map_err(|e| JsValue::from_str(&format!("Failed to send response: {:?}", e)))?; + + console_log!("✅ WASM: Sent signature response to server for request: {}", request_id); + } + + // 6. Remove the request after successful send SIGSOCKET_CLIENT.with(|c| { let mut client = c.borrow_mut(); - let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected"))?; - - // Send response (will be async in real implementation) - // client.send_response(request_id, &original_request.message, signature_base64).await?; - - // Remove the request - client.remove_pending_request(request_id); - - console_log!("Approved and sent signature for request: {}", request_id); - - Ok(signature_base64.to_string()) - }) + if let Some(client) = client.as_mut() { + client.remove_pending_request(request_id); + console_log!("✅ WASM: Removed request from pending list: {}", request_id); + } + }); + + console_log!("🎉 WASM: Successfully approved and sent signature for request: {}", request_id); + Ok(signature_base64) } /// Reject a sign request @@ -288,20 +377,32 @@ impl SigSocketManager { /// * `Err(error)` - If rejection failed #[wasm_bindgen] pub async fn reject_request(request_id: &str, reason: &str) -> Result<(), JsValue> { + // Send rejection to server first + { + let client_ref = SIGSOCKET_CLIENT.with(|c| { + c.borrow().as_ref().map(|client| client as *const SigSocketClient) + }).ok_or_else(|| JsValue::from_str("Not connected"))?; + + // SAFETY: We know the client exists and we're using it synchronously + let client = unsafe { &*client_ref }; + + client.send_rejection(request_id, reason).await + .map_err(|e| JsValue::from_str(&format!("Failed to send rejection: {:?}", e)))?; + + console_log!("✅ WASM: Sent rejection to server for request: {}", request_id); + } + + // Remove the request after successful send SIGSOCKET_CLIENT.with(|c| { let mut client = c.borrow_mut(); - let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected"))?; + if let Some(client) = client.as_mut() { + client.remove_pending_request(request_id); + console_log!("✅ WASM: Removed rejected request from pending list: {}", request_id); + } + }); - // Send rejection (will be async in real implementation) - // client.send_rejection(request_id, reason).await?; - - // Remove the request - client.remove_pending_request(request_id); - - console_log!("Rejected request {}: {}", request_id, reason); - - Ok(()) - }) + console_log!("🚫 WASM: Successfully rejected request: {} (reason: {})", request_id, reason); + Ok(()) } /// Get pending requests filtered by current workspace