feat: Enhance request management in SigSocket client with new methods and structures
This commit is contained in:
		| @@ -1,9 +1,18 @@ | ||||
| //! Main client interface for sigsocket communication | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use alloc::{string::String, vec::Vec, boxed::Box}; | ||||
| use alloc::{string::String, vec::Vec, boxed::Box, string::ToString}; | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use alloc::collections::BTreeMap as HashMap; | ||||
|  | ||||
| use crate::{SignRequest, SignResponse, Result, SigSocketError}; | ||||
| use crate::protocol::ManagedSignRequest; | ||||
|  | ||||
|  | ||||
|  | ||||
| /// Connection state of the sigsocket client | ||||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||
| @@ -67,6 +76,10 @@ pub struct SigSocketClient { | ||||
|     state: ConnectionState, | ||||
|     /// Sign request handler | ||||
|     sign_handler: Option<Box<dyn SignRequestHandler>>, | ||||
|     /// Pending sign requests managed by the client | ||||
|     pending_requests: HashMap<String, ManagedSignRequest>, | ||||
|     /// Connected public key (hex-encoded) - set when connection is established | ||||
|     connected_public_key: Option<String>, | ||||
|     /// Platform-specific implementation | ||||
|     #[cfg(not(target_arch = "wasm32"))] | ||||
|     inner: Option<crate::native::NativeClient>, | ||||
| @@ -100,14 +113,16 @@ impl SigSocketClient { | ||||
|             public_key, | ||||
|             state: ConnectionState::Disconnected, | ||||
|             sign_handler: None, | ||||
|             pending_requests: HashMap::new(), | ||||
|             connected_public_key: None, | ||||
|             inner: None, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /// Set the sign request handler | ||||
|     ///  | ||||
|     /// | ||||
|     /// This handler will be called whenever the server sends a signature request. | ||||
|     ///  | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `handler` - Implementation of SignRequestHandler trait | ||||
|     pub fn set_sign_handler<H>(&mut self, handler: H) | ||||
| @@ -117,6 +132,8 @@ impl SigSocketClient { | ||||
|         self.sign_handler = Some(Box::new(handler)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     /// Get the current connection state | ||||
|     pub fn state(&self) -> ConnectionState { | ||||
|         self.state | ||||
| @@ -136,6 +153,109 @@ impl SigSocketClient { | ||||
|     pub fn url(&self) -> &str { | ||||
|         &self.url | ||||
|     } | ||||
|  | ||||
|     /// Get the connected public key (if connected) | ||||
|     pub fn connected_public_key(&self) -> Option<&str> { | ||||
|         self.connected_public_key.as_deref() | ||||
|     } | ||||
|  | ||||
|     // === Request Management Methods === | ||||
|  | ||||
|     /// Add a pending sign request | ||||
|     /// | ||||
|     /// This is typically called when a sign request is received from the server. | ||||
|     /// The request will be stored and can be retrieved later for processing. | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `request` - The sign request to add | ||||
|     /// * `target_public_key` - The public key this request is intended for | ||||
|     pub fn add_pending_request(&mut self, request: SignRequest, target_public_key: String) { | ||||
|         let managed_request = ManagedSignRequest::new(request, target_public_key); | ||||
|         self.pending_requests.insert(managed_request.id().to_string(), managed_request); | ||||
|     } | ||||
|  | ||||
|     /// Remove a pending request by ID | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `request_id` - The ID of the request to remove | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// * `Some(request)` - The removed request if it existed | ||||
|     /// * `None` - If no request with that ID was found | ||||
|     pub fn remove_pending_request(&mut self, request_id: &str) -> Option<ManagedSignRequest> { | ||||
|         self.pending_requests.remove(request_id) | ||||
|     } | ||||
|  | ||||
|     /// Get a pending request by ID | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `request_id` - The ID of the request to retrieve | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// * `Some(request)` - The request if it exists | ||||
|     /// * `None` - If no request with that ID was found | ||||
|     pub fn get_pending_request(&self, request_id: &str) -> Option<&ManagedSignRequest> { | ||||
|         self.pending_requests.get(request_id) | ||||
|     } | ||||
|  | ||||
|     /// Get all pending requests | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// * A reference to the HashMap containing all pending requests | ||||
|     pub fn get_pending_requests(&self) -> &HashMap<String, ManagedSignRequest> { | ||||
|         &self.pending_requests | ||||
|     } | ||||
|  | ||||
|     /// Get pending requests filtered by public key | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `public_key` - The public key to filter by (hex-encoded) | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// * A vector of references to requests for the specified public key | ||||
|     pub fn get_requests_for_public_key(&self, public_key: &str) -> Vec<&ManagedSignRequest> { | ||||
|         self.pending_requests | ||||
|             .values() | ||||
|             .filter(|req| req.is_for_public_key(public_key)) | ||||
|             .collect() | ||||
|     } | ||||
|  | ||||
|     /// Check if a request can be handled for the given public key | ||||
|     /// | ||||
|     /// This performs protocol-level validation without cryptographic operations. | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `request` - The sign request to validate | ||||
|     /// * `public_key` - The public key to check against (hex-encoded) | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// * `true` - If the request can be handled for this public key | ||||
|     /// * `false` - If the request cannot be handled | ||||
|     pub fn can_handle_request_for_key(&self, request: &SignRequest, public_key: &str) -> bool { | ||||
|         // Basic protocol validation | ||||
|         if request.id.is_empty() || request.message.is_empty() { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // Check if we can decode the message | ||||
|         if request.message_bytes().is_err() { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // For now, we assume any valid request can be handled for any public key | ||||
|         // More sophisticated validation can be added here | ||||
|         !public_key.is_empty() | ||||
|     } | ||||
|  | ||||
|     /// Clear all pending requests | ||||
|     pub fn clear_pending_requests(&mut self) { | ||||
|         self.pending_requests.clear(); | ||||
|     } | ||||
|  | ||||
|     /// Get the count of pending requests | ||||
|     pub fn pending_request_count(&self) -> usize { | ||||
|         self.pending_requests.len() | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Platform-specific implementations will be added in separate modules | ||||
| @@ -176,6 +296,7 @@ impl SigSocketClient { | ||||
|         } | ||||
|  | ||||
|         self.state = ConnectionState::Connected; | ||||
|         self.connected_public_key = Some(self.public_key_hex()); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
| @@ -190,17 +311,19 @@ impl SigSocketClient { | ||||
|         } | ||||
|         self.inner = None; | ||||
|         self.state = ConnectionState::Disconnected; | ||||
|         self.connected_public_key = None; | ||||
|         self.clear_pending_requests(); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Send a sign response to the server | ||||
|     ///  | ||||
|     /// | ||||
|     /// This is typically called after the user has approved a signature request | ||||
|     /// and the application has generated the signature. | ||||
|     ///  | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `response` - The sign response containing the signature | ||||
|     ///  | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// * `Ok(())` - Response sent successfully | ||||
|     /// * `Err(error)` - Failed to send response | ||||
| @@ -215,6 +338,41 @@ impl SigSocketClient { | ||||
|             Err(SigSocketError::NotConnected) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Send a response for a specific request ID with signature | ||||
|     /// | ||||
|     /// This is a convenience method that creates a SignResponse and sends it. | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `request_id` - The ID of the request being responded to | ||||
|     /// * `message` - The original message (base64-encoded) | ||||
|     /// * `signature` - The signature (base64-encoded) | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// * `Ok(())` - Response sent successfully | ||||
|     /// * `Err(error)` - Failed to send response | ||||
|     pub async fn send_response(&self, request_id: &str, message: &str, signature: &str) -> Result<()> { | ||||
|         let response = SignResponse::new(request_id, message, signature); | ||||
|         self.send_sign_response(&response).await | ||||
|     } | ||||
|  | ||||
|     /// Send a rejection for a specific request ID | ||||
|     /// | ||||
|     /// This sends an error response to indicate the request was rejected. | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// * `request_id` - The ID of the request being rejected | ||||
|     /// * `reason` - The reason for rejection | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// * `Ok(())` - Rejection sent successfully | ||||
|     /// * `Err(error)` - Failed to send rejection | ||||
|     pub async fn send_rejection(&self, request_id: &str, _reason: &str) -> Result<()> { | ||||
|         // For now, we'll send an empty signature to indicate rejection | ||||
|         // This can be improved with a proper rejection protocol | ||||
|         let response = SignResponse::new(request_id, "", ""); | ||||
|         self.send_sign_response(&response).await | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Drop for SigSocketClient { | ||||
| @@ -222,3 +380,5 @@ impl Drop for SigSocketClient { | ||||
|         // Cleanup will be handled by the platform-specific implementations | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -60,10 +60,13 @@ mod native; | ||||
| mod wasm; | ||||
|  | ||||
| pub use error::{SigSocketError, Result}; | ||||
| pub use protocol::{SignRequest, SignResponse}; | ||||
| pub use protocol::{SignRequest, SignResponse, ManagedSignRequest, RequestStatus}; | ||||
| pub use client::{SigSocketClient, SignRequestHandler, ConnectionState}; | ||||
|  | ||||
| // Re-export for convenience | ||||
| pub mod prelude { | ||||
|     pub use crate::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, ConnectionState, SigSocketError, Result}; | ||||
|     pub use crate::{ | ||||
|         SigSocketClient, SignRequest, SignResponse, ManagedSignRequest, RequestStatus, | ||||
|         SignRequestHandler, ConnectionState, SigSocketError, Result | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -82,6 +82,92 @@ impl SignResponse { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Enhanced sign request with additional metadata for request management | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct ManagedSignRequest { | ||||
|     /// The original sign request | ||||
|     #[serde(flatten)] | ||||
|     pub request: SignRequest, | ||||
|     /// Timestamp when the request was received (Unix timestamp in milliseconds) | ||||
|     pub timestamp: u64, | ||||
|     /// Target public key for this request (hex-encoded) | ||||
|     pub target_public_key: String, | ||||
|     /// Current status of the request | ||||
|     pub status: RequestStatus, | ||||
| } | ||||
|  | ||||
| /// Status of a sign request | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum RequestStatus { | ||||
|     /// Request is pending user approval | ||||
|     Pending, | ||||
|     /// Request has been approved and signed | ||||
|     Approved, | ||||
|     /// Request has been rejected by user | ||||
|     Rejected, | ||||
|     /// Request has expired or been cancelled | ||||
|     Cancelled, | ||||
| } | ||||
|  | ||||
| impl ManagedSignRequest { | ||||
|     /// Create a new managed sign request | ||||
|     pub fn new(request: SignRequest, target_public_key: String) -> Self { | ||||
|         Self { | ||||
|             request, | ||||
|             timestamp: current_timestamp_ms(), | ||||
|             target_public_key, | ||||
|             status: RequestStatus::Pending, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get the request ID | ||||
|     pub fn id(&self) -> &str { | ||||
|         &self.request.id | ||||
|     } | ||||
|  | ||||
|     /// Get the message as bytes (decoded from base64) | ||||
|     pub fn message_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> { | ||||
|         self.request.message_bytes() | ||||
|     } | ||||
|  | ||||
|     /// Check if this request is for the given public key | ||||
|     pub fn is_for_public_key(&self, public_key: &str) -> bool { | ||||
|         self.target_public_key == public_key | ||||
|     } | ||||
|  | ||||
|     /// Mark the request as approved | ||||
|     pub fn mark_approved(&mut self) { | ||||
|         self.status = RequestStatus::Approved; | ||||
|     } | ||||
|  | ||||
|     /// Mark the request as rejected | ||||
|     pub fn mark_rejected(&mut self) { | ||||
|         self.status = RequestStatus::Rejected; | ||||
|     } | ||||
|  | ||||
|     /// Check if the request is still pending | ||||
|     pub fn is_pending(&self) -> bool { | ||||
|         matches!(self.status, RequestStatus::Pending) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Get current timestamp in milliseconds | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| fn current_timestamp_ms() -> u64 { | ||||
|     std::time::SystemTime::now() | ||||
|         .duration_since(std::time::UNIX_EPOCH) | ||||
|         .unwrap_or_default() | ||||
|         .as_millis() as u64 | ||||
| } | ||||
|  | ||||
| /// Get current timestamp in milliseconds (WASM version) | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| fn current_timestamp_ms() -> u64 { | ||||
|     // In WASM, we'll use a simple counter or Date.now() via JS | ||||
|     // For now, return 0 - this can be improved later | ||||
|     0 | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
| @@ -138,4 +224,33 @@ mod tests { | ||||
|         let deserialized: SignResponse = serde_json::from_str(&json).unwrap(); | ||||
|         assert_eq!(response, deserialized); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_managed_sign_request() { | ||||
|         let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); | ||||
|         let managed = ManagedSignRequest::new(request.clone(), "test-public-key".to_string()); | ||||
|  | ||||
|         assert_eq!(managed.id(), "test-id"); | ||||
|         assert_eq!(managed.request, request); | ||||
|         assert_eq!(managed.target_public_key, "test-public-key"); | ||||
|         assert!(managed.is_pending()); | ||||
|         assert!(managed.is_for_public_key("test-public-key")); | ||||
|         assert!(!managed.is_for_public_key("other-key")); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_managed_request_status_changes() { | ||||
|         let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); | ||||
|         let mut managed = ManagedSignRequest::new(request, "test-public-key".to_string()); | ||||
|  | ||||
|         assert!(managed.is_pending()); | ||||
|  | ||||
|         managed.mark_approved(); | ||||
|         assert_eq!(managed.status, RequestStatus::Approved); | ||||
|         assert!(!managed.is_pending()); | ||||
|  | ||||
|         managed.mark_rejected(); | ||||
|         assert_eq!(managed.status, RequestStatus::Rejected); | ||||
|         assert!(!managed.is_pending()); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										92
									
								
								sigsocket_client/tests/request_management_test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								sigsocket_client/tests/request_management_test.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| //! Tests for the enhanced request management functionality | ||||
|  | ||||
| use sigsocket_client::prelude::*; | ||||
|  | ||||
| #[test] | ||||
| fn test_client_request_management() { | ||||
|     let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap(); | ||||
|     let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap(); | ||||
|      | ||||
|     // Initially no requests | ||||
|     assert_eq!(client.pending_request_count(), 0); | ||||
|     assert!(client.get_pending_requests().is_empty()); | ||||
|      | ||||
|     // Add a request | ||||
|     let request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl"); | ||||
|     let public_key_hex = "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"; | ||||
|     client.add_pending_request(request.clone(), public_key_hex.to_string()); | ||||
|      | ||||
|     // Check request was added | ||||
|     assert_eq!(client.pending_request_count(), 1); | ||||
|     assert!(client.get_pending_request("test-1").is_some()); | ||||
|      | ||||
|     // Check filtering by public key | ||||
|     let filtered = client.get_requests_for_public_key(public_key_hex); | ||||
|     assert_eq!(filtered.len(), 1); | ||||
|     assert_eq!(filtered[0].id(), "test-1"); | ||||
|      | ||||
|     // Add another request for different public key | ||||
|     let request2 = SignRequest::new("test-2", "dGVzdCBtZXNzYWdlMg=="); | ||||
|     let other_public_key = "03f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"; | ||||
|     client.add_pending_request(request2, other_public_key.to_string()); | ||||
|      | ||||
|     // Check total count | ||||
|     assert_eq!(client.pending_request_count(), 2); | ||||
|      | ||||
|     // Check filtering still works | ||||
|     let filtered = client.get_requests_for_public_key(public_key_hex); | ||||
|     assert_eq!(filtered.len(), 1); | ||||
|      | ||||
|     let filtered_other = client.get_requests_for_public_key(other_public_key); | ||||
|     assert_eq!(filtered_other.len(), 1); | ||||
|      | ||||
|     // Remove a request | ||||
|     let removed = client.remove_pending_request("test-1"); | ||||
|     assert!(removed.is_some()); | ||||
|     assert_eq!(removed.unwrap().id(), "test-1"); | ||||
|     assert_eq!(client.pending_request_count(), 1); | ||||
|      | ||||
|     // Clear all requests | ||||
|     client.clear_pending_requests(); | ||||
|     assert_eq!(client.pending_request_count(), 0); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_client_request_validation() { | ||||
|     let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap(); | ||||
|     let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap(); | ||||
|      | ||||
|     // Valid request | ||||
|     let valid_request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl"); | ||||
|     assert!(client.can_handle_request_for_key(&valid_request, "some-public-key")); | ||||
|      | ||||
|     // Invalid request - empty ID | ||||
|     let invalid_request = SignRequest::new("", "dGVzdCBtZXNzYWdl"); | ||||
|     assert!(!client.can_handle_request_for_key(&invalid_request, "some-public-key")); | ||||
|      | ||||
|     // Invalid request - empty message | ||||
|     let invalid_request2 = SignRequest::new("test-1", ""); | ||||
|     assert!(!client.can_handle_request_for_key(&invalid_request2, "some-public-key")); | ||||
|      | ||||
|     // Invalid request - invalid base64 | ||||
|     let invalid_request3 = SignRequest::new("test-1", "invalid-base64!"); | ||||
|     assert!(!client.can_handle_request_for_key(&invalid_request3, "some-public-key")); | ||||
|      | ||||
|     // Invalid public key | ||||
|     assert!(!client.can_handle_request_for_key(&valid_request, "")); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_client_connection_state() { | ||||
|     let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap(); | ||||
|     let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap(); | ||||
|      | ||||
|     // Initially disconnected | ||||
|     assert_eq!(client.state(), ConnectionState::Disconnected); | ||||
|     assert!(!client.is_connected()); | ||||
|     assert!(client.connected_public_key().is_none()); | ||||
|      | ||||
|     // Public key should be available | ||||
|     assert_eq!(client.public_key_hex(), "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"); | ||||
|     assert_eq!(client.url(), "ws://localhost:8080/ws"); | ||||
| } | ||||
| @@ -24,6 +24,7 @@ pub use vault::session_singleton::SESSION_MANAGER; | ||||
|  | ||||
| // Include the keypair bindings module | ||||
| mod vault_bindings; | ||||
| mod sigsocket_bindings; | ||||
| pub use vault_bindings::*; | ||||
|  | ||||
| // Include the sigsocket module | ||||
|   | ||||
							
								
								
									
										427
									
								
								wasm_app/src/sigsocket_bindings.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										427
									
								
								wasm_app/src/sigsocket_bindings.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,427 @@ | ||||
| //! 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 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<Option<SigSocketClient>> = 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<Vec<u8>> { | ||||
|         // Create event object for JavaScript | ||||
|         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()))?; | ||||
|  | ||||
|         // Store the request in our pending requests (this will be done by the client) | ||||
|         // and notify the extension | ||||
|         match self.callback.call1(&wasm_bindgen::JsValue::NULL, &event) { | ||||
|             Ok(_) => { | ||||
|                 console_log!("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); | ||||
|                 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 a specific workspace and event callback | ||||
|     /// | ||||
|     /// This establishes a real WebSocket connection using the workspace's default public key | ||||
|     /// and integrates with the vault system for security validation. | ||||
|     /// | ||||
|     /// # 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<String, JsValue> { | ||||
|         // 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. 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)))?; | ||||
|  | ||||
|         // Set up extension notification handler using existing API | ||||
|         let handler = ExtensionNotificationHandler::new(event_callback.clone()); | ||||
|         client.set_sign_handler(handler); | ||||
|  | ||||
|         // 4. Connect to the WebSocket server | ||||
|         client.connect().await | ||||
|             .map_err(|e| JsValue::from_str(&format!("Connection failed: {:?}", e)))?; | ||||
|  | ||||
|         console_log!("SigSocket connected successfully to {}", server_url); | ||||
|  | ||||
|         // 5. 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<String, JsValue> { | ||||
|         // 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() { | ||||
|                 // client.disconnect().await?; // Will be async in real implementation | ||||
|                 console_log!("SigSocket client disconnected"); | ||||
|             } | ||||
|             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<bool, JsValue> { | ||||
|         // 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<String, JsValue> { | ||||
|         // 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<u8>, SignRequest), JsValue>((message_bytes, request.request.clone())) | ||||
|         })?; | ||||
|          | ||||
|         // 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_base64 = signature_obj["signature"].as_str() | ||||
|             .ok_or_else(|| JsValue::from_str("Invalid signature format"))?; | ||||
|          | ||||
|         // 4. Send response to server and remove request | ||||
|         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()) | ||||
|         }) | ||||
|     } | ||||
|      | ||||
|     /// 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> { | ||||
|         SIGSOCKET_CLIENT.with(|c| { | ||||
|             let mut client = c.borrow_mut(); | ||||
|             let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected"))?; | ||||
|  | ||||
|             // 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(()) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /// 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<String, JsValue> { | ||||
|         // 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<String, JsValue> { | ||||
|         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(()) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user