//! SigSocket bindings for WASM - integrates sigsocket_client with vault system use std::cell::RefCell; 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}; // Global SigSocket client instance thread_local! { static SIGSOCKET_CLIENT: RefCell> = RefCell::new(None); } // Helper macro for console logging macro_rules! console_log { ($($t:tt)*) => (console::log_1(&format!($($t)*).into())) } /// Extension notification handler that forwards requests to JavaScript pub struct ExtensionNotificationHandler { callback: js_sys::Function, } impl ExtensionNotificationHandler { pub fn new(callback: js_sys::Function) -> Self { Self { callback } } } impl SignRequestHandler for ExtensionNotificationHandler { fn handle_sign_request(&self, request: &SignRequest) -> SigSocketResult> { 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()))?; js_sys::Reflect::set(&event, &"request_id".into(), &request.id.clone().into()) .map_err(|_| SigSocketError::Other("Failed to set request_id".to_string()))?; js_sys::Reflect::set(&event, &"message".into(), &request.message.clone().into()) .map_err(|_| SigSocketError::Other("Failed to set message".to_string()))?; // Notify the extension match self.callback.call1(&wasm_bindgen::JsValue::NULL, &event) { Ok(_) => { 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!("❌ WASM: Failed to notify extension: {:?}", e); Err(SigSocketError::Other("Extension notification failed".to_string())) } } } } /// Connection information for SigSocket #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SigSocketConnectionInfo { pub workspace: String, pub public_key: String, pub is_connected: bool, pub server_url: String, } /// SigSocket manager for high-level operations #[wasm_bindgen] pub struct SigSocketManager; #[wasm_bindgen] impl SigSocketManager { /// 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 #[wasm_bindgen] pub async fn connect_workspace_with_events(workspace: &str, server_url: &str, event_callback: &js_sys::Function) -> Result { // 1. Validate workspace exists and get default public key from vault let public_key_js = get_workspace_default_public_key(workspace).await .map_err(|e| JsValue::from_str(&format!("Failed to get workspace public key: {:?}", e)))?; let public_key_hex = public_key_js.as_string() .ok_or_else(|| JsValue::from_str("Public key is not a string"))?; // 2. Decode public key let public_key_bytes = hex::decode(&public_key_hex) .map_err(|e| JsValue::from_str(&format!("Invalid public key format: {}", 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(); // 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)); // Disconnect the old client *client_opt = None; // This will drop the old client and close WebSocket console_log!("🔌 WASM: Disconnected from old workspace"); return true; // Need new connection } } } 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(), public_key: public_key_hex.clone(), is_connected: true, server_url: server_url.to_string(), }; // 7. Serialize and return connection info serde_json::to_string(&connection_info) .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))) } /// 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 #[wasm_bindgen] pub async fn connect_workspace(workspace: &str, server_url: &str) -> Result { // Create a dummy callback that just logs let dummy_callback = js_sys::Function::new_no_args("console.log('SigSocket event:', arguments[0]);"); Self::connect_workspace_with_events(workspace, server_url, &dummy_callback).await } /// Disconnect from SigSocket server /// /// # Returns /// * `Ok(())` - Successfully disconnected /// * `Err(error)` - If disconnect failed #[wasm_bindgen] pub async fn disconnect() -> Result<(), JsValue> { SIGSOCKET_CLIENT.with(|c| { let mut client_opt = c.borrow_mut(); 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(()) }) } /// 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 #[wasm_bindgen] pub async fn can_approve_request(request_id: &str) -> Result { // 1. Check if vault session is unlocked if !is_unlocked() { return Ok(false); } // 2. Get current workspace and its public key let current_workspace = get_current_keyspace_name() .map_err(|e| JsValue::from_str(&format!("Failed to get current workspace: {:?}", e)))?; let current_public_key_js = get_workspace_default_public_key(¤t_workspace).await .map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?; let current_public_key = current_public_key_js.as_string() .ok_or_else(|| JsValue::from_str("Current public key is not a string"))?; // 3. Check the request SIGSOCKET_CLIENT.with(|c| { let client = c.borrow(); let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?; // Get the request let request = client.get_pending_request(request_id) .ok_or_else(|| JsValue::from_str("Request not found"))?; // Check if request matches current session let can_approve = request.target_public_key == current_public_key; console_log!("Can approve request {}: {} (current: {}, target: {})", request_id, can_approve, current_public_key, request.target_public_key); Ok(can_approve) }) } /// 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 #[wasm_bindgen] pub async fn approve_request(request_id: &str) -> Result { // 1. Validate we can approve this request if !Self::can_approve_request(request_id).await? { return Err(JsValue::from_str("Cannot approve this request")); } // 2. Get request details and sign the message let (message_bytes, original_request) = 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"))?; // Decode the message let message_bytes = request.message_bytes() .map_err(|e| JsValue::from_str(&format!("Invalid message format: {}", e)))?; Ok::<(Vec, SignRequest), JsValue>((message_bytes, request.request.clone())) })?; // 3. Sign with vault let signature_result = sign_with_default_keypair(&message_bytes).await?; 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); // 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(); 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 /// /// # 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 #[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(); if let Some(client) = client.as_mut() { client.remove_pending_request(request_id); console_log!("✅ WASM: Removed rejected request from pending list: {}", request_id); } }); console_log!("🚫 WASM: Successfully rejected request: {} (reason: {})", request_id, reason); Ok(()) } /// 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 #[wasm_bindgen] pub async fn get_filtered_requests() -> Result { // If vault is locked, return empty array if !is_unlocked() { return Ok("[]".to_string()); } // Get current workspace public key let current_workspace = get_current_keyspace_name() .map_err(|e| JsValue::from_str(&format!("Failed to get current workspace: {:?}", e)))?; let current_public_key_js = get_workspace_default_public_key(¤t_workspace).await .map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?; let current_public_key = current_public_key_js.as_string() .ok_or_else(|| JsValue::from_str("Current public key is not a string"))?; // Filter requests for current workspace SIGSOCKET_CLIENT.with(|c| { let client = c.borrow(); let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?; let filtered_requests: Vec<_> = client.get_requests_for_public_key(¤t_public_key); console_log!("Filtered requests: {} total, {} for current workspace", client.pending_request_count(), filtered_requests.len()); // Serialize and return serde_json::to_string(&filtered_requests) .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))) }) } /// 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 #[wasm_bindgen] pub fn add_pending_request(request_json: &str) -> Result<(), JsValue> { // Parse the request let request: SignRequest = serde_json::from_str(request_json) .map_err(|e| JsValue::from_str(&format!("Invalid request JSON: {}", e)))?; SIGSOCKET_CLIENT.with(|c| { let mut client = c.borrow_mut(); let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?; // Get the connected public key as the target let target_public_key = client.connected_public_key() .ok_or_else(|| JsValue::from_str("No connected public key"))? .to_string(); // Add the request client.add_pending_request(request, target_public_key); console_log!("Added pending request: {}", request_json); Ok(()) }) } /// Get connection status /// /// # Returns /// * `Ok(status_json)` - JSON object with connection status /// * `Err(error)` - If getting status failed #[wasm_bindgen] pub fn get_connection_status() -> Result { SIGSOCKET_CLIENT.with(|c| { let client = c.borrow(); if let Some(client) = client.as_ref() { let status = serde_json::json!({ "is_connected": client.is_connected(), "connected_public_key": client.connected_public_key(), "pending_request_count": client.pending_request_count(), "server_url": client.url() }); Ok(status.to_string()) } else { let status = serde_json::json!({ "is_connected": false, "connected_public_key": null, "pending_request_count": 0, "server_url": null }); Ok(status.to_string()) } }) } /// Clear all pending requests /// /// # Returns /// * `Ok(())` - Requests cleared successfully #[wasm_bindgen] pub fn clear_pending_requests() -> Result<(), JsValue> { SIGSOCKET_CLIENT.with(|c| { let mut client = c.borrow_mut(); if let Some(client) = client.as_mut() { client.clear_pending_requests(); console_log!("Cleared all pending requests"); } Ok(()) }) } }