feat: implement browser extension UI with WebAssembly integration
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -7,4 +7,5 @@ | ||||
| .vscode/ | ||||
|  | ||||
| # Ignore test databases | ||||
| /vault/vault_native_test/ | ||||
| /vault/vault_native_test/ | ||||
| node_modules/ | ||||
| @@ -4,6 +4,6 @@ members = [ | ||||
|     "kvstore", | ||||
|     "vault", | ||||
|     "evm_client", | ||||
|     "wasm", | ||||
|     "wasm_app", | ||||
| ] | ||||
|  | ||||
|   | ||||
							
								
								
									
										12
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| BROWSER ?= firefox | ||||
|  | ||||
| .PHONY: test-browser-all test-browser-kvstore test-browser-vault test-browser-evm-client | ||||
| .PHONY: test-browser-all test-browser-kvstore test-browser-vault test-browser-evm-client build-wasm-app | ||||
|  | ||||
| test-browser-all: test-browser-kvstore test-browser-vault test-browser-evm-client | ||||
|  | ||||
| @@ -21,3 +21,13 @@ test-browser-vault: | ||||
| test-browser-evm-client: | ||||
| 	cd evm_client && wasm-pack test --headless --$(BROWSER) | ||||
|  | ||||
| # Build wasm_app as a WASM library | ||||
| build-wasm-app: | ||||
| 	cd wasm_app && wasm-pack build --target web | ||||
|  | ||||
| # Build everything: wasm, copy, then extension | ||||
| build-extension-all: build-wasm-app | ||||
| 	cp wasm_app/pkg/wasm_app.js extension/public/wasm/wasm_app.js | ||||
| 	cp wasm_app/pkg/wasm_app_bg.wasm extension/public/wasm/wasm_app_bg.wasm | ||||
| 	cd extension && npm run build | ||||
|  | ||||
|   | ||||
| @@ -29,8 +29,67 @@ The browser extension is the main user interface for interacting with the modula | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Script Permissions & Security | ||||
| - **Session Password Handling**: The extension stores the keyspace password (or a derived key) securely in memory only for the duration of an unlocked session. The password is never persisted or written to disk/storage, and is zeroized from memory immediately upon session lock/logout, following cryptographic best practices (see also Developer Notes below). | ||||
| ## Security Considerations | ||||
|  | ||||
| ### Restricting WASM and Session API Access to the Extension | ||||
|  | ||||
| To ensure that sensitive APIs (such as session state, cryptographic operations, and key management) are accessible **only** from the browser extension and not from arbitrary web pages, follow these best practices: | ||||
|  | ||||
| 1. **Export Only Safe, High-Level APIs** | ||||
|    - Use `#[wasm_bindgen]` only on functions you explicitly want to expose to the extension. | ||||
|    - Do **not** export internal helpers, state singletons, or low-level APIs. | ||||
|  | ||||
|    ```rust | ||||
|    // Safe to export | ||||
|    #[wasm_bindgen] | ||||
|    pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> { | ||||
|        // ... | ||||
|    } | ||||
|  | ||||
|    // NOT exported: internal state | ||||
|    // pub static SESSION_MANAGER: ... | ||||
|    ``` | ||||
|  | ||||
| 2. **Do Not Attach WASM Exports to `window` or `globalThis`** | ||||
|    - When loading the WASM module in your extension, do not attach its exports to any global object accessible by web pages. | ||||
|    - Keep all WASM interaction within the extension’s background/content scripts. | ||||
|  | ||||
| 3. **Validate All Inputs** | ||||
|    - Even though only your extension should call WASM APIs, always validate inputs to exported functions to prevent injection or misuse. | ||||
|  | ||||
| 4. **Use Message Passing Carefully** | ||||
|    - If you use `postMessage` or similar mechanisms, always check the message origin and type before processing. | ||||
|    - Only process messages from trusted origins (e.g., your extension’s own scripts). | ||||
|  | ||||
| 5. **Load WASM in Extension-Only Context** | ||||
|    - Load and instantiate the WASM module in a context (such as a background script or content script) that is not accessible to arbitrary websites. | ||||
|    - Never inject your WASM module directly into web page scopes. | ||||
|  | ||||
| #### Example: Secure WASM Export | ||||
|  | ||||
| ```rust | ||||
| // Only export high-level, safe APIs | ||||
| #[wasm_bindgen] | ||||
| pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> { | ||||
|     // ... | ||||
| } | ||||
| // Do NOT export SESSION_MANAGER or internal helpers | ||||
| ``` | ||||
|  | ||||
| #### Example: Secure JS Loading (Extension Only) | ||||
|  | ||||
| ```js | ||||
| // In your extension's background or content script: | ||||
| import init, { run_rhai } from "./your_wasm_module.js"; | ||||
|  | ||||
| // Only your extension's JS can call run_rhai | ||||
| // Do NOT attach run_rhai to window/globalThis | ||||
| ``` | ||||
|  | ||||
| By following these guidelines, your WASM session state and sensitive APIs will only be accessible to your browser extension, not to untrusted web pages. | ||||
|  | ||||
| ### Session Password Handling | ||||
| - The extension stores the keyspace password (or a derived key) securely in memory only for the duration of an unlocked session. The password is never persisted or written to disk/storage, and is zeroized from memory immediately upon session lock/logout, following cryptographic best practices (see also Developer Notes below). | ||||
| - **Signer Access**: Scripts can access the session's signer only after explicit user approval per execution. | ||||
| - **Approval Model**: Every script execution (local or remote) requires user approval. | ||||
| - **No global permissions**: Permissions are not granted globally or permanently. | ||||
|   | ||||
| @@ -54,7 +54,7 @@ async fn main() { | ||||
| use kvstore::{KVStore, WasmStore}; | ||||
|  | ||||
| // Must be called from an async context (e.g., JS Promise) | ||||
| let store = WasmStore::open("mydb").await.unwrap(); | ||||
| let store = WasmStore::open("vault").await.unwrap(); | ||||
| store.set("foo", b"bar").await.unwrap(); | ||||
| let val = store.get("foo").await.unwrap(); | ||||
| // Use the value as needed | ||||
|   | ||||
| @@ -39,7 +39,7 @@ sal/ | ||||
|  | ||||
| - **Each core component (`kvstore`, `vault`, `evm_client`, `rhai`) is a separate crate at the repo root.** | ||||
| - **CLI binary** is in `cli_app` and depends on the core crates. | ||||
| - **WebAssembly target** is in `web_app`. | ||||
| - **WebAssembly target** is in `wasm_app`. | ||||
| - **Rhai bindings** live in their own crate (`rhai/`), so both CLI and WASM can depend on them. | ||||
|  | ||||
| --- | ||||
|   | ||||
| @@ -7,20 +7,16 @@ edition = "2021" | ||||
| path = "src/lib.rs" | ||||
|  | ||||
| [dependencies] | ||||
| kvstore = { path = "../kvstore" } | ||||
| # Only universal/core dependencies here | ||||
|  | ||||
| tokio = { version = "1.37", features = ["rt", "macros"] } | ||||
| rhai = "1.16" | ||||
| ethers-core = "2.0" | ||||
| gloo-net = { version = "0.5", features = ["http"] } | ||||
| rlp = "0.5" | ||||
| reqwest = { version = "0.11", features = ["json"] } | ||||
| async-trait = "0.1" | ||||
| serde = { version = "1", features = ["derive"] } | ||||
| serde_json = "1" | ||||
| vault = { path = "../vault" } | ||||
| thiserror = "1" | ||||
| alloy-rlp = { version = "0.3.11", features = ["derive"] } | ||||
| alloy-primitives = "1.1.0" | ||||
| log = "0.4" | ||||
| hex = "0.4" | ||||
| k256 = { version = "0.13", features = ["ecdsa"] } | ||||
| @@ -32,10 +28,17 @@ web-sys = { version = "0.3", features = ["console"] } | ||||
|  | ||||
| [target.'cfg(target_arch = "wasm32")'.dependencies] | ||||
| getrandom = { version = "0.3", features = ["wasm_js"] } | ||||
| getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] } | ||||
| getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] } | ||||
| wasm-bindgen = "0.2" | ||||
| js-sys = "0.3" | ||||
| console_error_panic_hook = "0.1" | ||||
| # console_error_panic_hook = "0.1" | ||||
| gloo-net = { version = "0.5", features = ["http"] } | ||||
| console_log = "1" | ||||
|  | ||||
| [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] | ||||
| tokio = { version = "1", features = ["macros", "rt-multi-thread"] } | ||||
| log = "0.4" | ||||
|  | ||||
| [target.'cfg(not(target_arch = "wasm32"))'.dependencies] | ||||
| env_logger = "0.11" | ||||
| reqwest = { version = "0.11", features = ["json"] } | ||||
|   | ||||
							
								
								
									
										9
									
								
								evm_client/src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| #[derive(Debug, thiserror::Error)] | ||||
| pub enum EvmError { | ||||
|     #[error("RPC error: {0}")] | ||||
|     Rpc(String), | ||||
|     #[error("Signing error: {0}")] | ||||
|     Signing(String), | ||||
|     #[error("Other error: {0}")] | ||||
|     Other(String), | ||||
| } | ||||
| @@ -14,22 +14,151 @@ | ||||
|  | ||||
| pub use ethers_core::types::*; | ||||
| pub mod provider; | ||||
| pub mod signer; | ||||
| pub mod rhai_bindings; | ||||
| pub mod rhai_sync_helpers; | ||||
| pub mod error; | ||||
| pub use provider::send_rpc; | ||||
| pub use error::EvmError; | ||||
|  | ||||
| /// Public EVM client struct for use in bindings and sync helpers | ||||
| pub struct Provider { | ||||
|     pub rpc_url: String, | ||||
|     pub chain_id: u64, | ||||
|     pub explorer_url: Option<String>, | ||||
| } | ||||
|  | ||||
| pub struct EvmClient { | ||||
|     // Add fields as needed for your implementation | ||||
|     pub provider: Provider, | ||||
| } | ||||
|  | ||||
| impl EvmClient { | ||||
|     pub async fn get_balance(&self, provider_url: &str, public_key: &[u8]) -> Result<u64, String> { | ||||
|         // TODO: Implement actual logic | ||||
|         Ok(0) | ||||
|     pub fn new(provider: Provider) -> Self { | ||||
|         Self { provider } | ||||
|     } | ||||
|     pub async fn send_transaction(&self, provider_url: &str, key_id: &str, password: &[u8], tx_data: rhai::Map) -> Result<String, String> { | ||||
|         // TODO: Implement actual logic | ||||
|         Ok("tx_hash_placeholder".to_string()) | ||||
|  | ||||
|     /// Initialize logging for the current target (native: env_logger, WASM: console_log) | ||||
|     /// Call this before using any log macros. | ||||
|     pub fn init_logging() { | ||||
|         use std::sync::Once; | ||||
|         static INIT: Once = Once::new(); | ||||
|         INIT.call_once(|| { | ||||
|             #[cfg(not(target_arch = "wasm32"))] | ||||
|             { | ||||
|                 use env_logger; | ||||
|                 let _ = env_logger::builder().is_test(false).try_init(); | ||||
|             } | ||||
|             #[cfg(target_arch = "wasm32")] | ||||
|             { | ||||
|                 use console_log; | ||||
|                 let _ = console_log::init_with_level(log::Level::Debug); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     pub async fn get_balance(&self, address: Address) -> Result<U256, EvmError> { | ||||
|         // TODO: Use provider info | ||||
|         provider::get_balance(&self.provider.rpc_url, address) | ||||
|             .await | ||||
|             .map_err(|e| EvmError::Rpc(e.to_string())) | ||||
|     } | ||||
|  | ||||
|     pub async fn send_transaction( | ||||
|         &self, | ||||
|         mut tx: provider::Transaction, | ||||
|         signer: &dyn crate::signer::Signer, | ||||
|     ) -> Result<ethers_core::types::H256, EvmError> { | ||||
|         use ethers_core::types::{U256, H256, Bytes, Address}; | ||||
|         use std::str::FromStr; | ||||
|         use serde_json::json; | ||||
|         use crate::provider::{send_rpc, parse_signature_rs_v}; | ||||
|  | ||||
|         // 1. Fill in missing fields via JSON-RPC if needed | ||||
|         // Parse signer address as H160 | ||||
|         let signer_addr = ethers_core::types::Address::from_str(&signer.address()) | ||||
|             .map_err(|e| EvmError::Rpc(format!("Invalid signer address: {}", e)))?; | ||||
|         // Nonce | ||||
|         if tx.nonce.is_none() { | ||||
|             let body = json!({ | ||||
|                 "jsonrpc": "2.0", | ||||
|                 "method": "eth_getTransactionCount", | ||||
|                 "params": [format!("0x{:x}", signer_addr), "pending"], | ||||
|                 "id": 1 | ||||
|             }).to_string(); | ||||
|             let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|             let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|             let hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_getTransactionCount".to_string()))?; | ||||
|             tx.nonce = Some(U256::from_str_radix(hex.trim_start_matches("0x"), 16).map_err(|e| EvmError::Rpc(e.to_string()))?); | ||||
|         } | ||||
|         // Gas Price | ||||
|         if tx.gas_price.is_none() { | ||||
|             let body = json!({ | ||||
|                 "jsonrpc": "2.0", | ||||
|                 "method": "eth_gasPrice", | ||||
|                 "params": [], | ||||
|                 "id": 1 | ||||
|             }).to_string(); | ||||
|             let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|             let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|             let hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_gasPrice".to_string()))?; | ||||
|             tx.gas_price = Some(U256::from_str_radix(hex.trim_start_matches("0x"), 16).map_err(|e| EvmError::Rpc(e.to_string()))?); | ||||
|         } | ||||
|         // Chain ID | ||||
|         if tx.chain_id.is_none() { | ||||
|             tx.chain_id = Some(self.provider.chain_id); | ||||
|         } | ||||
|         // Gas (optional: estimate if missing) | ||||
|         if tx.gas.is_none() { | ||||
|             let body = json!({ | ||||
|                 "jsonrpc": "2.0", | ||||
|                 "method": "eth_estimateGas", | ||||
|                 "params": [{ | ||||
|                     "to": format!("0x{:x}", tx.to), | ||||
|                     "from": format!("0x{:x}", signer_addr), | ||||
|                     "value": format!("0x{:x}", tx.value), | ||||
|                     "data": format!("0x{}", hex::encode(&tx.data)), | ||||
|                 }], | ||||
|                 "id": 1 | ||||
|             }).to_string(); | ||||
|             let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|             let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|             let hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_estimateGas".to_string()))?; | ||||
|             tx.gas = Some(U256::from_str_radix(hex.trim_start_matches("0x"), 16).map_err(|e| EvmError::Rpc(e.to_string()))?); | ||||
|         } | ||||
|  | ||||
|         // 2. RLP encode unsigned transaction | ||||
|         let rlp_unsigned = tx.rlp_encode_unsigned(); | ||||
|  | ||||
|         // 3. Sign the RLP-encoded unsigned transaction | ||||
|         let sig = signer.sign(&rlp_unsigned).await?; | ||||
|         let (r, s, v) = parse_signature_rs_v(&sig, tx.chain_id.unwrap()).ok_or_else(|| EvmError::Signing("Invalid signature format".to_string()))?; | ||||
|  | ||||
|         // 4. RLP encode signed transaction (EIP-155) | ||||
|         use rlp::RlpStream; | ||||
|         let mut rlp_stream = RlpStream::new_list(9); | ||||
|         rlp_stream.append(&tx.nonce.unwrap()); | ||||
|         rlp_stream.append(&tx.gas_price.unwrap()); | ||||
|         rlp_stream.append(&tx.gas.unwrap()); | ||||
|         rlp_stream.append(&tx.to); | ||||
|         rlp_stream.append(&tx.value); | ||||
|         rlp_stream.append(&tx.data.to_vec()); | ||||
|         rlp_stream.append(&tx.chain_id.unwrap()); | ||||
|         rlp_stream.append(&r); | ||||
|         rlp_stream.append(&s); | ||||
|         let raw_tx = rlp_stream.out().to_vec(); | ||||
|  | ||||
|         // 5. Broadcast the raw transaction | ||||
|         let raw_hex = format!("0x{}", hex::encode(&raw_tx)); | ||||
|         let body = json!({ | ||||
|             "jsonrpc": "2.0", | ||||
|             "method": "eth_sendRawTransaction", | ||||
|             "params": [raw_hex], | ||||
|             "id": 1 | ||||
|         }).to_string(); | ||||
|         let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|         let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?; | ||||
|         let tx_hash_hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_sendRawTransaction".to_string()))?; | ||||
|         let tx_hash = H256::from_slice(&hex::decode(tx_hash_hex.trim_start_matches("0x")).map_err(|e| EvmError::Rpc(e.to_string()))?); | ||||
|         Ok(tx_hash) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -30,13 +30,13 @@ pub async fn send_rpc(url: &str, body: &str) -> Result<String, Box<dyn Error>> { | ||||
|     } | ||||
| } | ||||
| pub struct Transaction { | ||||
|     pub nonce: U256, | ||||
|     pub to: Address, | ||||
|     pub value: U256, | ||||
|     pub gas: U256, | ||||
|     pub gas_price: U256, | ||||
|     pub data: Bytes, | ||||
|     pub chain_id: u64, | ||||
|     pub gas: Option<U256>, | ||||
|     pub gas_price: Option<U256>, | ||||
|     pub nonce: Option<U256>, | ||||
|     pub chain_id: Option<u64>, | ||||
| } | ||||
|  | ||||
| impl Transaction { | ||||
| @@ -94,5 +94,4 @@ pub async fn get_balance(url: &str, address: Address) -> Result<U256, Box<dyn st | ||||
|     Ok(balance) | ||||
| } | ||||
|  | ||||
| // (Remove old sign_and_serialize placeholder) | ||||
|  | ||||
|   | ||||
| @@ -14,20 +14,16 @@ pub fn register_rhai_api(engine: &mut Engine, evm_client: std::sync::Arc<EvmClie | ||||
|     } | ||||
|     impl RhaiEvmClient { | ||||
|         /// Get balance using the EVM client. | ||||
|         pub fn get_balance(&self, provider_url: String, public_key: rhai::Blob) -> Result<String, String> { | ||||
|             // Use the sync helper from crate::rhai_sync_helpers | ||||
|             crate::rhai_sync_helpers::get_balance_sync(&self.inner, &provider_url, &public_key) | ||||
|         pub fn get_balance(&self, address_hex: String) -> Result<String, String> { | ||||
|             use ethers_core::types::Address; | ||||
|             let address = Address::from_slice(&hex::decode(address_hex.trim_start_matches("0x")).map_err(|e| format!("hex decode error: {e}"))?); | ||||
|             crate::rhai_sync_helpers::get_balance_sync(&self.inner, address) | ||||
|         } | ||||
|  | ||||
|         /// Send transaction using the EVM client. | ||||
|         pub fn send_transaction(&self, provider_url: String, key_id: String, password: rhai::Blob, tx_data: rhai::Map) -> Result<String, String> { | ||||
|             // Use the sync helper from crate::rhai_sync_helpers | ||||
|             crate::rhai_sync_helpers::send_transaction_sync(&self.inner, &provider_url, &key_id, &password, tx_data) | ||||
|         } | ||||
|  | ||||
|     } | ||||
|     engine.register_type::<RhaiEvmClient>(); | ||||
|     engine.register_fn("get_balance", RhaiEvmClient::get_balance); | ||||
|     engine.register_fn("send_transaction", RhaiEvmClient::send_transaction); | ||||
|     // Register instance for scripts | ||||
|     let rhai_ec = RhaiEvmClient { inner: evm_client.clone() }; | ||||
|     // Rhai does not support register_global_constant; pass the client as a parameter or use module scope. | ||||
|   | ||||
| @@ -11,11 +11,10 @@ use tokio::runtime::Handle; | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| pub fn get_balance_sync( | ||||
|     evm_client: &EvmClient, | ||||
|     provider_url: &str, | ||||
|     public_key: &[u8], | ||||
|     address: ethers_core::types::Address, | ||||
| ) -> Result<String, String> { | ||||
|     Handle::current().block_on(async { | ||||
|         evm_client.get_balance(provider_url, public_key) | ||||
|         evm_client.get_balance(address) | ||||
|             .await | ||||
|             .map(|b| b.to_string()) | ||||
|             .map_err(|e| format!("get_balance error: {e}")) | ||||
| @@ -26,15 +25,13 @@ pub fn get_balance_sync( | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| pub fn send_transaction_sync( | ||||
|     evm_client: &EvmClient, | ||||
|     provider_url: &str, | ||||
|     key_id: &str, | ||||
|     password: &[u8], | ||||
|     tx_data: Map, | ||||
|     tx: crate::provider::Transaction, | ||||
|     signer: &dyn crate::signer::Signer, | ||||
| ) -> Result<String, String> { | ||||
|     Handle::current().block_on(async { | ||||
|         evm_client.send_transaction(provider_url, key_id, password, tx_data) | ||||
|         evm_client.send_transaction(tx, signer) | ||||
|             .await | ||||
|             .map(|tx| tx.to_string()) | ||||
|             .map(|tx| format!("0x{:x}", tx)) | ||||
|             .map_err(|e| format!("send_transaction error: {e}")) | ||||
|     }) | ||||
| } | ||||
|   | ||||
| @@ -1,80 +1,8 @@ | ||||
| // Signing should be done using ethers-core utilities directly. This file is now empty. | ||||
| use super::error::EvmError; | ||||
|  | ||||
| // Native: Only compile for non-WASM | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| #[async_trait] | ||||
| #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] | ||||
| #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] | ||||
| pub trait Signer: Send + Sync { | ||||
|     async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>; | ||||
|     fn address(&self) -> String; | ||||
| } | ||||
|  | ||||
| // WASM: Only compile for WASM | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| #[async_trait(?Send)] | ||||
| pub trait Signer { | ||||
|     async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>; | ||||
|     fn address(&self) -> String; | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| // --- Implementation for vault::SessionManager --- | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| #[async_trait::async_trait(?Send)] | ||||
| impl<S: vault::KVStore> Signer for vault::SessionManager<S> { | ||||
|     async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError> { | ||||
|         log::debug!("SessionManager::sign called"); | ||||
|         self.sign(message) | ||||
|             .await | ||||
|             .map_err(|e| { | ||||
|                 log::error!("Vault signing error: {}", e); | ||||
|                 EvmError::Vault(e.to_string()) | ||||
|             }) | ||||
|     } | ||||
|     fn address(&self) -> String { | ||||
|         log::debug!("SessionManager::address called"); | ||||
|         self.current_keypair() | ||||
|             .map(|k| { | ||||
|                 if k.key_type == vault::KeyType::Secp256k1 { | ||||
|                     let pubkey = &k.public_key; | ||||
|                     use alloy_primitives::keccak256; | ||||
|                     let hash = keccak256(&pubkey[1..]); | ||||
|                     format!("0x{}", hex::encode(&hash[12..])) | ||||
|                 } else { | ||||
|                     format!("0x{}", hex::encode(&k.public_key)) | ||||
|                 } | ||||
|             }) | ||||
|             .unwrap_or_default() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| #[async_trait::async_trait] | ||||
|  | ||||
| impl<S: vault::KVStore + Send + Sync> Signer for vault::SessionManager<S> { | ||||
|     async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError> { | ||||
|         log::debug!("SessionManager::sign called"); | ||||
|         self.sign(message) | ||||
|             .await | ||||
|             .map_err(|e| { | ||||
|                 log::error!("Vault signing error: {}", e); | ||||
|                 EvmError::Vault(e.to_string()) | ||||
|             }) | ||||
|     } | ||||
|     fn address(&self) -> String { | ||||
|         log::debug!("SessionManager::address called"); | ||||
|         self.current_keypair() | ||||
|             .map(|k| { | ||||
|                 if k.key_type == vault::KeyType::Secp256k1 { | ||||
|                     let pubkey = &k.public_key; | ||||
|                     use alloy_primitives::keccak256; | ||||
|                     let hash = keccak256(&pubkey[1..]); | ||||
|                     format!("0x{}", hex::encode(&hash[12..])) | ||||
|                 } else { | ||||
|                     format!("0x{}", hex::encode(&k.public_key)) | ||||
|                 } | ||||
|             }) | ||||
|             .unwrap_or_default() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,63 +1,21 @@ | ||||
| // This file contains native-only integration tests for EVM client balance and signing logic. | ||||
| // All code is strictly separated from WASM code using cfg attributes. | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| mod native_tests { | ||||
|     use vault::{SessionManager, KeyType}; | ||||
|     use tempfile::TempDir; | ||||
|     use kvstore::native::NativeStore; | ||||
|     use alloy_primitives::keccak256; | ||||
|      | ||||
|  | ||||
|  | ||||
|     #[tokio::test] | ||||
|     async fn test_vault_sessionmanager_balance_for_new_keypair() { | ||||
|         use ethers_core::types::{Address, U256}; | ||||
|         use evm_client::provider::get_balance; | ||||
|  | ||||
|         let tmp_dir = TempDir::new().expect("create temp dir"); | ||||
|         let store = NativeStore::open(tmp_dir.path().to_str().unwrap()).expect("Failed to open native store"); | ||||
|         let mut vault = vault::Vault::new(store.clone()); | ||||
|         let keyspace = "testspace"; | ||||
|         let password = b"testpass"; | ||||
|         // 1. Create keyspace | ||||
|         vault.create_keyspace(keyspace, password, None).await.expect("create keyspace"); | ||||
|         // 2. Add secp256k1 keypair | ||||
|         let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Secp256k1), None).await.expect("add keypair"); | ||||
|         // 3. Create SessionManager and unlock keyspace | ||||
|         let mut session = SessionManager::new(vault); | ||||
|         session.unlock_keyspace(keyspace, password).await.expect("unlock keyspace"); | ||||
|         session.select_keyspace(keyspace).expect("select keyspace"); | ||||
|         session.select_keypair(&key_id).expect("select keypair"); | ||||
|         let kp = session.current_keypair().expect("current keypair"); | ||||
|         // 4. Derive Ethereum address from public key (same as in evm_client) | ||||
|         let pubkey = &kp.public_key; | ||||
|         // Remove leading 0x04 if present (uncompressed SEC1) | ||||
|         let pubkey = if pubkey.len() == 65 && pubkey[0] == 0x04 { &pubkey[1..] } else { pubkey.as_slice() }; | ||||
|         let hash = keccak256(pubkey); | ||||
|         let address = Address::from_slice(&hash[12..]); | ||||
|         // 5. Query balance | ||||
|         let url = "https://ethereum.blockpi.network/v1/rpc/public"; | ||||
|         let balance = get_balance(url, address).await.expect("Failed to get balance"); | ||||
|         assert_eq!(balance, ethers_core::types::U256::zero(), "New keypair should have zero balance"); | ||||
|     } | ||||
| } | ||||
|  | ||||
|     use ethers_core::types::Bytes; | ||||
|  | ||||
| #[test] | ||||
|     fn test_rlp_encode_unsigned() { | ||||
|         use ethers_core::types::{Address, U256, Bytes}; | ||||
|         use evm_client::provider::Transaction; | ||||
|  | ||||
|     let tx = Transaction { | ||||
|         nonce: U256::from(1), | ||||
|         to: Address::zero(), | ||||
|         value: U256::from(100), | ||||
|         gas: U256::from(21000), | ||||
|         gas_price: U256::from(1), | ||||
|         data: Bytes::new(), | ||||
|         chain_id: 1u64, | ||||
|         gas: Some(U256::from(21000)), | ||||
|         gas_price: Some(U256::from(1)), | ||||
|         nonce: Some(U256::from(1)), | ||||
|         chain_id: Some(1u64), | ||||
|     }; | ||||
|     let rlp = tx.rlp_encode_unsigned(); | ||||
|     assert!(!rlp.is_empty()); | ||||
| @@ -86,6 +44,6 @@ mod native_tests { | ||||
|     let address = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; | ||||
|     let address = ethers_core::types::Address::from_slice(&hex::decode(address).unwrap()); | ||||
|     let url = "https://ethereum.blockpi.network/v1/rpc/public"; | ||||
|     let balance = get_balance(url, address).await.expect("Failed to get balance"); | ||||
|     let balance = get_balance(url, address).await.expect("Failed to get balance"); // TODO: Update to use new EvmClient API | ||||
|     assert!(balance > ethers_core::types::U256::zero(), "Vitalik's balance should be greater than zero"); | ||||
| } | ||||
|   | ||||
| @@ -11,13 +11,13 @@ use hex; | ||||
| #[wasm_bindgen_test] | ||||
| fn test_rlp_encode_unsigned() { | ||||
|     let tx = Transaction { | ||||
|         nonce: U256::from(1), | ||||
|         to: Address::zero(), | ||||
|         value: U256::from(100), | ||||
|         gas: U256::from(21000), | ||||
|         gas_price: U256::from(1), | ||||
|         data: Bytes::new(), | ||||
|         chain_id: 1, | ||||
|         gas: Some(U256::from(21000)), | ||||
|         gas_price: Some(U256::from(1)), | ||||
|         nonce: Some(U256::from(1)), | ||||
|         chain_id: Some(1), | ||||
|     }; | ||||
|     let rlp = tx.rlp_encode_unsigned(); | ||||
|     assert!(!rlp.is_empty()); | ||||
| @@ -31,44 +31,11 @@ pub async fn test_get_balance_real_address_wasm_unique() { | ||||
|     let address = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; | ||||
|     let address = Address::from_slice(&hex::decode(address).unwrap()); | ||||
|     let url = "https://ethereum.blockpi.network/v1/rpc/public"; | ||||
|     let balance = get_balance(url, address).await.expect("Failed to get balance"); | ||||
|     let balance = get_balance(url, address).await.expect("Failed to get balance"); // TODO: Update to use new EvmClient API | ||||
|     web_sys::console::log_1(&format!("Balance: {balance:?}").into()); | ||||
|     assert!(balance > U256::zero(), "Vitalik's balance should be greater than zero"); | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen_test(async)] | ||||
| pub async fn test_vault_sessionmanager_balance_for_new_keypair_wasm() { | ||||
|     use vault::{SessionManager, KeyType}; | ||||
|     use ethers_core::types::Address; | ||||
|     use evm_client::provider::get_balance; | ||||
|     use alloy_primitives::keccak256; | ||||
|     web_sys::console::log_1(&"WASM vault-session balance test running!".into()); | ||||
|     let store = kvstore::wasm::WasmStore::open("test-db").await.expect("open"); | ||||
|     let mut vault = vault::Vault::new(store); | ||||
|     let keyspace = "testspace-wasm"; | ||||
|     let password = b"testpass"; | ||||
|     // 1. Create keyspace | ||||
|     vault.create_keyspace(keyspace, password, None).await.expect("create keyspace"); | ||||
|     // 2. Add secp256k1 keypair | ||||
|     let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Secp256k1), None).await.expect("add keypair"); | ||||
|     // 3. Create SessionManager and unlock keyspace | ||||
|     let mut session = SessionManager::new(vault); | ||||
|     session.unlock_keyspace(keyspace, password).await.expect("unlock keyspace"); | ||||
|     session.select_keyspace(keyspace).expect("select keyspace"); | ||||
|     session.select_keypair(&key_id).expect("select keypair"); | ||||
|     let kp = session.current_keypair().expect("current keypair"); | ||||
|     // 4. Derive Ethereum address from public key | ||||
|     let pubkey = &kp.public_key; | ||||
|     let pubkey = if pubkey.len() == 65 && pubkey[0] == 0x04 { &pubkey[1..] } else { pubkey.as_slice() }; | ||||
|     let hash = keccak256(pubkey); | ||||
|     let address = Address::from_slice(&hash[12..]); | ||||
|     // 5. Query balance | ||||
|     let url = "https://ethereum.blockpi.network/v1/rpc/public"; | ||||
|     let balance = get_balance(url, address).await.expect("Failed to get balance"); | ||||
|     web_sys::console::log_1(&format!("Balance: {balance:?}").into()); | ||||
|     assert_eq!(balance, ethers_core::types::U256::zero(), "New keypair should have zero balance"); | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| fn test_parse_signature_rs_v() { | ||||
|     let mut sig = [0u8; 65]; | ||||
|   | ||||
							
								
								
									
										35
									
								
								extension/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | ||||
| # Modular Vault Browser Extension | ||||
|  | ||||
| A cross-browser (Manifest V3) extension for secure cryptographic operations and Rhai scripting, powered by Rust/WASM. | ||||
|  | ||||
| ## Features | ||||
| - Session/keypair management | ||||
| - Cryptographic signing, encryption, and EVM actions | ||||
| - Secure WASM integration (signing only accessible from extension scripts) | ||||
| - React-based popup UI with dark mode | ||||
| - Future: WebSocket integration for remote scripting | ||||
|  | ||||
| ## Structure | ||||
| - `manifest.json`: Extension manifest (MV3, Chrome/Firefox) | ||||
| - `popup/`: React UI for user interaction | ||||
| - `background/`: Service worker for session, keypair, and WASM logic | ||||
| - `assets/`: Icons and static assets | ||||
|  | ||||
| ## Dev Workflow | ||||
| 1. Build Rust WASM: `wasm-pack build --target web --out-dir ../extension/wasm` | ||||
| 2. Install JS deps: `npm install` (from `extension/`) | ||||
| 3. Build popup: `npm run build` | ||||
| 4. Load `/extension` as an unpacked extension in your browser | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## Security | ||||
| - WASM cryptographic APIs are only accessible from extension scripts (not content scripts or web pages). | ||||
| - All sensitive actions require explicit user approval. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## TODO | ||||
| - Implement background logic for session/keypair | ||||
| - Integrate popup UI with WASM APIs | ||||
| - Add WebSocket support (Phase 2) | ||||
							
								
								
									
										
											BIN
										
									
								
								extension/assets/icon-128.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								extension/assets/icon-16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 454 B | 
							
								
								
									
										
											BIN
										
									
								
								extension/assets/icon-32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 712 B | 
							
								
								
									
										
											BIN
										
									
								
								extension/assets/icon-48.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										81
									
								
								extension/background/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,81 @@ | ||||
| // Background service worker for Modular Vault Extension | ||||
| // Handles state persistence between popup sessions | ||||
|  | ||||
| console.log('Background service worker started'); | ||||
|  | ||||
| // Store session state locally for quicker access | ||||
| let sessionState = { | ||||
|   currentKeyspace: null, | ||||
|   keypairs: [], | ||||
|   selectedKeypair: null | ||||
| }; | ||||
|  | ||||
| // Initialize state from storage | ||||
| chrome.storage.local.get(['currentKeyspace', 'keypairs', 'selectedKeypair']) | ||||
|   .then(state => { | ||||
|     sessionState = { | ||||
|       currentKeyspace: state.currentKeyspace || null, | ||||
|       keypairs: state.keypairs || [], | ||||
|       selectedKeypair: state.selectedKeypair || null | ||||
|     }; | ||||
|     console.log('Session state loaded from storage:', sessionState); | ||||
|   }) | ||||
|   .catch(error => { | ||||
|     console.error('Failed to load session state:', error); | ||||
|   }); | ||||
|  | ||||
| // Handle messages from the popup | ||||
| chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | ||||
|   console.log('Background received message:', message.action, message.type || ''); | ||||
|    | ||||
|   // Update session state | ||||
|   if (message.action === 'update_session') { | ||||
|     try { | ||||
|       const { type, data } = message; | ||||
|        | ||||
|       // Update our local state | ||||
|       if (type === 'keyspace') { | ||||
|         sessionState.currentKeyspace = data; | ||||
|       } else if (type === 'keypair_selected') { | ||||
|         sessionState.selectedKeypair = data; | ||||
|       } else if (type === 'keypair_added') { | ||||
|         sessionState.keypairs = [...sessionState.keypairs, data]; | ||||
|       } else if (type === 'keypairs_loaded') { | ||||
|         // Replace the entire keypair list with what came from the vault | ||||
|         console.log('Updating keypairs from vault:', data); | ||||
|         sessionState.keypairs = data; | ||||
|       } else if (type === 'session_locked') { | ||||
|         // When locking, we don't need to maintain keypairs in memory anymore | ||||
|         // since they'll be reloaded from the vault when unlocking | ||||
|         sessionState = { | ||||
|           currentKeyspace: null, | ||||
|           keypairs: [], // Clear keypairs from memory since they're in the vault | ||||
|           selectedKeypair: null | ||||
|         }; | ||||
|       } | ||||
|        | ||||
|       // Persist to storage | ||||
|       chrome.storage.local.set(sessionState) | ||||
|         .then(() => { | ||||
|           console.log('Updated session state in storage:', sessionState); | ||||
|           sendResponse({ success: true }); | ||||
|         }) | ||||
|         .catch(error => { | ||||
|           console.error('Failed to persist session state:', error); | ||||
|           sendResponse({ success: false, error: error.message }); | ||||
|         }); | ||||
|        | ||||
|       return true; // Keep connection open for async response | ||||
|     } catch (error) { | ||||
|       console.error('Error in update_session message handler:', error); | ||||
|       sendResponse({ success: false, error: error.message }); | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Get session state | ||||
|   if (message.action === 'get_session') { | ||||
|     sendResponse(sessionState); | ||||
|     return false; // No async response needed | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										84
									
								
								extension/build.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,84 @@ | ||||
| // Simple build script for browser extension | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
|  | ||||
| // Paths | ||||
| const sourceDir = __dirname; | ||||
| const distDir = path.join(sourceDir, 'dist'); | ||||
|  | ||||
| // Make sure the dist directory exists | ||||
| if (!fs.existsSync(distDir)) { | ||||
|   fs.mkdirSync(distDir, { recursive: true }); | ||||
| } | ||||
|  | ||||
| // Helper function to copy a file | ||||
| function copyFile(src, dest) { | ||||
|   // Create destination directory if it doesn't exist | ||||
|   const destDir = path.dirname(dest); | ||||
|   if (!fs.existsSync(destDir)) { | ||||
|     fs.mkdirSync(destDir, { recursive: true }); | ||||
|   } | ||||
|    | ||||
|   // Copy the file | ||||
|   fs.copyFileSync(src, dest); | ||||
|   console.log(`Copied: ${path.relative(sourceDir, src)} -> ${path.relative(sourceDir, dest)}`); | ||||
| } | ||||
|  | ||||
| // Helper function to copy an entire directory | ||||
| function copyDir(src, dest) { | ||||
|   // Create destination directory | ||||
|   if (!fs.existsSync(dest)) { | ||||
|     fs.mkdirSync(dest, { recursive: true }); | ||||
|   } | ||||
|    | ||||
|   // Get list of files | ||||
|   const files = fs.readdirSync(src); | ||||
|    | ||||
|   // Copy each file | ||||
|   for (const file of files) { | ||||
|     const srcPath = path.join(src, file); | ||||
|     const destPath = path.join(dest, file); | ||||
|      | ||||
|     const stat = fs.statSync(srcPath); | ||||
|      | ||||
|     if (stat.isDirectory()) { | ||||
|       // Recursively copy directories | ||||
|       copyDir(srcPath, destPath); | ||||
|     } else { | ||||
|       // Copy file | ||||
|       copyFile(srcPath, destPath); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Copy manifest | ||||
| copyFile( | ||||
|   path.join(sourceDir, 'manifest.json'), | ||||
|   path.join(distDir, 'manifest.json') | ||||
| ); | ||||
|  | ||||
| // Copy assets | ||||
| copyDir( | ||||
|   path.join(sourceDir, 'assets'), | ||||
|   path.join(distDir, 'assets') | ||||
| ); | ||||
|  | ||||
| // Copy popup files | ||||
| copyDir( | ||||
|   path.join(sourceDir, 'popup'), | ||||
|   path.join(distDir, 'popup') | ||||
| ); | ||||
|  | ||||
| // Copy background script | ||||
| copyDir( | ||||
|   path.join(sourceDir, 'background'), | ||||
|   path.join(distDir, 'background') | ||||
| ); | ||||
|  | ||||
| // Copy WebAssembly files | ||||
| copyDir( | ||||
|   path.join(sourceDir, 'wasm'), | ||||
|   path.join(distDir, 'wasm') | ||||
| ); | ||||
|  | ||||
| console.log('Build complete! Extension files copied to dist directory.'); | ||||
							
								
								
									
										
											BIN
										
									
								
								extension/dist/assets/icon-128.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								extension/dist/assets/icon-16.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 454 B | 
							
								
								
									
										
											BIN
										
									
								
								extension/dist/assets/icon-32.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 712 B | 
							
								
								
									
										
											BIN
										
									
								
								extension/dist/assets/icon-48.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										2
									
								
								extension/dist/assets/popup.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| (function(){"use strict";var o=document.createElement("style");o.textContent=`body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;margin:0;padding:0;background-color:#202124;color:#e8eaed}.container{width:350px;padding:15px}h1{font-size:18px;margin:0 0 15px;border-bottom:1px solid #3c4043;padding-bottom:10px}h2{font-size:16px;margin:10px 0}.form-section{margin-bottom:20px;background-color:#292a2d;border-radius:8px;padding:15px}.form-group{margin-bottom:10px}label{display:block;margin-bottom:5px;font-size:13px;color:#9aa0a6}input,textarea{width:100%;padding:8px;border:1px solid #3c4043;border-radius:4px;background-color:#202124;color:#e8eaed;box-sizing:border-box}textarea{min-height:60px;resize:vertical}button{background-color:#8ab4f8;color:#202124;border:none;border-radius:4px;padding:8px 16px;font-weight:500;cursor:pointer;transition:background-color .3s}button:hover{background-color:#669df6}button.small{padding:4px 8px;font-size:12px}.button-group{display:flex;gap:10px}.status{margin:10px 0;padding:8px;background-color:#292a2d;border-radius:4px;font-size:13px}.list{margin-top:10px;max-height:150px;overflow-y:auto}.list-item{display:flex;justify-content:space-between;align-items:center;padding:8px;border-bottom:1px solid #3c4043}.list-item.selected{background-color:#8ab4f81a}.hidden{display:none}.session-info{margin-top:15px} | ||||
| `,document.head.appendChild(o);const e=""})(); | ||||
							
								
								
									
										81
									
								
								extension/dist/background/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,81 @@ | ||||
| // Background service worker for Modular Vault Extension | ||||
| // Handles state persistence between popup sessions | ||||
|  | ||||
| console.log('Background service worker started'); | ||||
|  | ||||
| // Store session state locally for quicker access | ||||
| let sessionState = { | ||||
|   currentKeyspace: null, | ||||
|   keypairs: [], | ||||
|   selectedKeypair: null | ||||
| }; | ||||
|  | ||||
| // Initialize state from storage | ||||
| chrome.storage.local.get(['currentKeyspace', 'keypairs', 'selectedKeypair']) | ||||
|   .then(state => { | ||||
|     sessionState = { | ||||
|       currentKeyspace: state.currentKeyspace || null, | ||||
|       keypairs: state.keypairs || [], | ||||
|       selectedKeypair: state.selectedKeypair || null | ||||
|     }; | ||||
|     console.log('Session state loaded from storage:', sessionState); | ||||
|   }) | ||||
|   .catch(error => { | ||||
|     console.error('Failed to load session state:', error); | ||||
|   }); | ||||
|  | ||||
| // Handle messages from the popup | ||||
| chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | ||||
|   console.log('Background received message:', message.action, message.type || ''); | ||||
|    | ||||
|   // Update session state | ||||
|   if (message.action === 'update_session') { | ||||
|     try { | ||||
|       const { type, data } = message; | ||||
|        | ||||
|       // Update our local state | ||||
|       if (type === 'keyspace') { | ||||
|         sessionState.currentKeyspace = data; | ||||
|       } else if (type === 'keypair_selected') { | ||||
|         sessionState.selectedKeypair = data; | ||||
|       } else if (type === 'keypair_added') { | ||||
|         sessionState.keypairs = [...sessionState.keypairs, data]; | ||||
|       } else if (type === 'keypairs_loaded') { | ||||
|         // Replace the entire keypair list with what came from the vault | ||||
|         console.log('Updating keypairs from vault:', data); | ||||
|         sessionState.keypairs = data; | ||||
|       } else if (type === 'session_locked') { | ||||
|         // When locking, we don't need to maintain keypairs in memory anymore | ||||
|         // since they'll be reloaded from the vault when unlocking | ||||
|         sessionState = { | ||||
|           currentKeyspace: null, | ||||
|           keypairs: [], // Clear keypairs from memory since they're in the vault | ||||
|           selectedKeypair: null | ||||
|         }; | ||||
|       } | ||||
|        | ||||
|       // Persist to storage | ||||
|       chrome.storage.local.set(sessionState) | ||||
|         .then(() => { | ||||
|           console.log('Updated session state in storage:', sessionState); | ||||
|           sendResponse({ success: true }); | ||||
|         }) | ||||
|         .catch(error => { | ||||
|           console.error('Failed to persist session state:', error); | ||||
|           sendResponse({ success: false, error: error.message }); | ||||
|         }); | ||||
|        | ||||
|       return true; // Keep connection open for async response | ||||
|     } catch (error) { | ||||
|       console.error('Error in update_session message handler:', error); | ||||
|       sendResponse({ success: false, error: error.message }); | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Get session state | ||||
|   if (message.action === 'get_session') { | ||||
|     sendResponse(sessionState); | ||||
|     return false; // No async response needed | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										36
									
								
								extension/dist/manifest.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| { | ||||
|   "manifest_version": 3, | ||||
|   "name": "Modular Vault Extension", | ||||
|   "version": "0.1.0", | ||||
|   "description": "Cross-browser modular vault for cryptographic operations and scripting.", | ||||
|   "action": { | ||||
|     "default_popup": "popup/index.html", | ||||
|     "default_icon": { | ||||
|       "16": "assets/icon-16.png", | ||||
|       "32": "assets/icon-32.png", | ||||
|       "48": "assets/icon-48.png", | ||||
|       "128": "assets/icon-128.png" | ||||
|     } | ||||
|   }, | ||||
|   "background": { | ||||
|     "service_worker": "background/index.js", | ||||
|     "type": "module" | ||||
|   }, | ||||
|   "permissions": [ | ||||
|     "storage", | ||||
|     "scripting" | ||||
|   ], | ||||
|   "host_permissions": [], | ||||
|   "icons": { | ||||
|     "16": "assets/icon-16.png", | ||||
|     "32": "assets/icon-32.png", | ||||
|     "48": "assets/icon-48.png", | ||||
|     "128": "assets/icon-128.png" | ||||
|   }, | ||||
|   "web_accessible_resources": [ | ||||
|     { | ||||
|       "resources": ["wasm/*.wasm", "wasm/*.js"], | ||||
|       "matches": ["<all_urls>"] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										13
									
								
								extension/dist/popup/index.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Modular Vault Extension</title> | ||||
|     <link rel="stylesheet" href="popup.css"> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="root"></div> | ||||
|     <script src="popup.js"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										117
									
								
								extension/dist/popup/popup.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,117 @@ | ||||
| /* Basic styles for the extension popup */ | ||||
| body { | ||||
|   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   background-color: #202124; | ||||
|   color: #e8eaed; | ||||
| } | ||||
|  | ||||
| .container { | ||||
|   width: 350px; | ||||
|   padding: 15px; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   font-size: 18px; | ||||
|   margin: 0 0 15px 0; | ||||
|   border-bottom: 1px solid #3c4043; | ||||
|   padding-bottom: 10px; | ||||
| } | ||||
|  | ||||
| h2 { | ||||
|   font-size: 16px; | ||||
|   margin: 10px 0; | ||||
| } | ||||
|  | ||||
| .form-section { | ||||
|   margin-bottom: 20px; | ||||
|   background-color: #292a2d; | ||||
|   border-radius: 8px; | ||||
|   padding: 15px; | ||||
| } | ||||
|  | ||||
| .form-group { | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| label { | ||||
|   display: block; | ||||
|   margin-bottom: 5px; | ||||
|   font-size: 13px; | ||||
|   color: #9aa0a6; | ||||
| } | ||||
|  | ||||
| input, textarea { | ||||
|   width: 100%; | ||||
|   padding: 8px; | ||||
|   border: 1px solid #3c4043; | ||||
|   border-radius: 4px; | ||||
|   background-color: #202124; | ||||
|   color: #e8eaed; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| textarea { | ||||
|   min-height: 60px; | ||||
|   resize: vertical; | ||||
| } | ||||
|  | ||||
| button { | ||||
|   background-color: #8ab4f8; | ||||
|   color: #202124; | ||||
|   border: none; | ||||
|   border-radius: 4px; | ||||
|   padding: 8px 16px; | ||||
|   font-weight: 500; | ||||
|   cursor: pointer; | ||||
|   transition: background-color 0.3s; | ||||
| } | ||||
|  | ||||
| button:hover { | ||||
|   background-color: #669df6; | ||||
| } | ||||
|  | ||||
| button.small { | ||||
|   padding: 4px 8px; | ||||
|   font-size: 12px; | ||||
| } | ||||
|  | ||||
| .button-group { | ||||
|   display: flex; | ||||
|   gap: 10px; | ||||
| } | ||||
|  | ||||
| .status { | ||||
|   margin: 10px 0; | ||||
|   padding: 8px; | ||||
|   background-color: #292a2d; | ||||
|   border-radius: 4px; | ||||
|   font-size: 13px; | ||||
| } | ||||
|  | ||||
| .list { | ||||
|   margin-top: 10px; | ||||
|   max-height: 150px; | ||||
|   overflow-y: auto; | ||||
| } | ||||
|  | ||||
| .list-item { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   padding: 8px; | ||||
|   border-bottom: 1px solid #3c4043; | ||||
| } | ||||
|  | ||||
| .list-item.selected { | ||||
|   background-color: rgba(138, 180, 248, 0.1); | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
|   display: none; | ||||
| } | ||||
|  | ||||
| .session-info { | ||||
|   margin-top: 15px; | ||||
| } | ||||
							
								
								
									
										306
									
								
								extension/dist/popup/popup.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,306 @@ | ||||
| // Simple non-module JavaScript for browser extension popup | ||||
| document.addEventListener('DOMContentLoaded', async function() { | ||||
|   const root = document.getElementById('root'); | ||||
|   root.innerHTML = ` | ||||
|     <div class="container"> | ||||
|       <h1>Modular Vault Extension</h1> | ||||
|       <div id="status" class="status">Loading WASM module...</div> | ||||
|        | ||||
|       <div id="session-controls"> | ||||
|         <div id="keyspace-form" class="form-section"> | ||||
|           <h2>Session</h2> | ||||
|           <div class="form-group"> | ||||
|             <label for="keyspace">Keyspace:</label> | ||||
|             <input type="text" id="keyspace" placeholder="Enter keyspace name"> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label for="password">Password:</label> | ||||
|             <input type="password" id="password" placeholder="Enter password"> | ||||
|           </div> | ||||
|           <div class="button-group"> | ||||
|             <button id="unlock-btn">Unlock</button> | ||||
|             <button id="create-btn">Create New</button> | ||||
|           </div> | ||||
|         </div> | ||||
|          | ||||
|         <div id="session-info" class="session-info hidden"> | ||||
|           <h2>Active Session</h2> | ||||
|           <p>Current keyspace: <span id="current-keyspace"></span></p> | ||||
|           <button id="lock-btn">Lock Session</button> | ||||
|            | ||||
|           <div id="keypair-section" class="form-section"> | ||||
|             <h2>Keypairs</h2> | ||||
|             <button id="create-keypair-btn">Create New Keypair</button> | ||||
|             <div id="keypair-list" class="list"></div> | ||||
|           </div> | ||||
|            | ||||
|           <div id="sign-section" class="form-section hidden"> | ||||
|             <h2>Sign Message</h2> | ||||
|             <div class="form-group"> | ||||
|               <label for="message">Message:</label> | ||||
|               <textarea id="message" placeholder="Enter message to sign"></textarea> | ||||
|             </div> | ||||
|             <button id="sign-btn">Sign</button> | ||||
|             <div class="form-group"> | ||||
|               <label for="signature">Signature:</label> | ||||
|               <textarea id="signature" readonly></textarea> | ||||
|               <button id="copy-btn" class="small">Copy</button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   `; | ||||
|    | ||||
|   // DOM elements | ||||
|   const statusEl = document.getElementById('status'); | ||||
|   const keyspaceFormEl = document.getElementById('keyspace-form'); | ||||
|   const sessionInfoEl = document.getElementById('session-info'); | ||||
|   const currentKeyspaceEl = document.getElementById('current-keyspace'); | ||||
|   const keyspaceInput = document.getElementById('keyspace'); | ||||
|   const passwordInput = document.getElementById('password'); | ||||
|   const unlockBtn = document.getElementById('unlock-btn'); | ||||
|   const createBtn = document.getElementById('create-btn'); | ||||
|   const lockBtn = document.getElementById('lock-btn'); | ||||
|   const createKeypairBtn = document.getElementById('create-keypair-btn'); | ||||
|   const keypairListEl = document.getElementById('keypair-list'); | ||||
|   const signSectionEl = document.getElementById('sign-section'); | ||||
|   const messageInput = document.getElementById('message'); | ||||
|   const signBtn = document.getElementById('sign-btn'); | ||||
|   const signatureOutput = document.getElementById('signature'); | ||||
|   const copyBtn = document.getElementById('copy-btn'); | ||||
|    | ||||
|   // State | ||||
|   let wasmModule = null; | ||||
|   let currentKeyspace = null; | ||||
|   let keypairs = []; | ||||
|   let selectedKeypairId = null; | ||||
|    | ||||
|   // Initialize | ||||
|   init(); | ||||
|    | ||||
|   async function init() { | ||||
|     try { | ||||
|       // Get session state from background | ||||
|       const sessionState = await getSessionState(); | ||||
|        | ||||
|       if (sessionState.currentKeyspace) { | ||||
|         // We have an active session | ||||
|         currentKeyspace = sessionState.currentKeyspace; | ||||
|         keypairs = sessionState.keypairs || []; | ||||
|         selectedKeypairId = sessionState.selectedKeypair; | ||||
|          | ||||
|         updateUI(); | ||||
|       } | ||||
|        | ||||
|       statusEl.textContent = 'Ready'; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   function updateUI() { | ||||
|     if (currentKeyspace) { | ||||
|       // Show session info | ||||
|       keyspaceFormEl.classList.add('hidden'); | ||||
|       sessionInfoEl.classList.remove('hidden'); | ||||
|       currentKeyspaceEl.textContent = currentKeyspace; | ||||
|        | ||||
|       // Update keypair list | ||||
|       updateKeypairList(); | ||||
|        | ||||
|       // Show/hide sign section based on selected keypair | ||||
|       if (selectedKeypairId) { | ||||
|         signSectionEl.classList.remove('hidden'); | ||||
|       } else { | ||||
|         signSectionEl.classList.add('hidden'); | ||||
|       } | ||||
|     } else { | ||||
|       // Show keyspace form | ||||
|       keyspaceFormEl.classList.remove('hidden'); | ||||
|       sessionInfoEl.classList.add('hidden'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   function updateKeypairList() { | ||||
|     // Clear list | ||||
|     keypairListEl.innerHTML = ''; | ||||
|      | ||||
|     // Add each keypair | ||||
|     keypairs.forEach(keypair => { | ||||
|       const item = document.createElement('div'); | ||||
|       item.className = 'list-item' + (selectedKeypairId === keypair.id ? ' selected' : ''); | ||||
|       item.innerHTML = ` | ||||
|         <span>${keypair.label || keypair.id}</span> | ||||
|         <button class="select-btn" data-id="${keypair.id}">Select</button> | ||||
|       `; | ||||
|       keypairListEl.appendChild(item); | ||||
|        | ||||
|       // Add select handler | ||||
|       item.querySelector('.select-btn').addEventListener('click', async () => { | ||||
|         try { | ||||
|           statusEl.textContent = 'Selecting keypair...'; | ||||
|           // Use background service to select keypair for now | ||||
|           await chrome.runtime.sendMessage({  | ||||
|             action: 'update_session',  | ||||
|             type: 'keypair_selected',  | ||||
|             data: keypair.id  | ||||
|           }); | ||||
|           selectedKeypairId = keypair.id; | ||||
|           updateUI(); | ||||
|           statusEl.textContent = 'Keypair selected: ' + keypair.id; | ||||
|         } catch (error) { | ||||
|           statusEl.textContent = 'Error selecting keypair: ' + (error.message || 'Unknown error'); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   // Get session state from background | ||||
|   async function getSessionState() { | ||||
|     return new Promise((resolve) => { | ||||
|       chrome.runtime.sendMessage({ action: 'get_session' }, (response) => { | ||||
|         resolve(response || { currentKeyspace: null, keypairs: [], selectedKeypair: null }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   // Event handlers | ||||
|   unlockBtn.addEventListener('click', async () => { | ||||
|     const keyspace = keyspaceInput.value.trim(); | ||||
|     const password = passwordInput.value; | ||||
|      | ||||
|     if (!keyspace || !password) { | ||||
|       statusEl.textContent = 'Please enter keyspace and password'; | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     statusEl.textContent = 'Unlocking session...'; | ||||
|      | ||||
|     try { | ||||
|       // For now, use the background service worker mock | ||||
|       await chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keyspace',  | ||||
|         data: keyspace  | ||||
|       }); | ||||
|        | ||||
|       currentKeyspace = keyspace; | ||||
|       updateUI(); | ||||
|       statusEl.textContent = 'Session unlocked!'; | ||||
|        | ||||
|       // Refresh state | ||||
|       const state = await getSessionState(); | ||||
|       keypairs = state.keypairs || []; | ||||
|       selectedKeypairId = state.selectedKeypair; | ||||
|       updateUI(); | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error unlocking session: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   createBtn.addEventListener('click', async () => { | ||||
|     const keyspace = keyspaceInput.value.trim(); | ||||
|     const password = passwordInput.value; | ||||
|      | ||||
|     if (!keyspace || !password) { | ||||
|       statusEl.textContent = 'Please enter keyspace and password'; | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     statusEl.textContent = 'Creating keyspace...'; | ||||
|      | ||||
|     try { | ||||
|       // For now, use the background service worker mock | ||||
|       await chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keyspace',  | ||||
|         data: keyspace  | ||||
|       }); | ||||
|        | ||||
|       currentKeyspace = keyspace; | ||||
|       updateUI(); | ||||
|       statusEl.textContent = 'Keyspace created and unlocked!'; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error creating keyspace: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   lockBtn.addEventListener('click', async () => { | ||||
|     statusEl.textContent = 'Locking session...'; | ||||
|      | ||||
|     try { | ||||
|       await chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'session_locked' | ||||
|       }); | ||||
|        | ||||
|       currentKeyspace = null; | ||||
|       keypairs = []; | ||||
|       selectedKeypairId = null; | ||||
|       updateUI(); | ||||
|       statusEl.textContent = 'Session locked'; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error locking session: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   createKeypairBtn.addEventListener('click', async () => { | ||||
|     statusEl.textContent = 'Creating keypair...'; | ||||
|      | ||||
|     try { | ||||
|       // Generate a mock keypair ID | ||||
|       const keyId = 'key-' + Date.now().toString(16); | ||||
|       const newKeypair = {  | ||||
|         id: keyId,  | ||||
|         label: `Secp256k1-Key-${keypairs.length + 1}`  | ||||
|       }; | ||||
|        | ||||
|       await chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keypair_added',  | ||||
|         data: newKeypair | ||||
|       }); | ||||
|        | ||||
|       // Refresh state | ||||
|       const state = await getSessionState(); | ||||
|       keypairs = state.keypairs || []; | ||||
|       updateUI(); | ||||
|        | ||||
|       statusEl.textContent = 'Keypair created: ' + keyId; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error creating keypair: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   signBtn.addEventListener('click', async () => { | ||||
|     const message = messageInput.value.trim(); | ||||
|      | ||||
|     if (!message) { | ||||
|       statusEl.textContent = 'Please enter a message to sign'; | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     if (!selectedKeypairId) { | ||||
|       statusEl.textContent = 'Please select a keypair first'; | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     statusEl.textContent = 'Signing message...'; | ||||
|      | ||||
|     try { | ||||
|       // For now, generate a mock signature | ||||
|       const mockSignature = Array.from({length: 64}, () => Math.floor(Math.random() * 16).toString(16)).join(''); | ||||
|       signatureOutput.value = mockSignature; | ||||
|       statusEl.textContent = 'Message signed!'; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error signing message: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   copyBtn.addEventListener('click', () => { | ||||
|     signatureOutput.select(); | ||||
|     document.execCommand('copy'); | ||||
|     statusEl.textContent = 'Signature copied to clipboard!'; | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										765
									
								
								extension/dist/wasm/wasm_app.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,765 @@ | ||||
| import * as __wbg_star0 from 'env'; | ||||
|  | ||||
| let wasm; | ||||
|  | ||||
| function addToExternrefTable0(obj) { | ||||
|     const idx = wasm.__externref_table_alloc(); | ||||
|     wasm.__wbindgen_export_2.set(idx, obj); | ||||
|     return idx; | ||||
| } | ||||
|  | ||||
| function handleError(f, args) { | ||||
|     try { | ||||
|         return f.apply(this, args); | ||||
|     } catch (e) { | ||||
|         const idx = addToExternrefTable0(e); | ||||
|         wasm.__wbindgen_exn_store(idx); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); | ||||
|  | ||||
| if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; | ||||
|  | ||||
| let cachedUint8ArrayMemory0 = null; | ||||
|  | ||||
| function getUint8ArrayMemory0() { | ||||
|     if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { | ||||
|         cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); | ||||
|     } | ||||
|     return cachedUint8ArrayMemory0; | ||||
| } | ||||
|  | ||||
| function getStringFromWasm0(ptr, len) { | ||||
|     ptr = ptr >>> 0; | ||||
|     return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); | ||||
| } | ||||
|  | ||||
| function isLikeNone(x) { | ||||
|     return x === undefined || x === null; | ||||
| } | ||||
|  | ||||
| function getArrayU8FromWasm0(ptr, len) { | ||||
|     ptr = ptr >>> 0; | ||||
|     return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); | ||||
| } | ||||
|  | ||||
| let WASM_VECTOR_LEN = 0; | ||||
|  | ||||
| const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); | ||||
|  | ||||
| const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' | ||||
|     ? function (arg, view) { | ||||
|     return cachedTextEncoder.encodeInto(arg, view); | ||||
| } | ||||
|     : function (arg, view) { | ||||
|     const buf = cachedTextEncoder.encode(arg); | ||||
|     view.set(buf); | ||||
|     return { | ||||
|         read: arg.length, | ||||
|         written: buf.length | ||||
|     }; | ||||
| }); | ||||
|  | ||||
| function passStringToWasm0(arg, malloc, realloc) { | ||||
|  | ||||
|     if (realloc === undefined) { | ||||
|         const buf = cachedTextEncoder.encode(arg); | ||||
|         const ptr = malloc(buf.length, 1) >>> 0; | ||||
|         getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); | ||||
|         WASM_VECTOR_LEN = buf.length; | ||||
|         return ptr; | ||||
|     } | ||||
|  | ||||
|     let len = arg.length; | ||||
|     let ptr = malloc(len, 1) >>> 0; | ||||
|  | ||||
|     const mem = getUint8ArrayMemory0(); | ||||
|  | ||||
|     let offset = 0; | ||||
|  | ||||
|     for (; offset < len; offset++) { | ||||
|         const code = arg.charCodeAt(offset); | ||||
|         if (code > 0x7F) break; | ||||
|         mem[ptr + offset] = code; | ||||
|     } | ||||
|  | ||||
|     if (offset !== len) { | ||||
|         if (offset !== 0) { | ||||
|             arg = arg.slice(offset); | ||||
|         } | ||||
|         ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; | ||||
|         const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); | ||||
|         const ret = encodeString(arg, view); | ||||
|  | ||||
|         offset += ret.written; | ||||
|         ptr = realloc(ptr, len, offset, 1) >>> 0; | ||||
|     } | ||||
|  | ||||
|     WASM_VECTOR_LEN = offset; | ||||
|     return ptr; | ||||
| } | ||||
|  | ||||
| let cachedDataViewMemory0 = null; | ||||
|  | ||||
| function getDataViewMemory0() { | ||||
|     if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { | ||||
|         cachedDataViewMemory0 = new DataView(wasm.memory.buffer); | ||||
|     } | ||||
|     return cachedDataViewMemory0; | ||||
| } | ||||
|  | ||||
| const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') | ||||
|     ? { register: () => {}, unregister: () => {} } | ||||
|     : new FinalizationRegistry(state => { | ||||
|     wasm.__wbindgen_export_5.get(state.dtor)(state.a, state.b) | ||||
| }); | ||||
|  | ||||
| function makeMutClosure(arg0, arg1, dtor, f) { | ||||
|     const state = { a: arg0, b: arg1, cnt: 1, dtor }; | ||||
|     const real = (...args) => { | ||||
|         // First up with a closure we increment the internal reference | ||||
|         // count. This ensures that the Rust closure environment won't | ||||
|         // be deallocated while we're invoking it. | ||||
|         state.cnt++; | ||||
|         const a = state.a; | ||||
|         state.a = 0; | ||||
|         try { | ||||
|             return f(a, state.b, ...args); | ||||
|         } finally { | ||||
|             if (--state.cnt === 0) { | ||||
|                 wasm.__wbindgen_export_5.get(state.dtor)(a, state.b); | ||||
|                 CLOSURE_DTORS.unregister(state); | ||||
|             } else { | ||||
|                 state.a = a; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|     real.original = state; | ||||
|     CLOSURE_DTORS.register(real, state, state); | ||||
|     return real; | ||||
| } | ||||
|  | ||||
| function debugString(val) { | ||||
|     // primitive types | ||||
|     const type = typeof val; | ||||
|     if (type == 'number' || type == 'boolean' || val == null) { | ||||
|         return  `${val}`; | ||||
|     } | ||||
|     if (type == 'string') { | ||||
|         return `"${val}"`; | ||||
|     } | ||||
|     if (type == 'symbol') { | ||||
|         const description = val.description; | ||||
|         if (description == null) { | ||||
|             return 'Symbol'; | ||||
|         } else { | ||||
|             return `Symbol(${description})`; | ||||
|         } | ||||
|     } | ||||
|     if (type == 'function') { | ||||
|         const name = val.name; | ||||
|         if (typeof name == 'string' && name.length > 0) { | ||||
|             return `Function(${name})`; | ||||
|         } else { | ||||
|             return 'Function'; | ||||
|         } | ||||
|     } | ||||
|     // objects | ||||
|     if (Array.isArray(val)) { | ||||
|         const length = val.length; | ||||
|         let debug = '['; | ||||
|         if (length > 0) { | ||||
|             debug += debugString(val[0]); | ||||
|         } | ||||
|         for(let i = 1; i < length; i++) { | ||||
|             debug += ', ' + debugString(val[i]); | ||||
|         } | ||||
|         debug += ']'; | ||||
|         return debug; | ||||
|     } | ||||
|     // Test for built-in | ||||
|     const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); | ||||
|     let className; | ||||
|     if (builtInMatches && builtInMatches.length > 1) { | ||||
|         className = builtInMatches[1]; | ||||
|     } else { | ||||
|         // Failed to match the standard '[object ClassName]' | ||||
|         return toString.call(val); | ||||
|     } | ||||
|     if (className == 'Object') { | ||||
|         // we're a user defined class or Object | ||||
|         // JSON.stringify avoids problems with cycles, and is generally much | ||||
|         // easier than looping through ownProperties of `val`. | ||||
|         try { | ||||
|             return 'Object(' + JSON.stringify(val) + ')'; | ||||
|         } catch (_) { | ||||
|             return 'Object'; | ||||
|         } | ||||
|     } | ||||
|     // errors | ||||
|     if (val instanceof Error) { | ||||
|         return `${val.name}: ${val.message}\n${val.stack}`; | ||||
|     } | ||||
|     // TODO we could test for more things here, like `Set`s and `Map`s. | ||||
|     return className; | ||||
| } | ||||
| /** | ||||
|  * Initialize the scripting environment (must be called before run_rhai) | ||||
|  */ | ||||
| export function init_rhai_env() { | ||||
|     wasm.init_rhai_env(); | ||||
| } | ||||
|  | ||||
| function takeFromExternrefTable0(idx) { | ||||
|     const value = wasm.__wbindgen_export_2.get(idx); | ||||
|     wasm.__externref_table_dealloc(idx); | ||||
|     return value; | ||||
| } | ||||
| /** | ||||
|  * Securely run a Rhai script in the extension context (must be called only after user approval) | ||||
|  * @param {string} script | ||||
|  * @returns {any} | ||||
|  */ | ||||
| export function run_rhai(script) { | ||||
|     const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.run_rhai(ptr0, len0); | ||||
|     if (ret[2]) { | ||||
|         throw takeFromExternrefTable0(ret[1]); | ||||
|     } | ||||
|     return takeFromExternrefTable0(ret[0]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Initialize session with keyspace and password | ||||
|  * @param {string} keyspace | ||||
|  * @param {string} password | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export function init_session(keyspace, password) { | ||||
|     const ptr0 = passStringToWasm0(keyspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len1 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.init_session(ptr0, len0, ptr1, len1); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Lock the session (zeroize password and session) | ||||
|  */ | ||||
| export function lock_session() { | ||||
|     wasm.lock_session(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get all keypairs from the current session | ||||
|  * Returns an array of keypair objects with id, type, and metadata | ||||
|  * Select keypair for the session | ||||
|  * @param {string} key_id | ||||
|  */ | ||||
| export function select_keypair(key_id) { | ||||
|     const ptr0 = passStringToWasm0(key_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.select_keypair(ptr0, len0); | ||||
|     if (ret[1]) { | ||||
|         throw takeFromExternrefTable0(ret[0]); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * List keypairs in the current session's keyspace | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function list_keypairs() { | ||||
|     const ret = wasm.list_keypairs(); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Add a keypair to the current keyspace | ||||
|  * @param {string | null} [key_type] | ||||
|  * @param {string | null} [metadata] | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function add_keypair(key_type, metadata) { | ||||
|     var ptr0 = isLikeNone(key_type) ? 0 : passStringToWasm0(key_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     var len0 = WASM_VECTOR_LEN; | ||||
|     var ptr1 = isLikeNone(metadata) ? 0 : passStringToWasm0(metadata, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     var len1 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.add_keypair(ptr0, len0, ptr1, len1); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function passArray8ToWasm0(arg, malloc) { | ||||
|     const ptr = malloc(arg.length * 1, 1) >>> 0; | ||||
|     getUint8ArrayMemory0().set(arg, ptr / 1); | ||||
|     WASM_VECTOR_LEN = arg.length; | ||||
|     return ptr; | ||||
| } | ||||
| /** | ||||
|  * Sign message with current session | ||||
|  * @param {Uint8Array} message | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function sign(message) { | ||||
|     const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.sign(ptr0, len0); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_32(arg0, arg1, arg2) { | ||||
|     wasm.closure77_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_35(arg0, arg1, arg2) { | ||||
|     wasm.closure126_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_38(arg0, arg1, arg2) { | ||||
|     wasm.closure188_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_123(arg0, arg1, arg2, arg3) { | ||||
|     wasm.closure213_externref_shim(arg0, arg1, arg2, arg3); | ||||
| } | ||||
|  | ||||
| const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"]; | ||||
|  | ||||
| async function __wbg_load(module, imports) { | ||||
|     if (typeof Response === 'function' && module instanceof Response) { | ||||
|         if (typeof WebAssembly.instantiateStreaming === 'function') { | ||||
|             try { | ||||
|                 return await WebAssembly.instantiateStreaming(module, imports); | ||||
|  | ||||
|             } catch (e) { | ||||
|                 if (module.headers.get('Content-Type') != 'application/wasm') { | ||||
|                     console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); | ||||
|  | ||||
|                 } else { | ||||
|                     throw e; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const bytes = await module.arrayBuffer(); | ||||
|         return await WebAssembly.instantiate(bytes, imports); | ||||
|  | ||||
|     } else { | ||||
|         const instance = await WebAssembly.instantiate(module, imports); | ||||
|  | ||||
|         if (instance instanceof WebAssembly.Instance) { | ||||
|             return { instance, module }; | ||||
|  | ||||
|         } else { | ||||
|             return instance; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| function __wbg_get_imports() { | ||||
|     const imports = {}; | ||||
|     imports.wbg = {}; | ||||
|     imports.wbg.__wbg_buffer_609cc3eee51ed158 = function(arg0) { | ||||
|         const ret = arg0.buffer; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.call(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.call(arg1, arg2); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) { | ||||
|         const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) { | ||||
|         const ret = arg0.crypto; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) { | ||||
|         console.error(arg0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_error_ff4ddaabdfc5dbb3 = function() { return handleError(function (arg0) { | ||||
|         const ret = arg0.error; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_getRandomValues_3c9c0d586e575a16 = function() { return handleError(function (arg0, arg1) { | ||||
|         globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1)); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) { | ||||
|         arg0.getRandomValues(arg1); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_get_4f73335ab78445db = function(arg0, arg1, arg2) { | ||||
|         const ret = arg1[arg2 >>> 0]; | ||||
|         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.__wbg_get_67b2ba62fc30de12 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = Reflect.get(arg0, arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_get_8da03f81f6a1111e = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.get(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_instanceof_IdbDatabase_a3ef009ca00059f9 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBDatabase; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbFactory_12eaba3366f4302f = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBFactory; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbOpenDbRequest_a3416e156c9db893 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBOpenDBRequest; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbRequest_4813c3f207666aa4 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBRequest; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) { | ||||
|         const ret = arg0.length; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) { | ||||
|         const ret = arg0.msCrypto; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) { | ||||
|         try { | ||||
|             var state0 = {a: arg0, b: arg1}; | ||||
|             var cb0 = (arg0, arg1) => { | ||||
|                 const a = state0.a; | ||||
|                 state0.a = 0; | ||||
|                 try { | ||||
|                     return __wbg_adapter_123(a, state0.b, arg0, arg1); | ||||
|                 } finally { | ||||
|                     state0.a = a; | ||||
|                 } | ||||
|             }; | ||||
|             const ret = new Promise(cb0); | ||||
|             return ret; | ||||
|         } finally { | ||||
|             state0.a = state0.b = 0; | ||||
|         } | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_405e22f390576ce2 = function() { | ||||
|         const ret = new Object(); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_78feb108b6472713 = function() { | ||||
|         const ret = new Array(); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) { | ||||
|         const ret = new Uint8Array(arg0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) { | ||||
|         const ret = new Function(getStringFromWasm0(arg0, arg1)); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function(arg0, arg1, arg2) { | ||||
|         const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newwithlength_a381634e90c276d4 = function(arg0) { | ||||
|         const ret = new Uint8Array(arg0 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) { | ||||
|         const ret = arg0.node; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) { | ||||
|         const ret = arg0.objectStoreNames; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_objectStore_21878d46d25b64b6 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2)); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.open(getStringFromWasm0(arg1, arg2)); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_open_e0c0b2993eb596e1 = function() { return handleError(function (arg0, arg1, arg2, arg3) { | ||||
|         const ret = arg0.open(getStringFromWasm0(arg1, arg2), arg3 >>> 0); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_process_dc0fbacc7c1c06f7 = function(arg0) { | ||||
|         const ret = arg0.process; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_push_737cfc8c1432c2c6 = function(arg0, arg1) { | ||||
|         const ret = arg0.push(arg1); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_put_066faa31a6a88f5b = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.put(arg1, arg2); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_put_9ef5363941008835 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.put(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) { | ||||
|         queueMicrotask(arg0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) { | ||||
|         const ret = arg0.queueMicrotask; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) { | ||||
|         arg0.randomFillSync(arg1); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () { | ||||
|         const ret = module.require; | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) { | ||||
|         const ret = Promise.resolve(arg0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_result_f29afabdf2c05826 = function() { return handleError(function (arg0) { | ||||
|         const ret = arg0.result; | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) { | ||||
|         arg0.set(arg1, arg2 >>> 0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) { | ||||
|         arg0.onerror = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) { | ||||
|         arg0.onsuccess = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonupgradeneeded_fcf7ce4f2eb0cb5f = function(arg0, arg1) { | ||||
|         arg0.onupgradeneeded = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() { | ||||
|         const ret = typeof global === 'undefined' ? null : global; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() { | ||||
|         const ret = typeof globalThis === 'undefined' ? null : globalThis; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() { | ||||
|         const ret = typeof self === 'undefined' ? null : self; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() { | ||||
|         const ret = typeof window === 'undefined' ? null : window; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_subarray_aa9065fa9dc5df96 = function(arg0, arg1, arg2) { | ||||
|         const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_target_0a62d9d79a2a1ede = function(arg0) { | ||||
|         const ret = arg0.target; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) { | ||||
|         const ret = arg0.then(arg1); | ||||
|         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; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) { | ||||
|         const ret = arg0.versions; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_cb_drop = function(arg0) { | ||||
|         const obj = arg0.original; | ||||
|         if (obj.cnt-- == 1) { | ||||
|             obj.a = 0; | ||||
|             return true; | ||||
|         } | ||||
|         const ret = false; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { | ||||
|         const ret = debugString(arg1); | ||||
|         const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         const len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_init_externref_table = function() { | ||||
|         const table = wasm.__wbindgen_export_2; | ||||
|         const offset = table.grow(4); | ||||
|         table.set(0, undefined); | ||||
|         table.set(offset + 0, undefined); | ||||
|         table.set(offset + 1, null); | ||||
|         table.set(offset + 2, true); | ||||
|         table.set(offset + 3, false); | ||||
|         ; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_function = function(arg0) { | ||||
|         const ret = typeof(arg0) === 'function'; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_null = function(arg0) { | ||||
|         const ret = arg0 === null; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_object = function(arg0) { | ||||
|         const val = arg0; | ||||
|         const ret = typeof(val) === 'object' && val !== null; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_string = function(arg0) { | ||||
|         const ret = typeof(arg0) === 'string'; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_undefined = function(arg0) { | ||||
|         const ret = arg0 === undefined; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_json_parse = function(arg0, arg1) { | ||||
|         const ret = JSON.parse(getStringFromWasm0(arg0, arg1)); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { | ||||
|         const obj = arg1; | ||||
|         const ret = JSON.stringify(obj === undefined ? null : obj); | ||||
|         const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         const len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_memory = function() { | ||||
|         const ret = wasm.memory; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_string_new = function(arg0, arg1) { | ||||
|         const ret = getStringFromWasm0(arg0, arg1); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_throw = function(arg0, arg1) { | ||||
|         throw new Error(getStringFromWasm0(arg0, arg1)); | ||||
|     }; | ||||
|     imports['env'] = __wbg_star0; | ||||
|  | ||||
|     return imports; | ||||
| } | ||||
|  | ||||
| function __wbg_init_memory(imports, memory) { | ||||
|  | ||||
| } | ||||
|  | ||||
| function __wbg_finalize_init(instance, module) { | ||||
|     wasm = instance.exports; | ||||
|     __wbg_init.__wbindgen_wasm_module = module; | ||||
|     cachedDataViewMemory0 = null; | ||||
|     cachedUint8ArrayMemory0 = null; | ||||
|  | ||||
|  | ||||
|     wasm.__wbindgen_start(); | ||||
|     return wasm; | ||||
| } | ||||
|  | ||||
| function initSync(module) { | ||||
|     if (wasm !== undefined) return wasm; | ||||
|  | ||||
|  | ||||
|     if (typeof module !== 'undefined') { | ||||
|         if (Object.getPrototypeOf(module) === Object.prototype) { | ||||
|             ({module} = module) | ||||
|         } else { | ||||
|             console.warn('using deprecated parameters for `initSync()`; pass a single object instead') | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const imports = __wbg_get_imports(); | ||||
|  | ||||
|     __wbg_init_memory(imports); | ||||
|  | ||||
|     if (!(module instanceof WebAssembly.Module)) { | ||||
|         module = new WebAssembly.Module(module); | ||||
|     } | ||||
|  | ||||
|     const instance = new WebAssembly.Instance(module, imports); | ||||
|  | ||||
|     return __wbg_finalize_init(instance, module); | ||||
| } | ||||
|  | ||||
| async function __wbg_init(module_or_path) { | ||||
|     if (wasm !== undefined) return wasm; | ||||
|  | ||||
|  | ||||
|     if (typeof module_or_path !== 'undefined') { | ||||
|         if (Object.getPrototypeOf(module_or_path) === Object.prototype) { | ||||
|             ({module_or_path} = module_or_path) | ||||
|         } else { | ||||
|             console.warn('using deprecated parameters for the initialization function; pass a single object instead') | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (typeof module_or_path === 'undefined') { | ||||
|         module_or_path = new URL('wasm_app_bg.wasm', import.meta.url); | ||||
|     } | ||||
|     const imports = __wbg_get_imports(); | ||||
|  | ||||
|     if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { | ||||
|         module_or_path = fetch(module_or_path); | ||||
|     } | ||||
|  | ||||
|     __wbg_init_memory(imports); | ||||
|  | ||||
|     const { instance, module } = await __wbg_load(await module_or_path, imports); | ||||
|  | ||||
|     return __wbg_finalize_init(instance, module); | ||||
| } | ||||
|  | ||||
| export { initSync }; | ||||
| export default __wbg_init; | ||||
							
								
								
									
										
											BIN
										
									
								
								extension/dist/wasm/wasm_app_bg.wasm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										36
									
								
								extension/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| { | ||||
|   "manifest_version": 3, | ||||
|   "name": "Modular Vault Extension", | ||||
|   "version": "0.1.0", | ||||
|   "description": "Cross-browser modular vault for cryptographic operations and scripting.", | ||||
|   "action": { | ||||
|     "default_popup": "popup/index.html", | ||||
|     "default_icon": { | ||||
|       "16": "assets/icon-16.png", | ||||
|       "32": "assets/icon-32.png", | ||||
|       "48": "assets/icon-48.png", | ||||
|       "128": "assets/icon-128.png" | ||||
|     } | ||||
|   }, | ||||
|   "background": { | ||||
|     "service_worker": "background/index.js", | ||||
|     "type": "module" | ||||
|   }, | ||||
|   "permissions": [ | ||||
|     "storage", | ||||
|     "scripting" | ||||
|   ], | ||||
|   "host_permissions": [], | ||||
|   "icons": { | ||||
|     "16": "assets/icon-16.png", | ||||
|     "32": "assets/icon-32.png", | ||||
|     "48": "assets/icon-48.png", | ||||
|     "128": "assets/icon-128.png" | ||||
|   }, | ||||
|   "web_accessible_resources": [ | ||||
|     { | ||||
|       "resources": ["wasm/*.wasm", "wasm/*.js"], | ||||
|       "matches": ["<all_urls>"] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										1474
									
								
								extension/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										21
									
								
								extension/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "name": "modular-vault-extension", | ||||
|   "version": "0.1.0", | ||||
|   "description": "Cross-browser modular vault extension with secure WASM integration and React UI.", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "dev": "vite --mode development", | ||||
|     "build": "vite build", | ||||
|     "build:ext": "node build.js" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@vitejs/plugin-react": "^4.4.1", | ||||
|     "react": "^18.3.1", | ||||
|     "react-dom": "^18.3.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "vite": "^4.5.0", | ||||
|     "vite-plugin-top-level-await": "^1.4.0", | ||||
|     "vite-plugin-wasm": "^3.4.1" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										219
									
								
								extension/popup/App.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,219 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import KeyspaceManager from './KeyspaceManager'; | ||||
| import KeypairManager from './KeypairManager'; | ||||
| import SignMessage from './SignMessage'; | ||||
| import * as wasmHelper from './WasmHelper'; | ||||
|  | ||||
| function App() { | ||||
|   const [wasmState, setWasmState] = useState({ | ||||
|     loading: false, | ||||
|     initialized: false, | ||||
|     error: null | ||||
|   }); | ||||
|   const [locked, setLocked] = useState(true); | ||||
|   const [keyspaces, setKeyspaces] = useState([]); | ||||
|   const [currentKeyspace, setCurrentKeyspace] = useState(''); | ||||
|   const [keypairs, setKeypairs] = useState([]); // [{id, label, publicKey}] | ||||
|   const [selectedKeypair, setSelectedKeypair] = useState(''); | ||||
|   const [signature, setSignature] = useState(''); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [status, setStatus] = useState(''); | ||||
|  | ||||
|   // Load WebAssembly on component mount | ||||
|   useEffect(() => { | ||||
|     async function initWasm() { | ||||
|       try { | ||||
|         setStatus('Loading WebAssembly module...'); | ||||
|         await wasmHelper.loadWasmModule(); | ||||
|         setWasmState(wasmHelper.getWasmState()); | ||||
|         setStatus('WebAssembly module loaded'); | ||||
|         // Load session state | ||||
|         await refreshStatus(); | ||||
|       } catch (error) { | ||||
|         console.error('Failed to load WebAssembly:', error); | ||||
|         setStatus('Error loading WebAssembly: ' + (error.message || 'Unknown error')); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     initWasm(); | ||||
|   }, []); | ||||
|  | ||||
|   // Fetch status from background on mount | ||||
|   async function refreshStatus() { | ||||
|     const state = await wasmHelper.getSessionState(); | ||||
|     setCurrentKeyspace(state.currentKeyspace || ''); | ||||
|     setKeypairs(state.keypairs || []); | ||||
|     setSelectedKeypair(state.selectedKeypair || ''); | ||||
|     setLocked(!state.currentKeyspace); | ||||
|      | ||||
|     // For demo: collect all keyspaces from storage | ||||
|     if (state.keypairs && state.keypairs.length > 0) { | ||||
|       setKeyspaces([state.currentKeyspace]); | ||||
|     } else { | ||||
|       setKeyspaces([state.currentKeyspace].filter(Boolean)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Session unlock/create | ||||
|   const handleUnlock = async (keyspace, password) => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Unlocking...'); | ||||
|     try { | ||||
|       await wasmHelper.initSession(keyspace, password); | ||||
|       setCurrentKeyspace(keyspace); | ||||
|       setLocked(false); | ||||
|       setStatus('Session unlocked!'); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Unlock failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleCreateKeyspace = async (keyspace, password) => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Creating keyspace...'); | ||||
|     try { | ||||
|       await wasmHelper.initSession(keyspace, password); | ||||
|       setCurrentKeyspace(keyspace); | ||||
|       setLocked(false); | ||||
|       setStatus('Keyspace created and unlocked!'); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Create failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleLock = async () => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Locking...'); | ||||
|     try { | ||||
|       await wasmHelper.lockSession(); | ||||
|       setLocked(true); | ||||
|       setCurrentKeyspace(''); | ||||
|       setKeypairs([]); | ||||
|       setSelectedKeypair(''); | ||||
|       setStatus('Session locked.'); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Lock failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleSelectKeypair = async (id) => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Selecting keypair...'); | ||||
|     try { | ||||
|       await wasmHelper.selectKeypair(id); | ||||
|       setSelectedKeypair(id); | ||||
|       setStatus('Keypair selected.'); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Select failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleCreateKeypair = async () => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Creating keypair...'); | ||||
|     try { | ||||
|       const keyId = await wasmHelper.addKeypair(); | ||||
|       setStatus('Keypair created. ID: ' + keyId); | ||||
|       await refreshStatus(); | ||||
|     } catch (e) { | ||||
|       setStatus('Create failed: ' + e); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|    | ||||
|   const handleSign = async (message) => { | ||||
|     if (!wasmState.initialized) { | ||||
|       setStatus('WebAssembly module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     setLoading(true); | ||||
|     setStatus('Signing message...'); | ||||
|     try { | ||||
|       if (!selectedKeypair) { | ||||
|         throw new Error('No keypair selected'); | ||||
|       } | ||||
|       const sig = await wasmHelper.sign(message); | ||||
|       setSignature(sig); | ||||
|       setStatus('Message signed!'); | ||||
|     } catch (e) { | ||||
|       setStatus('Signing failed: ' + e); | ||||
|       setSignature(''); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <div className="App"> | ||||
|       <h1>Modular Vault Extension</h1> | ||||
|       {wasmState.error && ( | ||||
|         <div className="error"> | ||||
|           WebAssembly Error: {wasmState.error} | ||||
|         </div> | ||||
|       )} | ||||
|       <KeyspaceManager | ||||
|         keyspaces={keyspaces} | ||||
|         onUnlock={handleUnlock} | ||||
|         onCreate={handleCreateKeyspace} | ||||
|         locked={locked} | ||||
|         onLock={handleLock} | ||||
|         currentKeyspace={currentKeyspace} | ||||
|       /> | ||||
|       {!locked && ( | ||||
|         <> | ||||
|           <KeypairManager | ||||
|             keypairs={keypairs} | ||||
|             onSelect={handleSelectKeypair} | ||||
|             onCreate={handleCreateKeypair} | ||||
|             selectedKeypair={selectedKeypair} | ||||
|           /> | ||||
|           {selectedKeypair && ( | ||||
|             <SignMessage | ||||
|               onSign={handleSign} | ||||
|               signature={signature} | ||||
|               loading={loading} | ||||
|             /> | ||||
|           )} | ||||
|         </> | ||||
|       )} | ||||
|       <div className="status" style={{marginTop: '1rem', minHeight: 24}}> | ||||
|         {status} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default App; | ||||
							
								
								
									
										30
									
								
								extension/popup/KeypairManager.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| import React, { useState } from 'react'; | ||||
|  | ||||
| export default function KeypairManager({ keypairs, onSelect, onCreate, selectedKeypair }) { | ||||
|   const [creating, setCreating] = useState(false); | ||||
|  | ||||
|   return ( | ||||
|     <div className="keypair-manager"> | ||||
|       <label>Keypair:</label> | ||||
|       <select value={selectedKeypair || ''} onChange={e => onSelect(e.target.value)}> | ||||
|         <option value="" disabled>Select keypair</option> | ||||
|         {keypairs.map(kp => ( | ||||
|           <option key={kp.id} value={kp.id}>{kp.label}</option> | ||||
|         ))} | ||||
|       </select> | ||||
|       <button onClick={() => setCreating(true)} style={{marginLeft: 8}}>Create New</button> | ||||
|       {creating && ( | ||||
|         <div style={{marginTop: '0.5rem'}}> | ||||
|           <button onClick={() => { onCreate(); setCreating(false); }}>Create Secp256k1 Keypair</button> | ||||
|           <button onClick={() => setCreating(false)} style={{marginLeft: 8}}>Cancel</button> | ||||
|         </div> | ||||
|       )} | ||||
|       {selectedKeypair && ( | ||||
|         <div style={{marginTop: '0.5rem'}}> | ||||
|           <span>Public Key: <code>{keypairs.find(kp => kp.id === selectedKeypair)?.publicKey}</code></span> | ||||
|           <button onClick={() => navigator.clipboard.writeText(keypairs.find(kp => kp.id === selectedKeypair)?.publicKey)} style={{marginLeft: 8}}>Copy</button> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										30
									
								
								extension/popup/KeyspaceManager.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| import React, { useState } from 'react'; | ||||
|  | ||||
| export default function KeyspaceManager({ keyspaces, onUnlock, onCreate, locked, onLock, currentKeyspace }) { | ||||
|   const [selected, setSelected] = useState(keyspaces[0] || ''); | ||||
|   const [password, setPassword] = useState(''); | ||||
|   const [newKeyspace, setNewKeyspace] = useState(''); | ||||
|  | ||||
|   if (locked) { | ||||
|     return ( | ||||
|       <div className="keyspace-manager"> | ||||
|         <label>Keyspace:</label> | ||||
|         <select value={selected} onChange={e => setSelected(e.target.value)}> | ||||
|           {keyspaces.map(k => <option key={k} value={k}>{k}</option>)} | ||||
|         </select> | ||||
|         <button onClick={() => onUnlock(selected, password)} disabled={!selected || !password}>Unlock</button> | ||||
|         <div style={{marginTop: '0.5rem'}}> | ||||
|           <input placeholder="New keyspace name" value={newKeyspace} onChange={e => setNewKeyspace(e.target.value)} /> | ||||
|           <input placeholder="Password" type="password" value={password} onChange={e => setPassword(e.target.value)} /> | ||||
|           <button onClick={() => onCreate(newKeyspace, password)} disabled={!newKeyspace || !password}>Create</button> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|   return ( | ||||
|     <div className="keyspace-manager"> | ||||
|       <span>Keyspace: <b>{currentKeyspace}</b></span> | ||||
|       <button onClick={onLock} style={{marginLeft: 8}}>Lock Session</button> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										27
									
								
								extension/popup/SignMessage.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| import React, { useState } from 'react'; | ||||
|  | ||||
| export default function SignMessage({ onSign, signature, loading }) { | ||||
|   const [message, setMessage] = useState(''); | ||||
|  | ||||
|   return ( | ||||
|     <div className="sign-message"> | ||||
|       <label>Message to sign:</label> | ||||
|       <input | ||||
|         type="text" | ||||
|         placeholder="Enter plaintext message" | ||||
|         value={message} | ||||
|         onChange={e => setMessage(e.target.value)} | ||||
|         style={{width: '100%', marginBottom: 8}} | ||||
|       /> | ||||
|       <button onClick={() => onSign(message)} disabled={!message || loading}> | ||||
|         {loading ? 'Signing...' : 'Sign'} | ||||
|       </button> | ||||
|       {signature && ( | ||||
|         <div style={{marginTop: '0.5rem'}}> | ||||
|           <span>Signature: <code>{signature}</code></span> | ||||
|           <button onClick={() => navigator.clipboard.writeText(signature)} style={{marginLeft: 8}}>Copy</button> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										667
									
								
								extension/popup/WasmHelper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,667 @@ | ||||
| /** | ||||
|  * Browser extension-friendly WebAssembly loader and helper functions | ||||
|  * This handles loading the WebAssembly module without relying on ES modules | ||||
|  */ | ||||
|  | ||||
| // Global reference to the loaded WebAssembly module | ||||
| let wasmModule = null; | ||||
|  | ||||
| // Initialization state | ||||
| const state = { | ||||
|   loading: false, | ||||
|   initialized: false, | ||||
|   error: null | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Load the WebAssembly module | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export async function loadWasmModule() { | ||||
|   if (state.initialized || state.loading) { | ||||
|     return; | ||||
|   } | ||||
|    | ||||
|   state.loading = true; | ||||
|    | ||||
|   try { | ||||
|     // Get paths to WebAssembly files | ||||
|     const wasmJsPath = chrome.runtime.getURL('wasm/wasm_app.js'); | ||||
|     const wasmBinaryPath = chrome.runtime.getURL('wasm/wasm_app_bg.wasm'); | ||||
|      | ||||
|     console.log('Loading WASM JS from:', wasmJsPath); | ||||
|     console.log('Loading WASM binary from:', wasmBinaryPath); | ||||
|      | ||||
|     // Create a container for our temporary WebAssembly globals | ||||
|     window.__wasmApp = {}; | ||||
|      | ||||
|     // Create a script element to load the JS file | ||||
|     const script = document.createElement('script'); | ||||
|     script.src = wasmJsPath; | ||||
|      | ||||
|     // Wait for the script to load | ||||
|     await new Promise((resolve, reject) => { | ||||
|       script.onload = resolve; | ||||
|       script.onerror = () => reject(new Error('Failed to load WASM JavaScript file')); | ||||
|       document.head.appendChild(script); | ||||
|     }); | ||||
|      | ||||
|     // Check if the wasm_app global was created | ||||
|     if (!window.wasm_app && !window.__wbg_init) { | ||||
|       throw new Error('WASM module did not export expected functions'); | ||||
|     } | ||||
|      | ||||
|     // Get the initialization function | ||||
|     const init = window.__wbg_init || (window.wasm_app && window.wasm_app.default); | ||||
|      | ||||
|     if (!init || typeof init !== 'function') { | ||||
|       throw new Error('WASM init function not found'); | ||||
|     } | ||||
|      | ||||
|     // Fetch the WASM binary file | ||||
|     const response = await fetch(wasmBinaryPath); | ||||
|     if (!response.ok) { | ||||
|       throw new Error(`Failed to fetch WASM binary: ${response.status} ${response.statusText}`); | ||||
|     } | ||||
|      | ||||
|     // Get the binary data | ||||
|     const wasmBinary = await response.arrayBuffer(); | ||||
|      | ||||
|     // Initialize the WASM module | ||||
|     await init(wasmBinary); | ||||
|      | ||||
|     // Debug logging for available functions in the WebAssembly module | ||||
|     console.log('Available WebAssembly functions:'); | ||||
|     console.log('init_rhai_env:', typeof window.init_rhai_env, typeof (window.wasm_app && window.wasm_app.init_rhai_env)); | ||||
|     console.log('init_session:', typeof window.init_session, typeof (window.wasm_app && window.wasm_app.init_session)); | ||||
|     console.log('lock_session:', typeof window.lock_session, typeof (window.wasm_app && window.wasm_app.lock_session)); | ||||
|     console.log('add_keypair:', typeof window.add_keypair, typeof (window.wasm_app && window.wasm_app.add_keypair)); | ||||
|     console.log('select_keypair:', typeof window.select_keypair, typeof (window.wasm_app && window.wasm_app.select_keypair)); | ||||
|     console.log('sign:', typeof window.sign, typeof (window.wasm_app && window.wasm_app.sign)); | ||||
|     console.log('run_rhai:', typeof window.run_rhai, typeof (window.wasm_app && window.wasm_app.run_rhai)); | ||||
|     console.log('list_keypairs:', typeof window.list_keypairs, typeof (window.wasm_app && window.wasm_app.list_keypairs)); | ||||
|      | ||||
|     // Store reference to all the exported functions | ||||
|     wasmModule = { | ||||
|       init_rhai_env: window.init_rhai_env || (window.wasm_app && window.wasm_app.init_rhai_env), | ||||
|       init_session: window.init_session || (window.wasm_app && window.wasm_app.init_session), | ||||
|       lock_session: window.lock_session || (window.wasm_app && window.wasm_app.lock_session), | ||||
|       add_keypair: window.add_keypair || (window.wasm_app && window.wasm_app.add_keypair), | ||||
|       select_keypair: window.select_keypair || (window.wasm_app && window.wasm_app.select_keypair), | ||||
|       sign: window.sign || (window.wasm_app && window.wasm_app.sign), | ||||
|       run_rhai: window.run_rhai || (window.wasm_app && window.wasm_app.run_rhai), | ||||
|       list_keypairs: window.list_keypairs || (window.wasm_app && window.wasm_app.list_keypairs), | ||||
|       list_keypairs_debug: window.list_keypairs_debug || (window.wasm_app && window.wasm_app.list_keypairs_debug), | ||||
|       check_indexeddb: window.check_indexeddb || (window.wasm_app && window.wasm_app.check_indexeddb) | ||||
|     }; | ||||
|      | ||||
|     // Log what was actually registered | ||||
|     console.log('Registered WebAssembly module functions:'); | ||||
|     for (const [key, value] of Object.entries(wasmModule)) { | ||||
|       console.log(`${key}: ${typeof value}`, value ? 'Available' : 'Missing'); | ||||
|     } | ||||
|      | ||||
|     // Initialize the WASM environment | ||||
|     if (typeof wasmModule.init_rhai_env === 'function') { | ||||
|       wasmModule.init_rhai_env(); | ||||
|     } | ||||
|      | ||||
|     state.initialized = true; | ||||
|     console.log('WASM module loaded and initialized successfully'); | ||||
|      | ||||
|   } catch (error) { | ||||
|     console.error('Failed to load WASM module:', error); | ||||
|     state.error = error.message || 'Unknown error loading WebAssembly module'; | ||||
|   } finally { | ||||
|     state.loading = false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get the current state of the WebAssembly module | ||||
|  * @returns {{loading: boolean, initialized: boolean, error: string|null}} | ||||
|  */ | ||||
| export function getWasmState() { | ||||
|   return { ...state }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get the WebAssembly module | ||||
|  * @returns {object|null} The WebAssembly module or null if not loaded | ||||
|  */ | ||||
| export function getWasmModule() { | ||||
|   return wasmModule; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Debug function to check the vault state | ||||
|  * @returns {Promise<object>} State information | ||||
|  */ | ||||
| export async function debugVaultState() { | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log('🔍 Debugging vault state...'); | ||||
|      | ||||
|     // Check if we have a valid session using Rhai script | ||||
|     const sessionCheck = ` | ||||
|       let has_session = vault::has_active_session(); | ||||
|       let keyspace = ""; | ||||
|       if has_session { | ||||
|           keyspace = vault::get_current_keyspace(); | ||||
|       } | ||||
|        | ||||
|       // Return info about the session | ||||
|       { | ||||
|           "has_session": has_session, | ||||
|           "keyspace": keyspace | ||||
|       } | ||||
|     `; | ||||
|      | ||||
|     console.log('Checking session status...'); | ||||
|     const sessionStatus = await module.run_rhai(sessionCheck); | ||||
|     console.log('Session status:', sessionStatus); | ||||
|      | ||||
|     // Get keypair info if we have a session | ||||
|     if (sessionStatus && sessionStatus.has_session) { | ||||
|       const keypairsScript = ` | ||||
|         // Get all keypairs for the current keyspace | ||||
|         let keypairs = vault::list_keypairs(); | ||||
|          | ||||
|         // Add diagnostic information | ||||
|         let diagnostic = { | ||||
|           "keypair_count": keypairs.len(), | ||||
|           "keyspace": vault::get_current_keyspace(), | ||||
|           "keypairs": keypairs | ||||
|         }; | ||||
|          | ||||
|         diagnostic | ||||
|       `; | ||||
|        | ||||
|       console.log('Fetching keypair details...'); | ||||
|       const keypairDiagnostic = await module.run_rhai(keypairsScript); | ||||
|       console.log('Keypair diagnostic:', keypairDiagnostic); | ||||
|        | ||||
|       return keypairDiagnostic; | ||||
|     } | ||||
|      | ||||
|     return sessionStatus; | ||||
|   } catch (error) { | ||||
|     console.error('Error in debug function:', error); | ||||
|     return { error: error.toString() }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get keypairs from the vault | ||||
|  * @returns {Promise<Array>} List of keypairs | ||||
|  */ | ||||
| export async function getKeypairsFromVault() { | ||||
|   console.log('==============================================='); | ||||
|   console.log('Starting getKeypairsFromVault...'); | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     console.error('WebAssembly module not loaded!'); | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|   console.log('WebAssembly module:', module); | ||||
|   console.log('Module functions available:', Object.keys(module)); | ||||
|    | ||||
|   // Check if IndexedDB is available and working | ||||
|   const isIndexedDBAvailable = await checkIndexedDBAvailability(); | ||||
|   if (!isIndexedDBAvailable) { | ||||
|     console.warn('IndexedDB is not available or not working properly'); | ||||
|     // We'll continue, but this is likely why keypairs aren't persisting | ||||
|   } | ||||
|    | ||||
|   // Force re-initialization of the current session if needed | ||||
|   try { | ||||
|     // This checks if we have the debug function available | ||||
|     if (typeof module.list_keypairs_debug === 'function') { | ||||
|       console.log('Using debug function to diagnose keypair loading issues...'); | ||||
|       const debugResult = await module.list_keypairs_debug(); | ||||
|       console.log('Debug keypair listing result:', debugResult); | ||||
|       if (Array.isArray(debugResult) && debugResult.length > 0) { | ||||
|         console.log('Debug function returned keypairs:', debugResult); | ||||
|         // If debug function worked but regular function doesn't, use its result | ||||
|         return debugResult; | ||||
|       } else { | ||||
|         console.log('Debug function did not return keypairs, continuing with normal flow...'); | ||||
|       } | ||||
|     } | ||||
|   } catch (err) { | ||||
|     console.error('Error in debug function:', err); | ||||
|     // Continue with normal flow even if the debug function fails | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log('-----------------------------------------------'); | ||||
|     console.log('Running diagnostics to check vault state...'); | ||||
|     // Run diagnostic first to log vault state | ||||
|     await debugVaultState(); | ||||
|     console.log('Diagnostics complete'); | ||||
|     console.log('-----------------------------------------------'); | ||||
|      | ||||
|     console.log('Checking if list_keypairs function is available:', typeof module.list_keypairs); | ||||
|     for (const key in module) { | ||||
|       console.log(`Module function: ${key} = ${typeof module[key]}`); | ||||
|     } | ||||
|     if (typeof module.list_keypairs !== 'function') { | ||||
|       console.error('list_keypairs function is not available in the WebAssembly module!'); | ||||
|       console.log('Available functions:', Object.keys(module)); | ||||
|       // Fall back to Rhai script | ||||
|       console.log('Falling back to using Rhai script for listing keypairs...'); | ||||
|       const script = ` | ||||
|         // Get all keypairs from the current keyspace | ||||
|         let keypairs = vault::list_keypairs(); | ||||
|         keypairs | ||||
|       `; | ||||
|       const keypairList = await module.run_rhai(script); | ||||
|       console.log('Retrieved keypairs from vault using Rhai:', keypairList); | ||||
|       return keypairList; | ||||
|     } | ||||
|      | ||||
|     console.log('Calling WebAssembly list_keypairs function...'); | ||||
|     // Use the direct list_keypairs function from WebAssembly instead of Rhai script | ||||
|     const keypairList = await module.list_keypairs(); | ||||
|     console.log('Retrieved keypairs from vault:', keypairList); | ||||
|      | ||||
|     console.log('Raw keypair list type:', typeof keypairList); | ||||
|     console.log('Is array?', Array.isArray(keypairList)); | ||||
|     console.log('Raw keypair list:', keypairList); | ||||
|      | ||||
|     // Format keypairs for UI | ||||
|     const formattedKeypairs = Array.isArray(keypairList) ? keypairList.map(kp => { | ||||
|       // Parse metadata if available | ||||
|       let metadata = {}; | ||||
|       if (kp.metadata) { | ||||
|         try { | ||||
|           if (typeof kp.metadata === 'string') { | ||||
|             metadata = JSON.parse(kp.metadata); | ||||
|           } else { | ||||
|             metadata = kp.metadata; | ||||
|           } | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse keypair metadata:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return { | ||||
|         id: kp.id, | ||||
|         label: metadata.label || `Key-${kp.id.substring(0, 4)}` | ||||
|       }; | ||||
|     }) : []; | ||||
|      | ||||
|     console.log('Formatted keypairs for UI:', formattedKeypairs); | ||||
|      | ||||
|     // Update background service worker | ||||
|     return new Promise((resolve) => { | ||||
|       chrome.runtime.sendMessage({ | ||||
|         action: 'update_session', | ||||
|         type: 'keypairs_loaded', | ||||
|         data: formattedKeypairs | ||||
|       }, (response) => { | ||||
|         console.log('Background response to keypairs update:', response); | ||||
|         resolve(formattedKeypairs); | ||||
|       }); | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     console.error('Error fetching keypairs from vault:', error); | ||||
|     return []; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if IndexedDB is available and working | ||||
|  * @returns {Promise<boolean>} True if IndexedDB is working | ||||
|  */ | ||||
| export async function checkIndexedDBAvailability() { | ||||
|   console.log('Checking IndexedDB availability...'); | ||||
|    | ||||
|   // First check if IndexedDB is available in the browser | ||||
|   if (!window.indexedDB) { | ||||
|     console.error('IndexedDB is not available in this browser'); | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   const module = getWasmModule(); | ||||
|   if (!module || typeof module.check_indexeddb !== 'function') { | ||||
|     console.error('WebAssembly module or check_indexeddb function not available'); | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     const result = await module.check_indexeddb(); | ||||
|     console.log('IndexedDB check result:', result); | ||||
|     return true; | ||||
|   } catch (error) { | ||||
|     console.error('IndexedDB check failed:', error); | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Initialize a session with the given keyspace and password | ||||
|  * @param {string} keyspace  | ||||
|  * @param {string} password  | ||||
|  * @returns {Promise<Array>} List of keypairs after initialization | ||||
|  */ | ||||
| export async function initSession(keyspace, password) { | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log(`Initializing session for keyspace: ${keyspace}`); | ||||
|      | ||||
|     // Check if IndexedDB is working | ||||
|     const isIndexedDBAvailable = await checkIndexedDBAvailability(); | ||||
|     if (!isIndexedDBAvailable) { | ||||
|       console.warn('IndexedDB is not available or not working properly. Keypairs might not persist.'); | ||||
|       // Continue anyway as we might fall back to memory storage | ||||
|     } | ||||
|      | ||||
|     // Initialize the session using the WASM module | ||||
|     await module.init_session(keyspace, password); | ||||
|     console.log('Session initialized successfully'); | ||||
|      | ||||
|     // Check if we have stored keypairs for this keyspace in Chrome storage | ||||
|     const storedKeypairs = await new Promise(resolve => { | ||||
|       chrome.storage.local.get([`keypairs:${keyspace}`], result => { | ||||
|         resolve(result[`keypairs:${keyspace}`] || []); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     console.log(`Found ${storedKeypairs.length} stored keypairs for keyspace ${keyspace}`); | ||||
|      | ||||
|     // Import stored keypairs into the WebAssembly session if they don't exist already | ||||
|     if (storedKeypairs.length > 0) { | ||||
|       console.log('Importing stored keypairs into WebAssembly session...'); | ||||
|        | ||||
|       // First get current keypairs from the vault directly | ||||
|       const wasmKeypairs = await module.list_keypairs(); | ||||
|       console.log('Current keypairs in WebAssembly vault:', wasmKeypairs); | ||||
|        | ||||
|       // Get the IDs of existing keypairs in the vault | ||||
|       const existingIds = new Set(wasmKeypairs.map(kp => kp.id)); | ||||
|        | ||||
|       // Import keypairs that don't already exist in the vault | ||||
|       for (const keypair of storedKeypairs) { | ||||
|         if (!existingIds.has(keypair.id)) { | ||||
|           console.log(`Importing keypair ${keypair.id} into WebAssembly vault...`); | ||||
|            | ||||
|           // Create metadata for the keypair | ||||
|           const metadata = JSON.stringify({ | ||||
|             label: keypair.label || `Key-${keypair.id.substring(0, 8)}`, | ||||
|             imported: true, | ||||
|             importDate: new Date().toISOString() | ||||
|           }); | ||||
|            | ||||
|           // For adding existing keypairs, we'd normally need the private key | ||||
|           // Since we can't retrieve it, we'll create a new one with the same label | ||||
|           // This is a placeholder - in a real implementation, you'd need to use the actual keys | ||||
|           try { | ||||
|             const keyType = keypair.type || 'Secp256k1'; | ||||
|             await module.add_keypair(keyType, metadata); | ||||
|             console.log(`Created keypair of type ${keyType} with label ${keypair.label}`); | ||||
|           } catch (err) { | ||||
|             console.warn(`Failed to import keypair ${keypair.id}:`, err); | ||||
|             // Continue with other keypairs even if one fails | ||||
|           } | ||||
|         } else { | ||||
|           console.log(`Keypair ${keypair.id} already exists in vault, skipping import`); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Initialize session using WASM (await the async function) | ||||
|     await module.init_session(keyspace, password); | ||||
|  | ||||
|     // Get keypairs from the vault after session is ready | ||||
|     const currentKeypairs = await getKeypairsFromVault(); | ||||
|  | ||||
|     // Update keypairs in background service worker | ||||
|     await new Promise(resolve => { | ||||
|       chrome.runtime.sendMessage({ | ||||
|         action: 'update_session', | ||||
|         type: 'keypairs_loaded', | ||||
|         data: currentKeypairs | ||||
|       }, response => { | ||||
|         console.log('Updated keypairs in background service worker'); | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     return currentKeypairs; | ||||
|   } catch (error) { | ||||
|     console.error('Failed to initialize session:', error); | ||||
|     throw error; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Lock the current session | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export async function lockSession() { | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log('Locking session...'); | ||||
|      | ||||
|     // First run diagnostics to see what we have before locking | ||||
|     await debugVaultState(); | ||||
|      | ||||
|     // Call the WASM lock_session function | ||||
|     module.lock_session(); | ||||
|     console.log('Session locked in WebAssembly module'); | ||||
|      | ||||
|     // Update session state in background | ||||
|     await new Promise((resolve, reject) => { | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'session_locked' | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           console.log('Background service worker updated for locked session'); | ||||
|           resolve(); | ||||
|         } else { | ||||
|           console.error('Failed to update session state in background:', response?.error); | ||||
|           reject(new Error(response?.error || 'Failed to update session state')); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Verify session is locked properly | ||||
|     const sessionStatus = await debugVaultState(); | ||||
|     console.log('Session status after locking:', sessionStatus); | ||||
|   } catch (error) { | ||||
|     console.error('Error locking session:', error); | ||||
|     throw error; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Add a new keypair | ||||
|  * @param {string} keyType The type of key to create (default: 'Secp256k1') | ||||
|  * @param {string} label Optional custom label for the keypair | ||||
|  * @returns {Promise<{id: string, label: string}>} The created keypair info | ||||
|  */ | ||||
| export async function addKeypair(keyType = 'Secp256k1', label = null) { | ||||
|   const module = getWasmModule(); | ||||
|   if (!module) { | ||||
|     throw new Error('WebAssembly module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     // Get current keyspace | ||||
|     const sessionState = await getSessionState(); | ||||
|     const keyspace = sessionState.currentKeyspace; | ||||
|     if (!keyspace) { | ||||
|       throw new Error('No active keyspace'); | ||||
|     } | ||||
|      | ||||
|     // Generate default label if not provided | ||||
|     const keyLabel = label || `${keyType}-Key-${Date.now().toString(16).slice(-4)}`; | ||||
|      | ||||
|     // Create metadata JSON | ||||
|     const metadata = JSON.stringify({ | ||||
|       label: keyLabel, | ||||
|       created: new Date().toISOString(), | ||||
|       type: keyType | ||||
|     }); | ||||
|      | ||||
|     console.log(`Adding new keypair of type ${keyType} with label ${keyLabel}`); | ||||
|     console.log('Keypair metadata:', metadata); | ||||
|      | ||||
|     // Call the WASM add_keypair function with metadata | ||||
|     // This will add the keypair to the WebAssembly vault | ||||
|     const keyId = await module.add_keypair(keyType, metadata); | ||||
|     console.log(`Keypair created with ID: ${keyId} in WebAssembly vault`); | ||||
|      | ||||
|     // Create keypair object for UI and storage | ||||
|     const newKeypair = {  | ||||
|       id: keyId,  | ||||
|       label: keyLabel, | ||||
|       type: keyType, | ||||
|       created: new Date().toISOString() | ||||
|     }; | ||||
|      | ||||
|     // Get the latest keypairs from the WebAssembly vault to ensure consistency | ||||
|     const vaultKeypairs = await module.list_keypairs(); | ||||
|     console.log('Current keypairs in vault after addition:', vaultKeypairs); | ||||
|      | ||||
|     // Format the vault keypairs for storage | ||||
|     const formattedVaultKeypairs = vaultKeypairs.map(kp => { | ||||
|       // Parse metadata if available | ||||
|       let metadata = {}; | ||||
|       if (kp.metadata) { | ||||
|         try { | ||||
|           if (typeof kp.metadata === 'string') { | ||||
|             metadata = JSON.parse(kp.metadata); | ||||
|           } else { | ||||
|             metadata = kp.metadata; | ||||
|           } | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse keypair metadata:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return { | ||||
|         id: kp.id, | ||||
|         label: metadata.label || `Key-${kp.id.substring(0, 8)}`, | ||||
|         type: kp.type || 'Secp256k1', | ||||
|         created: metadata.created || new Date().toISOString() | ||||
|       }; | ||||
|     }); | ||||
|      | ||||
|     // Save the formatted keypairs to Chrome storage | ||||
|     await new Promise(resolve => { | ||||
|       chrome.storage.local.set({ [`keypairs:${keyspace}`]: formattedVaultKeypairs }, () => { | ||||
|         console.log(`Saved ${formattedVaultKeypairs.length} keypairs to Chrome storage for keyspace ${keyspace}`); | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Update session state in background with the new keypair information | ||||
|     await new Promise((resolve, reject) => { | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keypair_added',  | ||||
|         data: newKeypair  | ||||
|       }, async (response) => { | ||||
|         if (response && response.success) { | ||||
|           console.log('Background service worker updated with new keypair'); | ||||
|           resolve(newKeypair); | ||||
|         } else { | ||||
|           const error = response?.error || 'Failed to update session state'; | ||||
|           console.error('Error updating background state:', error); | ||||
|           reject(new Error(error)); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Also update the complete keypair list in background with the current vault state | ||||
|     await new Promise(resolve => { | ||||
|       chrome.runtime.sendMessage({ | ||||
|         action: 'update_session', | ||||
|         type: 'keypairs_loaded', | ||||
|         data: formattedVaultKeypairs | ||||
|       }, () => { | ||||
|         console.log('Updated complete keypair list in background with vault state'); | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     return newKeypair; | ||||
|   } catch (error) { | ||||
|     console.error('Error adding keypair:', error); | ||||
|     throw error; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Select a keypair | ||||
|  * @param {string} keyId The ID of the keypair to select | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export async function selectKeypair(keyId) { | ||||
|   if (!wasmModule || !wasmModule.select_keypair) { | ||||
|     throw new Error('WASM module not loaded'); | ||||
|   } | ||||
|    | ||||
|   // Call the WASM select_keypair function | ||||
|   await wasmModule.select_keypair(keyId); | ||||
|    | ||||
|   // Update session state in background | ||||
|   await new Promise((resolve, reject) => { | ||||
|     chrome.runtime.sendMessage({  | ||||
|       action: 'update_session',  | ||||
|       type: 'keypair_selected',  | ||||
|       data: keyId | ||||
|     }, (response) => { | ||||
|       if (response && response.success) { | ||||
|         resolve(); | ||||
|       } else { | ||||
|         reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Sign a message with the selected keypair | ||||
|  * @param {string} message The message to sign | ||||
|  * @returns {Promise<string>} The signature as a hex string | ||||
|  */ | ||||
| export async function sign(message) { | ||||
|   if (!wasmModule || !wasmModule.sign) { | ||||
|     throw new Error('WASM module not loaded'); | ||||
|   } | ||||
|    | ||||
|   // Convert message to Uint8Array | ||||
|   const encoder = new TextEncoder(); | ||||
|   const messageBytes = encoder.encode(message); | ||||
|    | ||||
|   // Call the WASM sign function | ||||
|   return await wasmModule.sign(messageBytes); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get the current session state | ||||
|  * @returns {Promise<{currentKeyspace: string|null, keypairs: Array, selectedKeypair: string|null}>} | ||||
|  */ | ||||
| export async function getSessionState() { | ||||
|   return new Promise((resolve) => { | ||||
|     chrome.runtime.sendMessage({ action: 'get_session' }, (response) => { | ||||
|       resolve(response || { currentKeyspace: null, keypairs: [], selectedKeypair: null }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										88
									
								
								extension/popup/WasmLoader.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,88 @@ | ||||
| import React, { useState, useEffect, createContext, useContext } from 'react'; | ||||
|  | ||||
| // Create a context to share the WASM module across components | ||||
| export const WasmContext = createContext(null); | ||||
|  | ||||
| // Hook to access WASM module | ||||
| export function useWasm() { | ||||
|   return useContext(WasmContext); | ||||
| } | ||||
|  | ||||
| // Component that loads and initializes the WASM module | ||||
| export function WasmProvider({ children }) { | ||||
|   const [wasmModule, setWasmModule] = useState(null); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   const [error, setError] = useState(null); | ||||
|    | ||||
|   useEffect(() => { | ||||
|     async function loadWasm() { | ||||
|       try { | ||||
|         setLoading(true); | ||||
|          | ||||
|         // Instead of using dynamic imports which require correct MIME types, | ||||
|         // we'll use fetch to load the JavaScript file as text and eval it | ||||
|         const wasmJsPath = chrome.runtime.getURL('wasm/wasm_app.js'); | ||||
|         console.log('Loading WASM JS from:', wasmJsPath); | ||||
|          | ||||
|         // Load the JavaScript file | ||||
|         const jsResponse = await fetch(wasmJsPath); | ||||
|         if (!jsResponse.ok) { | ||||
|           throw new Error(`Failed to load WASM JS: ${jsResponse.status} ${jsResponse.statusText}`); | ||||
|         } | ||||
|          | ||||
|         // Get the JavaScript code as text | ||||
|         const jsCode = await jsResponse.text(); | ||||
|          | ||||
|         // Create a function to execute the code in an isolated scope | ||||
|         let wasmModuleExports = {}; | ||||
|         const moduleFunction = new Function('exports', jsCode + '\nreturn { initSync, default: __wbg_init, init_rhai_env, init_session, lock_session, add_keypair, select_keypair, sign, run_rhai };'); | ||||
|          | ||||
|         // Execute the function to get the exports | ||||
|         const wasmModule = moduleFunction(wasmModuleExports); | ||||
|          | ||||
|         // Initialize WASM with the binary | ||||
|         const wasmBinaryPath = chrome.runtime.getURL('wasm/wasm_app_bg.wasm'); | ||||
|         console.log('Initializing WASM with binary:', wasmBinaryPath); | ||||
|          | ||||
|         const binaryResponse = await fetch(wasmBinaryPath); | ||||
|         if (!binaryResponse.ok) { | ||||
|           throw new Error(`Failed to load WASM binary: ${binaryResponse.status} ${binaryResponse.statusText}`); | ||||
|         } | ||||
|          | ||||
|         const wasmBinary = await binaryResponse.arrayBuffer(); | ||||
|          | ||||
|         // Initialize the WASM module | ||||
|         await wasmModule.default(wasmBinary); | ||||
|          | ||||
|         // Initialize the WASM environment | ||||
|         if (typeof wasmModule.init_rhai_env === 'function') { | ||||
|           wasmModule.init_rhai_env(); | ||||
|         } | ||||
|          | ||||
|         console.log('WASM module loaded successfully'); | ||||
|         setWasmModule(wasmModule); | ||||
|         setLoading(false); | ||||
|       } catch (error) { | ||||
|         console.error('Failed to load WASM module:', error); | ||||
|         setError(error.message || 'Failed to load WebAssembly module'); | ||||
|         setLoading(false); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     loadWasm(); | ||||
|   }, []); | ||||
|    | ||||
|   if (loading) { | ||||
|     return <div className="wasm-loading">Loading WebAssembly module...</div>; | ||||
|   } | ||||
|    | ||||
|   if (error) { | ||||
|     return <div className="wasm-error">Error: {error}</div>; | ||||
|   } | ||||
|    | ||||
|   return ( | ||||
|     <WasmContext.Provider value={wasmModule}> | ||||
|       {children} | ||||
|     </WasmContext.Provider> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										88
									
								
								extension/popup/debug_rhai.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,88 @@ | ||||
| /** | ||||
|  * Debug helper for WebAssembly Vault with Rhai scripts | ||||
|  */ | ||||
|  | ||||
| // Helper to try various Rhai scripts for debugging | ||||
| export const RHAI_SCRIPTS = { | ||||
|   // Check if there's an active session | ||||
|   CHECK_SESSION: ` | ||||
|     let has_session = false; | ||||
|     let current_keyspace = ""; | ||||
|      | ||||
|     // Try to access functions expected to exist in the vault namespace | ||||
|     if (isdef(vault) && isdef(vault::has_active_session)) { | ||||
|       has_session = vault::has_active_session(); | ||||
|       if (has_session && isdef(vault::get_current_keyspace)) { | ||||
|         current_keyspace = vault::get_current_keyspace(); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     { | ||||
|       "has_session": has_session, | ||||
|       "keyspace": current_keyspace, | ||||
|       "available_functions": [ | ||||
|         isdef(vault::list_keypairs) ? "list_keypairs" : null, | ||||
|         isdef(vault::add_keypair) ? "add_keypair" : null, | ||||
|         isdef(vault::has_active_session) ? "has_active_session" : null, | ||||
|         isdef(vault::get_current_keyspace) ? "get_current_keyspace" : null | ||||
|       ] | ||||
|     } | ||||
|   `, | ||||
|    | ||||
|   // Explicitly get keypairs for the current keyspace using session data | ||||
|   LIST_KEYPAIRS: ` | ||||
|     let result = {"error": "Not initialized"}; | ||||
|      | ||||
|     if (isdef(vault) && isdef(vault::has_active_session) && vault::has_active_session()) { | ||||
|       let keyspace = vault::get_current_keyspace(); | ||||
|        | ||||
|       // Try to list the keypairs from the current session | ||||
|       if (isdef(vault::get_keypairs_from_session)) { | ||||
|         result = { | ||||
|           "keyspace": keyspace, | ||||
|           "keypairs": vault::get_keypairs_from_session() | ||||
|         }; | ||||
|       } else { | ||||
|         result = { | ||||
|           "error": "vault::get_keypairs_from_session is not defined", | ||||
|           "keyspace": keyspace | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     result | ||||
|   `, | ||||
|    | ||||
|   // Use Rhai to inspect the Vault storage directly (for advanced debugging) | ||||
|   INSPECT_VAULT_STORAGE: ` | ||||
|     let result = {"error": "Not accessible"}; | ||||
|      | ||||
|     if (isdef(vault) && isdef(vault::inspect_storage)) { | ||||
|       result = vault::inspect_storage(); | ||||
|     } | ||||
|      | ||||
|     result | ||||
|   ` | ||||
| }; | ||||
|  | ||||
| // Run all debug scripts and collect results | ||||
| export async function runDiagnostics(wasmModule) { | ||||
|   if (!wasmModule || !wasmModule.run_rhai) { | ||||
|     throw new Error('WebAssembly module not loaded or run_rhai not available'); | ||||
|   } | ||||
|  | ||||
|   const results = {}; | ||||
|    | ||||
|   for (const [name, script] of Object.entries(RHAI_SCRIPTS)) { | ||||
|     try { | ||||
|       console.log(`Running Rhai diagnostic script: ${name}`); | ||||
|       results[name] = await wasmModule.run_rhai(script); | ||||
|       console.log(`Result from ${name}:`, results[name]); | ||||
|     } catch (error) { | ||||
|       console.error(`Error running script ${name}:`, error); | ||||
|       results[name] = { error: error.toString() }; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   return results; | ||||
| } | ||||
							
								
								
									
										13
									
								
								extension/popup/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Modular Vault Extension</title> | ||||
|     <link rel="stylesheet" href="popup.css"> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="root"></div> | ||||
|     <script src="popup.js"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										8
									
								
								extension/popup/index.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| import React from 'react'; | ||||
| import { createRoot } from 'react-dom/client'; | ||||
| import App from './App'; | ||||
| import './style.css'; | ||||
|  | ||||
| // Render the React app | ||||
| const root = createRoot(document.getElementById('root')); | ||||
| root.render(<App />); | ||||
							
								
								
									
										117
									
								
								extension/popup/popup.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,117 @@ | ||||
| /* Basic styles for the extension popup */ | ||||
| body { | ||||
|   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   background-color: #202124; | ||||
|   color: #e8eaed; | ||||
| } | ||||
|  | ||||
| .container { | ||||
|   width: 350px; | ||||
|   padding: 15px; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   font-size: 18px; | ||||
|   margin: 0 0 15px 0; | ||||
|   border-bottom: 1px solid #3c4043; | ||||
|   padding-bottom: 10px; | ||||
| } | ||||
|  | ||||
| h2 { | ||||
|   font-size: 16px; | ||||
|   margin: 10px 0; | ||||
| } | ||||
|  | ||||
| .form-section { | ||||
|   margin-bottom: 20px; | ||||
|   background-color: #292a2d; | ||||
|   border-radius: 8px; | ||||
|   padding: 15px; | ||||
| } | ||||
|  | ||||
| .form-group { | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| label { | ||||
|   display: block; | ||||
|   margin-bottom: 5px; | ||||
|   font-size: 13px; | ||||
|   color: #9aa0a6; | ||||
| } | ||||
|  | ||||
| input, textarea { | ||||
|   width: 100%; | ||||
|   padding: 8px; | ||||
|   border: 1px solid #3c4043; | ||||
|   border-radius: 4px; | ||||
|   background-color: #202124; | ||||
|   color: #e8eaed; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| textarea { | ||||
|   min-height: 60px; | ||||
|   resize: vertical; | ||||
| } | ||||
|  | ||||
| button { | ||||
|   background-color: #8ab4f8; | ||||
|   color: #202124; | ||||
|   border: none; | ||||
|   border-radius: 4px; | ||||
|   padding: 8px 16px; | ||||
|   font-weight: 500; | ||||
|   cursor: pointer; | ||||
|   transition: background-color 0.3s; | ||||
| } | ||||
|  | ||||
| button:hover { | ||||
|   background-color: #669df6; | ||||
| } | ||||
|  | ||||
| button.small { | ||||
|   padding: 4px 8px; | ||||
|   font-size: 12px; | ||||
| } | ||||
|  | ||||
| .button-group { | ||||
|   display: flex; | ||||
|   gap: 10px; | ||||
| } | ||||
|  | ||||
| .status { | ||||
|   margin: 10px 0; | ||||
|   padding: 8px; | ||||
|   background-color: #292a2d; | ||||
|   border-radius: 4px; | ||||
|   font-size: 13px; | ||||
| } | ||||
|  | ||||
| .list { | ||||
|   margin-top: 10px; | ||||
|   max-height: 150px; | ||||
|   overflow-y: auto; | ||||
| } | ||||
|  | ||||
| .list-item { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   padding: 8px; | ||||
|   border-bottom: 1px solid #3c4043; | ||||
| } | ||||
|  | ||||
| .list-item.selected { | ||||
|   background-color: rgba(138, 180, 248, 0.1); | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
|   display: none; | ||||
| } | ||||
|  | ||||
| .session-info { | ||||
|   margin-top: 15px; | ||||
| } | ||||
							
								
								
									
										306
									
								
								extension/popup/popup.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,306 @@ | ||||
| // Simple non-module JavaScript for browser extension popup | ||||
| document.addEventListener('DOMContentLoaded', async function() { | ||||
|   const root = document.getElementById('root'); | ||||
|   root.innerHTML = ` | ||||
|     <div class="container"> | ||||
|       <h1>Modular Vault Extension</h1> | ||||
|       <div id="status" class="status">Loading WASM module...</div> | ||||
|        | ||||
|       <div id="session-controls"> | ||||
|         <div id="keyspace-form" class="form-section"> | ||||
|           <h2>Session</h2> | ||||
|           <div class="form-group"> | ||||
|             <label for="keyspace">Keyspace:</label> | ||||
|             <input type="text" id="keyspace" placeholder="Enter keyspace name"> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label for="password">Password:</label> | ||||
|             <input type="password" id="password" placeholder="Enter password"> | ||||
|           </div> | ||||
|           <div class="button-group"> | ||||
|             <button id="unlock-btn">Unlock</button> | ||||
|             <button id="create-btn">Create New</button> | ||||
|           </div> | ||||
|         </div> | ||||
|          | ||||
|         <div id="session-info" class="session-info hidden"> | ||||
|           <h2>Active Session</h2> | ||||
|           <p>Current keyspace: <span id="current-keyspace"></span></p> | ||||
|           <button id="lock-btn">Lock Session</button> | ||||
|            | ||||
|           <div id="keypair-section" class="form-section"> | ||||
|             <h2>Keypairs</h2> | ||||
|             <button id="create-keypair-btn">Create New Keypair</button> | ||||
|             <div id="keypair-list" class="list"></div> | ||||
|           </div> | ||||
|            | ||||
|           <div id="sign-section" class="form-section hidden"> | ||||
|             <h2>Sign Message</h2> | ||||
|             <div class="form-group"> | ||||
|               <label for="message">Message:</label> | ||||
|               <textarea id="message" placeholder="Enter message to sign"></textarea> | ||||
|             </div> | ||||
|             <button id="sign-btn">Sign</button> | ||||
|             <div class="form-group"> | ||||
|               <label for="signature">Signature:</label> | ||||
|               <textarea id="signature" readonly></textarea> | ||||
|               <button id="copy-btn" class="small">Copy</button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   `; | ||||
|    | ||||
|   // DOM elements | ||||
|   const statusEl = document.getElementById('status'); | ||||
|   const keyspaceFormEl = document.getElementById('keyspace-form'); | ||||
|   const sessionInfoEl = document.getElementById('session-info'); | ||||
|   const currentKeyspaceEl = document.getElementById('current-keyspace'); | ||||
|   const keyspaceInput = document.getElementById('keyspace'); | ||||
|   const passwordInput = document.getElementById('password'); | ||||
|   const unlockBtn = document.getElementById('unlock-btn'); | ||||
|   const createBtn = document.getElementById('create-btn'); | ||||
|   const lockBtn = document.getElementById('lock-btn'); | ||||
|   const createKeypairBtn = document.getElementById('create-keypair-btn'); | ||||
|   const keypairListEl = document.getElementById('keypair-list'); | ||||
|   const signSectionEl = document.getElementById('sign-section'); | ||||
|   const messageInput = document.getElementById('message'); | ||||
|   const signBtn = document.getElementById('sign-btn'); | ||||
|   const signatureOutput = document.getElementById('signature'); | ||||
|   const copyBtn = document.getElementById('copy-btn'); | ||||
|    | ||||
|   // State | ||||
|   let wasmModule = null; | ||||
|   let currentKeyspace = null; | ||||
|   let keypairs = []; | ||||
|   let selectedKeypairId = null; | ||||
|    | ||||
|   // Initialize | ||||
|   init(); | ||||
|    | ||||
|   async function init() { | ||||
|     try { | ||||
|       // Get session state from background | ||||
|       const sessionState = await getSessionState(); | ||||
|        | ||||
|       if (sessionState.currentKeyspace) { | ||||
|         // We have an active session | ||||
|         currentKeyspace = sessionState.currentKeyspace; | ||||
|         keypairs = sessionState.keypairs || []; | ||||
|         selectedKeypairId = sessionState.selectedKeypair; | ||||
|          | ||||
|         updateUI(); | ||||
|       } | ||||
|        | ||||
|       statusEl.textContent = 'Ready'; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   function updateUI() { | ||||
|     if (currentKeyspace) { | ||||
|       // Show session info | ||||
|       keyspaceFormEl.classList.add('hidden'); | ||||
|       sessionInfoEl.classList.remove('hidden'); | ||||
|       currentKeyspaceEl.textContent = currentKeyspace; | ||||
|        | ||||
|       // Update keypair list | ||||
|       updateKeypairList(); | ||||
|        | ||||
|       // Show/hide sign section based on selected keypair | ||||
|       if (selectedKeypairId) { | ||||
|         signSectionEl.classList.remove('hidden'); | ||||
|       } else { | ||||
|         signSectionEl.classList.add('hidden'); | ||||
|       } | ||||
|     } else { | ||||
|       // Show keyspace form | ||||
|       keyspaceFormEl.classList.remove('hidden'); | ||||
|       sessionInfoEl.classList.add('hidden'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   function updateKeypairList() { | ||||
|     // Clear list | ||||
|     keypairListEl.innerHTML = ''; | ||||
|      | ||||
|     // Add each keypair | ||||
|     keypairs.forEach(keypair => { | ||||
|       const item = document.createElement('div'); | ||||
|       item.className = 'list-item' + (selectedKeypairId === keypair.id ? ' selected' : ''); | ||||
|       item.innerHTML = ` | ||||
|         <span>${keypair.label || keypair.id}</span> | ||||
|         <button class="select-btn" data-id="${keypair.id}">Select</button> | ||||
|       `; | ||||
|       keypairListEl.appendChild(item); | ||||
|        | ||||
|       // Add select handler | ||||
|       item.querySelector('.select-btn').addEventListener('click', async () => { | ||||
|         try { | ||||
|           statusEl.textContent = 'Selecting keypair...'; | ||||
|           // Use background service to select keypair for now | ||||
|           await chrome.runtime.sendMessage({  | ||||
|             action: 'update_session',  | ||||
|             type: 'keypair_selected',  | ||||
|             data: keypair.id  | ||||
|           }); | ||||
|           selectedKeypairId = keypair.id; | ||||
|           updateUI(); | ||||
|           statusEl.textContent = 'Keypair selected: ' + keypair.id; | ||||
|         } catch (error) { | ||||
|           statusEl.textContent = 'Error selecting keypair: ' + (error.message || 'Unknown error'); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   // Get session state from background | ||||
|   async function getSessionState() { | ||||
|     return new Promise((resolve) => { | ||||
|       chrome.runtime.sendMessage({ action: 'get_session' }, (response) => { | ||||
|         resolve(response || { currentKeyspace: null, keypairs: [], selectedKeypair: null }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   // Event handlers | ||||
|   unlockBtn.addEventListener('click', async () => { | ||||
|     const keyspace = keyspaceInput.value.trim(); | ||||
|     const password = passwordInput.value; | ||||
|      | ||||
|     if (!keyspace || !password) { | ||||
|       statusEl.textContent = 'Please enter keyspace and password'; | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     statusEl.textContent = 'Unlocking session...'; | ||||
|      | ||||
|     try { | ||||
|       // For now, use the background service worker mock | ||||
|       await chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keyspace',  | ||||
|         data: keyspace  | ||||
|       }); | ||||
|        | ||||
|       currentKeyspace = keyspace; | ||||
|       updateUI(); | ||||
|       statusEl.textContent = 'Session unlocked!'; | ||||
|        | ||||
|       // Refresh state | ||||
|       const state = await getSessionState(); | ||||
|       keypairs = state.keypairs || []; | ||||
|       selectedKeypairId = state.selectedKeypair; | ||||
|       updateUI(); | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error unlocking session: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   createBtn.addEventListener('click', async () => { | ||||
|     const keyspace = keyspaceInput.value.trim(); | ||||
|     const password = passwordInput.value; | ||||
|      | ||||
|     if (!keyspace || !password) { | ||||
|       statusEl.textContent = 'Please enter keyspace and password'; | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     statusEl.textContent = 'Creating keyspace...'; | ||||
|      | ||||
|     try { | ||||
|       // For now, use the background service worker mock | ||||
|       await chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keyspace',  | ||||
|         data: keyspace  | ||||
|       }); | ||||
|        | ||||
|       currentKeyspace = keyspace; | ||||
|       updateUI(); | ||||
|       statusEl.textContent = 'Keyspace created and unlocked!'; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error creating keyspace: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   lockBtn.addEventListener('click', async () => { | ||||
|     statusEl.textContent = 'Locking session...'; | ||||
|      | ||||
|     try { | ||||
|       await chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'session_locked' | ||||
|       }); | ||||
|        | ||||
|       currentKeyspace = null; | ||||
|       keypairs = []; | ||||
|       selectedKeypairId = null; | ||||
|       updateUI(); | ||||
|       statusEl.textContent = 'Session locked'; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error locking session: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   createKeypairBtn.addEventListener('click', async () => { | ||||
|     statusEl.textContent = 'Creating keypair...'; | ||||
|      | ||||
|     try { | ||||
|       // Generate a mock keypair ID | ||||
|       const keyId = 'key-' + Date.now().toString(16); | ||||
|       const newKeypair = {  | ||||
|         id: keyId,  | ||||
|         label: `Secp256k1-Key-${keypairs.length + 1}`  | ||||
|       }; | ||||
|        | ||||
|       await chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keypair_added',  | ||||
|         data: newKeypair | ||||
|       }); | ||||
|        | ||||
|       // Refresh state | ||||
|       const state = await getSessionState(); | ||||
|       keypairs = state.keypairs || []; | ||||
|       updateUI(); | ||||
|        | ||||
|       statusEl.textContent = 'Keypair created: ' + keyId; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error creating keypair: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   signBtn.addEventListener('click', async () => { | ||||
|     const message = messageInput.value.trim(); | ||||
|      | ||||
|     if (!message) { | ||||
|       statusEl.textContent = 'Please enter a message to sign'; | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     if (!selectedKeypairId) { | ||||
|       statusEl.textContent = 'Please select a keypair first'; | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     statusEl.textContent = 'Signing message...'; | ||||
|      | ||||
|     try { | ||||
|       // For now, generate a mock signature | ||||
|       const mockSignature = Array.from({length: 64}, () => Math.floor(Math.random() * 16).toString(16)).join(''); | ||||
|       signatureOutput.value = mockSignature; | ||||
|       statusEl.textContent = 'Message signed!'; | ||||
|     } catch (error) { | ||||
|       statusEl.textContent = 'Error signing message: ' + (error.message || 'Unknown error'); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   copyBtn.addEventListener('click', () => { | ||||
|     signatureOutput.select(); | ||||
|     document.execCommand('copy'); | ||||
|     statusEl.textContent = 'Signature copied to clipboard!'; | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										26
									
								
								extension/popup/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,26 @@ | ||||
| body { | ||||
|   margin: 0; | ||||
|   font-family: 'Inter', Arial, sans-serif; | ||||
|   background: #181c20; | ||||
|   color: #f3f6fa; | ||||
| } | ||||
|  | ||||
| .App { | ||||
|   padding: 1.5rem; | ||||
|   min-width: 320px; | ||||
|   max-width: 400px; | ||||
|   background: #23272e; | ||||
|   border-radius: 12px; | ||||
|   box-shadow: 0 4px 24px rgba(0,0,0,0.2); | ||||
| } | ||||
| h1 { | ||||
|   font-size: 1.5rem; | ||||
|   margin-bottom: 0.5rem; | ||||
| } | ||||
| p { | ||||
|   color: #b0bac9; | ||||
|   margin-bottom: 1.5rem; | ||||
| } | ||||
| .status { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
							
								
								
									
										317
									
								
								extension/popup/wasm.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,317 @@ | ||||
| // WebAssembly API functions for accessing WASM operations directly | ||||
| // and synchronizing state with background service worker | ||||
|  | ||||
| // Get session state from the background service worker | ||||
| export function getStatus() { | ||||
|   return new Promise((resolve) => { | ||||
|     chrome.runtime.sendMessage({ action: 'get_session' }, (response) => { | ||||
|       resolve(response); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Debug function to examine vault state using Rhai scripts | ||||
| export async function debugVaultState(wasmModule) { | ||||
|   if (!wasmModule) { | ||||
|     throw new Error('WASM module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     console.log('🔍 Debugging vault state...'); | ||||
|      | ||||
|     // First check if we have a valid session | ||||
|     const sessionCheck = ` | ||||
|       let has_session = vault::has_active_session(); | ||||
|       let keyspace = ""; | ||||
|       if has_session { | ||||
|           keyspace = vault::get_current_keyspace(); | ||||
|       } | ||||
|        | ||||
|       // Return info about the session | ||||
|       { | ||||
|           "has_session": has_session, | ||||
|           "keyspace": keyspace | ||||
|       } | ||||
|     `; | ||||
|      | ||||
|     console.log('Checking session status...'); | ||||
|     const sessionStatus = await wasmModule.run_rhai(sessionCheck); | ||||
|     console.log('Session status:', sessionStatus); | ||||
|      | ||||
|     // Only try to get keypairs if we have an active session | ||||
|     if (sessionStatus && sessionStatus.has_session) { | ||||
|       // Get information about all keypairs | ||||
|       const keypairsScript = ` | ||||
|         // Get all keypairs for the current keyspace | ||||
|         let keypairs = vault::list_keypairs(); | ||||
|          | ||||
|         // Add more diagnostic information | ||||
|         let diagnostic = { | ||||
|           "keypair_count": keypairs.len(), | ||||
|           "keyspace": vault::get_current_keyspace(), | ||||
|           "keypairs": keypairs | ||||
|         }; | ||||
|          | ||||
|         diagnostic | ||||
|       `; | ||||
|        | ||||
|       console.log('Fetching keypair details...'); | ||||
|       const keypairDiagnostic = await wasmModule.run_rhai(keypairsScript); | ||||
|       console.log('Keypair diagnostic:', keypairDiagnostic); | ||||
|        | ||||
|       return keypairDiagnostic; | ||||
|     } else { | ||||
|       console.log('No active session, cannot fetch keypairs'); | ||||
|       return { error: 'No active session' }; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('Error in debug function:', error); | ||||
|     return { error: error.toString() }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Fetch all keypairs from the WebAssembly vault | ||||
| export async function getKeypairsFromVault(wasmModule) { | ||||
|   if (!wasmModule) { | ||||
|     throw new Error('WASM module not loaded'); | ||||
|   } | ||||
|    | ||||
|   try { | ||||
|     // First run diagnostics for debugging | ||||
|     await debugVaultState(wasmModule); | ||||
|      | ||||
|     console.log('Calling list_keypairs WebAssembly binding...'); | ||||
|      | ||||
|     // Use our new direct WebAssembly binding instead of Rhai script | ||||
|     const keypairList = await wasmModule.list_keypairs(); | ||||
|     console.log('Retrieved keypairs from vault:', keypairList); | ||||
|      | ||||
|     // Transform the keypairs into the expected format | ||||
|     // The WebAssembly binding returns an array of objects with id, type, and metadata | ||||
|     const formattedKeypairs = Array.isArray(keypairList) ? keypairList.map(kp => { | ||||
|       // Parse metadata if it's a string | ||||
|       let metadata = {}; | ||||
|       if (kp.metadata) { | ||||
|         try { | ||||
|           if (typeof kp.metadata === 'string') { | ||||
|             metadata = JSON.parse(kp.metadata); | ||||
|           } else { | ||||
|             metadata = kp.metadata; | ||||
|           } | ||||
|         } catch (e) { | ||||
|           console.warn('Failed to parse keypair metadata:', e); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return { | ||||
|         id: kp.id, | ||||
|         label: metadata.label || `${kp.type}-Key-${kp.id.substring(0, 4)}` | ||||
|       }; | ||||
|     }) : []; | ||||
|      | ||||
|     console.log('Formatted keypairs:', formattedKeypairs); | ||||
|      | ||||
|     // Update the keypairs in the background service worker | ||||
|     return new Promise((resolve) => { | ||||
|       chrome.runtime.sendMessage({ | ||||
|         action: 'update_session', | ||||
|         type: 'keypairs_loaded', | ||||
|         data: formattedKeypairs | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           console.log('Successfully updated keypairs in background'); | ||||
|           resolve(formattedKeypairs); | ||||
|         } else { | ||||
|           console.error('Failed to update keypairs in background:', response?.error); | ||||
|           resolve([]); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     console.error('Error fetching keypairs from vault:', error); | ||||
|     return []; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Initialize session with the WASM module | ||||
| export function initSession(wasmModule, keyspace, password) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Call the WASM init_session function | ||||
|       console.log(`Initializing session for keyspace: ${keyspace}`); | ||||
|       await wasmModule.init_session(keyspace, password); | ||||
|        | ||||
|       // Update the session state in the background service worker | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keyspace',  | ||||
|         data: keyspace  | ||||
|       }, async (response) => { | ||||
|         if (response && response.success) { | ||||
|           try { | ||||
|             // After successful session initialization, fetch keypairs from the vault | ||||
|             console.log('Session initialized, fetching keypairs from vault...'); | ||||
|             const keypairs = await getKeypairsFromVault(wasmModule); | ||||
|             console.log('Keypairs loaded:', keypairs); | ||||
|             resolve(keypairs); | ||||
|           } catch (fetchError) { | ||||
|             console.error('Error fetching keypairs:', fetchError); | ||||
|             // Even if fetching keypairs fails, the session is initialized | ||||
|             resolve([]); | ||||
|           } | ||||
|         } else { | ||||
|           reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|         } | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error('Session initialization error:', error); | ||||
|       reject(error.message || 'Failed to initialize session'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Lock the session using the WASM module | ||||
| export function lockSession(wasmModule) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Call the WASM lock_session function | ||||
|       wasmModule.lock_session(); | ||||
|        | ||||
|       // Update the session state in the background service worker | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'session_locked' | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           resolve(); | ||||
|         } else { | ||||
|           reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|         } | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       reject(error.message || 'Failed to lock session'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Add a keypair using the WASM module | ||||
| export function addKeypair(wasmModule, keyType = 'Secp256k1', label = null) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Create a default label if none provided | ||||
|       const keyLabel = label || `${keyType}-Key-${Date.now().toString(16).slice(-4)}`; | ||||
|        | ||||
|       // Create metadata JSON for the keypair | ||||
|       const metadata = JSON.stringify({ | ||||
|         label: keyLabel, | ||||
|         created: new Date().toISOString(), | ||||
|         type: keyType | ||||
|       }); | ||||
|        | ||||
|       console.log(`Adding new keypair of type ${keyType} with label ${keyLabel}`); | ||||
|        | ||||
|       // Call the WASM add_keypair function with metadata | ||||
|       const keyId = await wasmModule.add_keypair(keyType, metadata); | ||||
|       console.log(`Keypair created with ID: ${keyId}`); | ||||
|        | ||||
|       // Create keypair object with ID and label | ||||
|       const newKeypair = {  | ||||
|         id: keyId,  | ||||
|         label: keyLabel  | ||||
|       }; | ||||
|        | ||||
|       // Update the session state in the background service worker | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keypair_added',  | ||||
|         data: newKeypair  | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           // After adding a keypair, refresh the whole list from the vault | ||||
|           getKeypairsFromVault(wasmModule) | ||||
|             .then(() => { | ||||
|               console.log('Keypair list refreshed from vault'); | ||||
|               resolve(keyId); | ||||
|             }) | ||||
|             .catch(refreshError => { | ||||
|               console.warn('Error refreshing keypair list:', refreshError); | ||||
|               // Still resolve with the key ID since the key was created | ||||
|               resolve(keyId); | ||||
|             }); | ||||
|         } else { | ||||
|           reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|         } | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       console.error('Error adding keypair:', error); | ||||
|       reject(error.message || 'Failed to add keypair'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Select a keypair using the WASM module | ||||
| export function selectKeypair(wasmModule, keyId) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Call the WASM select_keypair function | ||||
|       await wasmModule.select_keypair(keyId); | ||||
|        | ||||
|       // Update the session state in the background service worker | ||||
|       chrome.runtime.sendMessage({  | ||||
|         action: 'update_session',  | ||||
|         type: 'keypair_selected',  | ||||
|         data: keyId  | ||||
|       }, (response) => { | ||||
|         if (response && response.success) { | ||||
|           resolve(); | ||||
|         } else { | ||||
|           reject(response && response.error ? response.error : 'Failed to update session state'); | ||||
|         } | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       reject(error.message || 'Failed to select keypair'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Sign a message using the WASM module | ||||
| export function sign(wasmModule, message) { | ||||
|   return new Promise(async (resolve, reject) => { | ||||
|     if (!wasmModule) { | ||||
|       reject('WASM module not loaded'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Convert message to Uint8Array for WASM | ||||
|       const encoder = new TextEncoder(); | ||||
|       const messageBytes = encoder.encode(message); | ||||
|        | ||||
|       // Call the WASM sign function | ||||
|       const signature = await wasmModule.sign(messageBytes); | ||||
|       resolve(signature); | ||||
|     } catch (error) { | ||||
|       reject(error.message || 'Failed to sign message'); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										102
									
								
								extension/public/background/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,102 @@ | ||||
| // Background service worker for Modular Vault Extension | ||||
| // Handles session, keypair, and WASM logic | ||||
|  | ||||
| // We need to use dynamic imports for service workers in MV3 | ||||
| let wasmModule; | ||||
| let init; | ||||
| let wasm; | ||||
| let wasmReady = false; | ||||
|  | ||||
| // Initialize WASM on startup with dynamic import | ||||
| async function loadWasm() { | ||||
|   try { | ||||
|     // Using importScripts for service worker | ||||
|     const wasmUrl = chrome.runtime.getURL('wasm/wasm_app.js'); | ||||
|     wasmModule = await import(wasmUrl); | ||||
|     init = wasmModule.default; | ||||
|     wasm = wasmModule; | ||||
|      | ||||
|     // Initialize WASM with explicit WASM file path | ||||
|     await init(chrome.runtime.getURL('wasm/wasm_app_bg.wasm')); | ||||
|     wasmReady = true; | ||||
|     console.log('WASM initialized in background'); | ||||
|   } catch (error) { | ||||
|     console.error('Failed to initialize WASM:', error); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Start loading WASM | ||||
| loadWasm(); | ||||
|  | ||||
| chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { | ||||
|   if (!wasmReady) { | ||||
|     sendResponse({ error: 'WASM not ready' }); | ||||
|     return true; | ||||
|   } | ||||
|   // Session unlock/create | ||||
|   if (request.action === 'init_session') { | ||||
|     try { | ||||
|       const result = await wasm.init_session(request.keyspace, request.password); | ||||
|       // Persist current session info | ||||
|       await chrome.storage.local.set({ currentKeyspace: request.keyspace }); | ||||
|       sendResponse({ ok: true }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Lock session | ||||
|   if (request.action === 'lock_session') { | ||||
|     try { | ||||
|       wasm.lock_session(); | ||||
|       await chrome.storage.local.set({ currentKeyspace: null }); | ||||
|       sendResponse({ ok: true }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Add keypair | ||||
|   if (request.action === 'add_keypair') { | ||||
|     try { | ||||
|       const keyId = await wasm.add_keypair('Secp256k1', null); | ||||
|       let keypairs = (await chrome.storage.local.get(['keypairs'])).keypairs || []; | ||||
|       keypairs.push({ id: keyId, label: `Secp256k1-${keypairs.length + 1}` }); | ||||
|       await chrome.storage.local.set({ keypairs }); | ||||
|       sendResponse({ keyId }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Select keypair | ||||
|   if (request.action === 'select_keypair') { | ||||
|     try { | ||||
|       await wasm.select_keypair(request.keyId); | ||||
|       await chrome.storage.local.set({ selectedKeypair: request.keyId }); | ||||
|       sendResponse({ ok: true }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Sign | ||||
|   if (request.action === 'sign') { | ||||
|     try { | ||||
|       // Convert plaintext to Uint8Array | ||||
|       const encoder = new TextEncoder(); | ||||
|       const msgBytes = encoder.encode(request.message); | ||||
|       const signature = await wasm.sign(msgBytes); | ||||
|       sendResponse({ signature }); | ||||
|     } catch (e) { | ||||
|       sendResponse({ error: e.message }); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|   // Query status | ||||
|   if (request.action === 'get_status') { | ||||
|     const { currentKeyspace, keypairs, selectedKeypair } = await chrome.storage.local.get(['currentKeyspace', 'keypairs', 'selectedKeypair']); | ||||
|     sendResponse({ currentKeyspace, keypairs: keypairs || [], selectedKeypair }); | ||||
|     return true; | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										765
									
								
								extension/public/wasm/wasm_app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,765 @@ | ||||
| import * as __wbg_star0 from 'env'; | ||||
|  | ||||
| let wasm; | ||||
|  | ||||
| function addToExternrefTable0(obj) { | ||||
|     const idx = wasm.__externref_table_alloc(); | ||||
|     wasm.__wbindgen_export_2.set(idx, obj); | ||||
|     return idx; | ||||
| } | ||||
|  | ||||
| function handleError(f, args) { | ||||
|     try { | ||||
|         return f.apply(this, args); | ||||
|     } catch (e) { | ||||
|         const idx = addToExternrefTable0(e); | ||||
|         wasm.__wbindgen_exn_store(idx); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); | ||||
|  | ||||
| if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; | ||||
|  | ||||
| let cachedUint8ArrayMemory0 = null; | ||||
|  | ||||
| function getUint8ArrayMemory0() { | ||||
|     if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { | ||||
|         cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); | ||||
|     } | ||||
|     return cachedUint8ArrayMemory0; | ||||
| } | ||||
|  | ||||
| function getStringFromWasm0(ptr, len) { | ||||
|     ptr = ptr >>> 0; | ||||
|     return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); | ||||
| } | ||||
|  | ||||
| function isLikeNone(x) { | ||||
|     return x === undefined || x === null; | ||||
| } | ||||
|  | ||||
| function getArrayU8FromWasm0(ptr, len) { | ||||
|     ptr = ptr >>> 0; | ||||
|     return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); | ||||
| } | ||||
|  | ||||
| let WASM_VECTOR_LEN = 0; | ||||
|  | ||||
| const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); | ||||
|  | ||||
| const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' | ||||
|     ? function (arg, view) { | ||||
|     return cachedTextEncoder.encodeInto(arg, view); | ||||
| } | ||||
|     : function (arg, view) { | ||||
|     const buf = cachedTextEncoder.encode(arg); | ||||
|     view.set(buf); | ||||
|     return { | ||||
|         read: arg.length, | ||||
|         written: buf.length | ||||
|     }; | ||||
| }); | ||||
|  | ||||
| function passStringToWasm0(arg, malloc, realloc) { | ||||
|  | ||||
|     if (realloc === undefined) { | ||||
|         const buf = cachedTextEncoder.encode(arg); | ||||
|         const ptr = malloc(buf.length, 1) >>> 0; | ||||
|         getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); | ||||
|         WASM_VECTOR_LEN = buf.length; | ||||
|         return ptr; | ||||
|     } | ||||
|  | ||||
|     let len = arg.length; | ||||
|     let ptr = malloc(len, 1) >>> 0; | ||||
|  | ||||
|     const mem = getUint8ArrayMemory0(); | ||||
|  | ||||
|     let offset = 0; | ||||
|  | ||||
|     for (; offset < len; offset++) { | ||||
|         const code = arg.charCodeAt(offset); | ||||
|         if (code > 0x7F) break; | ||||
|         mem[ptr + offset] = code; | ||||
|     } | ||||
|  | ||||
|     if (offset !== len) { | ||||
|         if (offset !== 0) { | ||||
|             arg = arg.slice(offset); | ||||
|         } | ||||
|         ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; | ||||
|         const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); | ||||
|         const ret = encodeString(arg, view); | ||||
|  | ||||
|         offset += ret.written; | ||||
|         ptr = realloc(ptr, len, offset, 1) >>> 0; | ||||
|     } | ||||
|  | ||||
|     WASM_VECTOR_LEN = offset; | ||||
|     return ptr; | ||||
| } | ||||
|  | ||||
| let cachedDataViewMemory0 = null; | ||||
|  | ||||
| function getDataViewMemory0() { | ||||
|     if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { | ||||
|         cachedDataViewMemory0 = new DataView(wasm.memory.buffer); | ||||
|     } | ||||
|     return cachedDataViewMemory0; | ||||
| } | ||||
|  | ||||
| const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') | ||||
|     ? { register: () => {}, unregister: () => {} } | ||||
|     : new FinalizationRegistry(state => { | ||||
|     wasm.__wbindgen_export_5.get(state.dtor)(state.a, state.b) | ||||
| }); | ||||
|  | ||||
| function makeMutClosure(arg0, arg1, dtor, f) { | ||||
|     const state = { a: arg0, b: arg1, cnt: 1, dtor }; | ||||
|     const real = (...args) => { | ||||
|         // First up with a closure we increment the internal reference | ||||
|         // count. This ensures that the Rust closure environment won't | ||||
|         // be deallocated while we're invoking it. | ||||
|         state.cnt++; | ||||
|         const a = state.a; | ||||
|         state.a = 0; | ||||
|         try { | ||||
|             return f(a, state.b, ...args); | ||||
|         } finally { | ||||
|             if (--state.cnt === 0) { | ||||
|                 wasm.__wbindgen_export_5.get(state.dtor)(a, state.b); | ||||
|                 CLOSURE_DTORS.unregister(state); | ||||
|             } else { | ||||
|                 state.a = a; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|     real.original = state; | ||||
|     CLOSURE_DTORS.register(real, state, state); | ||||
|     return real; | ||||
| } | ||||
|  | ||||
| function debugString(val) { | ||||
|     // primitive types | ||||
|     const type = typeof val; | ||||
|     if (type == 'number' || type == 'boolean' || val == null) { | ||||
|         return  `${val}`; | ||||
|     } | ||||
|     if (type == 'string') { | ||||
|         return `"${val}"`; | ||||
|     } | ||||
|     if (type == 'symbol') { | ||||
|         const description = val.description; | ||||
|         if (description == null) { | ||||
|             return 'Symbol'; | ||||
|         } else { | ||||
|             return `Symbol(${description})`; | ||||
|         } | ||||
|     } | ||||
|     if (type == 'function') { | ||||
|         const name = val.name; | ||||
|         if (typeof name == 'string' && name.length > 0) { | ||||
|             return `Function(${name})`; | ||||
|         } else { | ||||
|             return 'Function'; | ||||
|         } | ||||
|     } | ||||
|     // objects | ||||
|     if (Array.isArray(val)) { | ||||
|         const length = val.length; | ||||
|         let debug = '['; | ||||
|         if (length > 0) { | ||||
|             debug += debugString(val[0]); | ||||
|         } | ||||
|         for(let i = 1; i < length; i++) { | ||||
|             debug += ', ' + debugString(val[i]); | ||||
|         } | ||||
|         debug += ']'; | ||||
|         return debug; | ||||
|     } | ||||
|     // Test for built-in | ||||
|     const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); | ||||
|     let className; | ||||
|     if (builtInMatches && builtInMatches.length > 1) { | ||||
|         className = builtInMatches[1]; | ||||
|     } else { | ||||
|         // Failed to match the standard '[object ClassName]' | ||||
|         return toString.call(val); | ||||
|     } | ||||
|     if (className == 'Object') { | ||||
|         // we're a user defined class or Object | ||||
|         // JSON.stringify avoids problems with cycles, and is generally much | ||||
|         // easier than looping through ownProperties of `val`. | ||||
|         try { | ||||
|             return 'Object(' + JSON.stringify(val) + ')'; | ||||
|         } catch (_) { | ||||
|             return 'Object'; | ||||
|         } | ||||
|     } | ||||
|     // errors | ||||
|     if (val instanceof Error) { | ||||
|         return `${val.name}: ${val.message}\n${val.stack}`; | ||||
|     } | ||||
|     // TODO we could test for more things here, like `Set`s and `Map`s. | ||||
|     return className; | ||||
| } | ||||
| /** | ||||
|  * Initialize the scripting environment (must be called before run_rhai) | ||||
|  */ | ||||
| export function init_rhai_env() { | ||||
|     wasm.init_rhai_env(); | ||||
| } | ||||
|  | ||||
| function takeFromExternrefTable0(idx) { | ||||
|     const value = wasm.__wbindgen_export_2.get(idx); | ||||
|     wasm.__externref_table_dealloc(idx); | ||||
|     return value; | ||||
| } | ||||
| /** | ||||
|  * Securely run a Rhai script in the extension context (must be called only after user approval) | ||||
|  * @param {string} script | ||||
|  * @returns {any} | ||||
|  */ | ||||
| export function run_rhai(script) { | ||||
|     const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.run_rhai(ptr0, len0); | ||||
|     if (ret[2]) { | ||||
|         throw takeFromExternrefTable0(ret[1]); | ||||
|     } | ||||
|     return takeFromExternrefTable0(ret[0]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Initialize session with keyspace and password | ||||
|  * @param {string} keyspace | ||||
|  * @param {string} password | ||||
|  * @returns {Promise<void>} | ||||
|  */ | ||||
| export function init_session(keyspace, password) { | ||||
|     const ptr0 = passStringToWasm0(keyspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len1 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.init_session(ptr0, len0, ptr1, len1); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Lock the session (zeroize password and session) | ||||
|  */ | ||||
| export function lock_session() { | ||||
|     wasm.lock_session(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get all keypairs from the current session | ||||
|  * Returns an array of keypair objects with id, type, and metadata | ||||
|  * Select keypair for the session | ||||
|  * @param {string} key_id | ||||
|  */ | ||||
| export function select_keypair(key_id) { | ||||
|     const ptr0 = passStringToWasm0(key_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.select_keypair(ptr0, len0); | ||||
|     if (ret[1]) { | ||||
|         throw takeFromExternrefTable0(ret[0]); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * List keypairs in the current session's keyspace | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function list_keypairs() { | ||||
|     const ret = wasm.list_keypairs(); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Add a keypair to the current keyspace | ||||
|  * @param {string | null} [key_type] | ||||
|  * @param {string | null} [metadata] | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function add_keypair(key_type, metadata) { | ||||
|     var ptr0 = isLikeNone(key_type) ? 0 : passStringToWasm0(key_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     var len0 = WASM_VECTOR_LEN; | ||||
|     var ptr1 = isLikeNone(metadata) ? 0 : passStringToWasm0(metadata, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|     var len1 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.add_keypair(ptr0, len0, ptr1, len1); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function passArray8ToWasm0(arg, malloc) { | ||||
|     const ptr = malloc(arg.length * 1, 1) >>> 0; | ||||
|     getUint8ArrayMemory0().set(arg, ptr / 1); | ||||
|     WASM_VECTOR_LEN = arg.length; | ||||
|     return ptr; | ||||
| } | ||||
| /** | ||||
|  * Sign message with current session | ||||
|  * @param {Uint8Array} message | ||||
|  * @returns {Promise<any>} | ||||
|  */ | ||||
| export function sign(message) { | ||||
|     const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc); | ||||
|     const len0 = WASM_VECTOR_LEN; | ||||
|     const ret = wasm.sign(ptr0, len0); | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_32(arg0, arg1, arg2) { | ||||
|     wasm.closure77_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_35(arg0, arg1, arg2) { | ||||
|     wasm.closure126_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_38(arg0, arg1, arg2) { | ||||
|     wasm.closure188_externref_shim(arg0, arg1, arg2); | ||||
| } | ||||
|  | ||||
| function __wbg_adapter_123(arg0, arg1, arg2, arg3) { | ||||
|     wasm.closure213_externref_shim(arg0, arg1, arg2, arg3); | ||||
| } | ||||
|  | ||||
| const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"]; | ||||
|  | ||||
| async function __wbg_load(module, imports) { | ||||
|     if (typeof Response === 'function' && module instanceof Response) { | ||||
|         if (typeof WebAssembly.instantiateStreaming === 'function') { | ||||
|             try { | ||||
|                 return await WebAssembly.instantiateStreaming(module, imports); | ||||
|  | ||||
|             } catch (e) { | ||||
|                 if (module.headers.get('Content-Type') != 'application/wasm') { | ||||
|                     console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); | ||||
|  | ||||
|                 } else { | ||||
|                     throw e; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const bytes = await module.arrayBuffer(); | ||||
|         return await WebAssembly.instantiate(bytes, imports); | ||||
|  | ||||
|     } else { | ||||
|         const instance = await WebAssembly.instantiate(module, imports); | ||||
|  | ||||
|         if (instance instanceof WebAssembly.Instance) { | ||||
|             return { instance, module }; | ||||
|  | ||||
|         } else { | ||||
|             return instance; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| function __wbg_get_imports() { | ||||
|     const imports = {}; | ||||
|     imports.wbg = {}; | ||||
|     imports.wbg.__wbg_buffer_609cc3eee51ed158 = function(arg0) { | ||||
|         const ret = arg0.buffer; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.call(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.call(arg1, arg2); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) { | ||||
|         const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) { | ||||
|         const ret = arg0.crypto; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) { | ||||
|         console.error(arg0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_error_ff4ddaabdfc5dbb3 = function() { return handleError(function (arg0) { | ||||
|         const ret = arg0.error; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_getRandomValues_3c9c0d586e575a16 = function() { return handleError(function (arg0, arg1) { | ||||
|         globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1)); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) { | ||||
|         arg0.getRandomValues(arg1); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_get_4f73335ab78445db = function(arg0, arg1, arg2) { | ||||
|         const ret = arg1[arg2 >>> 0]; | ||||
|         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.__wbg_get_67b2ba62fc30de12 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = Reflect.get(arg0, arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_get_8da03f81f6a1111e = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.get(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_instanceof_IdbDatabase_a3ef009ca00059f9 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBDatabase; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbFactory_12eaba3366f4302f = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBFactory; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbOpenDbRequest_a3416e156c9db893 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBOpenDBRequest; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_instanceof_IdbRequest_4813c3f207666aa4 = function(arg0) { | ||||
|         let result; | ||||
|         try { | ||||
|             result = arg0 instanceof IDBRequest; | ||||
|         } catch (_) { | ||||
|             result = false; | ||||
|         } | ||||
|         const ret = result; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) { | ||||
|         const ret = arg0.length; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) { | ||||
|         const ret = arg0.msCrypto; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) { | ||||
|         try { | ||||
|             var state0 = {a: arg0, b: arg1}; | ||||
|             var cb0 = (arg0, arg1) => { | ||||
|                 const a = state0.a; | ||||
|                 state0.a = 0; | ||||
|                 try { | ||||
|                     return __wbg_adapter_123(a, state0.b, arg0, arg1); | ||||
|                 } finally { | ||||
|                     state0.a = a; | ||||
|                 } | ||||
|             }; | ||||
|             const ret = new Promise(cb0); | ||||
|             return ret; | ||||
|         } finally { | ||||
|             state0.a = state0.b = 0; | ||||
|         } | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_405e22f390576ce2 = function() { | ||||
|         const ret = new Object(); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_78feb108b6472713 = function() { | ||||
|         const ret = new Array(); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) { | ||||
|         const ret = new Uint8Array(arg0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) { | ||||
|         const ret = new Function(getStringFromWasm0(arg0, arg1)); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function(arg0, arg1, arg2) { | ||||
|         const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_newwithlength_a381634e90c276d4 = function(arg0) { | ||||
|         const ret = new Uint8Array(arg0 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) { | ||||
|         const ret = arg0.node; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) { | ||||
|         const ret = arg0.objectStoreNames; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_objectStore_21878d46d25b64b6 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2)); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.open(getStringFromWasm0(arg1, arg2)); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_open_e0c0b2993eb596e1 = function() { return handleError(function (arg0, arg1, arg2, arg3) { | ||||
|         const ret = arg0.open(getStringFromWasm0(arg1, arg2), arg3 >>> 0); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_process_dc0fbacc7c1c06f7 = function(arg0) { | ||||
|         const ret = arg0.process; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_push_737cfc8c1432c2c6 = function(arg0, arg1) { | ||||
|         const ret = arg0.push(arg1); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_put_066faa31a6a88f5b = function() { return handleError(function (arg0, arg1, arg2) { | ||||
|         const ret = arg0.put(arg1, arg2); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_put_9ef5363941008835 = function() { return handleError(function (arg0, arg1) { | ||||
|         const ret = arg0.put(arg1); | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) { | ||||
|         queueMicrotask(arg0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) { | ||||
|         const ret = arg0.queueMicrotask; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) { | ||||
|         arg0.randomFillSync(arg1); | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () { | ||||
|         const ret = module.require; | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) { | ||||
|         const ret = Promise.resolve(arg0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_result_f29afabdf2c05826 = function() { return handleError(function (arg0) { | ||||
|         const ret = arg0.result; | ||||
|         return ret; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) { | ||||
|         arg0.set(arg1, arg2 >>> 0); | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) { | ||||
|         arg0.onerror = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) { | ||||
|         arg0.onsuccess = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_setonupgradeneeded_fcf7ce4f2eb0cb5f = function(arg0, arg1) { | ||||
|         arg0.onupgradeneeded = arg1; | ||||
|     }; | ||||
|     imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() { | ||||
|         const ret = typeof global === 'undefined' ? null : global; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() { | ||||
|         const ret = typeof globalThis === 'undefined' ? null : globalThis; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() { | ||||
|         const ret = typeof self === 'undefined' ? null : self; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() { | ||||
|         const ret = typeof window === 'undefined' ? null : window; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_subarray_aa9065fa9dc5df96 = function(arg0, arg1, arg2) { | ||||
|         const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbg_target_0a62d9d79a2a1ede = function(arg0) { | ||||
|         const ret = arg0.target; | ||||
|         return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); | ||||
|     }; | ||||
|     imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) { | ||||
|         const ret = arg0.then(arg1); | ||||
|         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; | ||||
|     }, arguments) }; | ||||
|     imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) { | ||||
|         const ret = arg0.versions; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_cb_drop = function(arg0) { | ||||
|         const obj = arg0.original; | ||||
|         if (obj.cnt-- == 1) { | ||||
|             obj.a = 0; | ||||
|             return true; | ||||
|         } | ||||
|         const ret = false; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) { | ||||
|         const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { | ||||
|         const ret = debugString(arg1); | ||||
|         const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         const len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_init_externref_table = function() { | ||||
|         const table = wasm.__wbindgen_export_2; | ||||
|         const offset = table.grow(4); | ||||
|         table.set(0, undefined); | ||||
|         table.set(offset + 0, undefined); | ||||
|         table.set(offset + 1, null); | ||||
|         table.set(offset + 2, true); | ||||
|         table.set(offset + 3, false); | ||||
|         ; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_function = function(arg0) { | ||||
|         const ret = typeof(arg0) === 'function'; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_null = function(arg0) { | ||||
|         const ret = arg0 === null; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_object = function(arg0) { | ||||
|         const val = arg0; | ||||
|         const ret = typeof(val) === 'object' && val !== null; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_string = function(arg0) { | ||||
|         const ret = typeof(arg0) === 'string'; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_is_undefined = function(arg0) { | ||||
|         const ret = arg0 === undefined; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_json_parse = function(arg0, arg1) { | ||||
|         const ret = JSON.parse(getStringFromWasm0(arg0, arg1)); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { | ||||
|         const obj = arg1; | ||||
|         const ret = JSON.stringify(obj === undefined ? null : obj); | ||||
|         const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); | ||||
|         const len1 = WASM_VECTOR_LEN; | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); | ||||
|         getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_memory = function() { | ||||
|         const ret = wasm.memory; | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_string_new = function(arg0, arg1) { | ||||
|         const ret = getStringFromWasm0(arg0, arg1); | ||||
|         return ret; | ||||
|     }; | ||||
|     imports.wbg.__wbindgen_throw = function(arg0, arg1) { | ||||
|         throw new Error(getStringFromWasm0(arg0, arg1)); | ||||
|     }; | ||||
|     imports['env'] = __wbg_star0; | ||||
|  | ||||
|     return imports; | ||||
| } | ||||
|  | ||||
| function __wbg_init_memory(imports, memory) { | ||||
|  | ||||
| } | ||||
|  | ||||
| function __wbg_finalize_init(instance, module) { | ||||
|     wasm = instance.exports; | ||||
|     __wbg_init.__wbindgen_wasm_module = module; | ||||
|     cachedDataViewMemory0 = null; | ||||
|     cachedUint8ArrayMemory0 = null; | ||||
|  | ||||
|  | ||||
|     wasm.__wbindgen_start(); | ||||
|     return wasm; | ||||
| } | ||||
|  | ||||
| function initSync(module) { | ||||
|     if (wasm !== undefined) return wasm; | ||||
|  | ||||
|  | ||||
|     if (typeof module !== 'undefined') { | ||||
|         if (Object.getPrototypeOf(module) === Object.prototype) { | ||||
|             ({module} = module) | ||||
|         } else { | ||||
|             console.warn('using deprecated parameters for `initSync()`; pass a single object instead') | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const imports = __wbg_get_imports(); | ||||
|  | ||||
|     __wbg_init_memory(imports); | ||||
|  | ||||
|     if (!(module instanceof WebAssembly.Module)) { | ||||
|         module = new WebAssembly.Module(module); | ||||
|     } | ||||
|  | ||||
|     const instance = new WebAssembly.Instance(module, imports); | ||||
|  | ||||
|     return __wbg_finalize_init(instance, module); | ||||
| } | ||||
|  | ||||
| async function __wbg_init(module_or_path) { | ||||
|     if (wasm !== undefined) return wasm; | ||||
|  | ||||
|  | ||||
|     if (typeof module_or_path !== 'undefined') { | ||||
|         if (Object.getPrototypeOf(module_or_path) === Object.prototype) { | ||||
|             ({module_or_path} = module_or_path) | ||||
|         } else { | ||||
|             console.warn('using deprecated parameters for the initialization function; pass a single object instead') | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (typeof module_or_path === 'undefined') { | ||||
|         module_or_path = new URL('wasm_app_bg.wasm', import.meta.url); | ||||
|     } | ||||
|     const imports = __wbg_get_imports(); | ||||
|  | ||||
|     if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { | ||||
|         module_or_path = fetch(module_or_path); | ||||
|     } | ||||
|  | ||||
|     __wbg_init_memory(imports); | ||||
|  | ||||
|     const { instance, module } = await __wbg_load(await module_or_path, imports); | ||||
|  | ||||
|     return __wbg_finalize_init(instance, module); | ||||
| } | ||||
|  | ||||
| export { initSync }; | ||||
| export default __wbg_init; | ||||
							
								
								
									
										
											BIN
										
									
								
								extension/public/wasm/wasm_app_bg.wasm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										120
									
								
								extension/vite.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,120 @@ | ||||
| import { defineConfig } from 'vite'; | ||||
| import react from '@vitejs/plugin-react'; | ||||
| import wasm from 'vite-plugin-wasm'; | ||||
| import topLevelAwait from 'vite-plugin-top-level-await'; | ||||
| import { resolve } from 'path'; | ||||
| import fs from 'fs'; | ||||
| import { Plugin } from 'vite'; | ||||
|  | ||||
| // Custom plugin to copy extension files directly to the dist directory | ||||
| const copyExtensionFiles = () => { | ||||
|   return { | ||||
|     name: 'copy-extension-files', | ||||
|     closeBundle() { | ||||
|       // Create the wasm directory in dist if it doesn't exist | ||||
|       const wasmDistDir = resolve(__dirname, 'dist/wasm'); | ||||
|       if (!fs.existsSync(wasmDistDir)) { | ||||
|         fs.mkdirSync(wasmDistDir, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|       // Copy the wasm.js file | ||||
|       const wasmJsSource = resolve(__dirname, 'wasm/wasm_app.js'); | ||||
|       const wasmJsDest = resolve(wasmDistDir, 'wasm_app.js'); | ||||
|       fs.copyFileSync(wasmJsSource, wasmJsDest); | ||||
|        | ||||
|       // Copy the wasm binary file | ||||
|       const wasmBinSource = resolve(__dirname, 'wasm/wasm_app_bg.wasm'); | ||||
|       const wasmBinDest = resolve(wasmDistDir, 'wasm_app_bg.wasm'); | ||||
|       fs.copyFileSync(wasmBinSource, wasmBinDest); | ||||
|  | ||||
|       // Create background directory and copy the background script | ||||
|       const bgDistDir = resolve(__dirname, 'dist/background'); | ||||
|       if (!fs.existsSync(bgDistDir)) { | ||||
|         fs.mkdirSync(bgDistDir, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|       const bgSource = resolve(__dirname, 'background/index.js'); | ||||
|       const bgDest = resolve(bgDistDir, 'index.js'); | ||||
|       fs.copyFileSync(bgSource, bgDest); | ||||
|        | ||||
|       // Create popup directory and copy the popup files | ||||
|       const popupDistDir = resolve(__dirname, 'dist/popup'); | ||||
|       if (!fs.existsSync(popupDistDir)) { | ||||
|         fs.mkdirSync(popupDistDir, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|       // Copy HTML file | ||||
|       const htmlSource = resolve(__dirname, 'popup/index.html'); | ||||
|       const htmlDest = resolve(popupDistDir, 'index.html'); | ||||
|       fs.copyFileSync(htmlSource, htmlDest); | ||||
|        | ||||
|       // Copy JS file | ||||
|       const jsSource = resolve(__dirname, 'popup/popup.js'); | ||||
|       const jsDest = resolve(popupDistDir, 'popup.js'); | ||||
|       fs.copyFileSync(jsSource, jsDest); | ||||
|        | ||||
|       // Copy CSS file | ||||
|       const cssSource = resolve(__dirname, 'popup/popup.css'); | ||||
|       const cssDest = resolve(popupDistDir, 'popup.css'); | ||||
|       fs.copyFileSync(cssSource, cssDest); | ||||
|        | ||||
|       // Also copy the manifest.json file | ||||
|       const manifestSource = resolve(__dirname, 'manifest.json'); | ||||
|       const manifestDest = resolve(__dirname, 'dist/manifest.json'); | ||||
|       fs.copyFileSync(manifestSource, manifestDest); | ||||
|        | ||||
|       // Copy assets directory | ||||
|       const assetsDistDir = resolve(__dirname, 'dist/assets'); | ||||
|       if (!fs.existsSync(assetsDistDir)) { | ||||
|         fs.mkdirSync(assetsDistDir, { recursive: true }); | ||||
|       } | ||||
|        | ||||
|       // Copy icon files | ||||
|       const iconSizes = [16, 32, 48, 128]; | ||||
|       iconSizes.forEach(size => { | ||||
|         const iconSource = resolve(__dirname, `assets/icon-${size}.png`); | ||||
|         const iconDest = resolve(assetsDistDir, `icon-${size}.png`); | ||||
|         if (fs.existsSync(iconSource)) { | ||||
|           fs.copyFileSync(iconSource, iconDest); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       console.log('Extension files copied to dist directory'); | ||||
|     } | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export default defineConfig({ | ||||
|   plugins: [ | ||||
|     react(), | ||||
|     wasm(), | ||||
|     topLevelAwait(), | ||||
|     copyExtensionFiles() | ||||
|   ], | ||||
|   build: { | ||||
|     outDir: 'dist', | ||||
|     emptyOutDir: true, | ||||
|     // Simplify the build output for browser extension | ||||
|     rollupOptions: { | ||||
|       input: { | ||||
|         popup: resolve(__dirname, 'popup/index.html') | ||||
|       }, | ||||
|       output: { | ||||
|         // Use a simpler output format without hash values | ||||
|         entryFileNames: 'assets/[name].js', | ||||
|         chunkFileNames: 'assets/[name]-[hash].js', | ||||
|         assetFileNames: 'assets/[name].[ext]', | ||||
|         // Make sure output is compatible with browser extensions | ||||
|         format: 'iife', | ||||
|         // Don't generate separate code-split chunks | ||||
|         manualChunks: undefined | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   // Provide a simple dev server config | ||||
|   server: { | ||||
|     fs: { | ||||
|       allow: ['../'] | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| @@ -9,8 +9,9 @@ path = "src/lib.rs" | ||||
| [dependencies] | ||||
| tokio = { version = "1.37", features = ["rt", "macros"] } | ||||
| async-trait = "0.1" | ||||
| js-sys = "0.3" | ||||
| wasm-bindgen = "0.2" | ||||
|  | ||||
| getrandom = { version = "0.3", features = ["wasm_js"] } | ||||
| wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } | ||||
| wasm-bindgen-futures = "0.4" | ||||
| thiserror = "1" | ||||
|  | ||||
| @@ -22,7 +23,9 @@ tempfile = "3" | ||||
| tokio = { version = "1", features = ["rt-multi-thread", "macros"] } | ||||
|  | ||||
| [target.'cfg(target_arch = "wasm32")'.dependencies] | ||||
| idb = { version = "0.4" } | ||||
| getrandom = { version = "0.3", features = ["wasm_js"] } | ||||
| getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] } | ||||
| idb = { version = "0.6" } | ||||
| wasm-bindgen-test = "0.3" | ||||
|  | ||||
| [features] | ||||
|   | ||||
| @@ -22,3 +22,12 @@ pub enum KVError { | ||||
| } | ||||
|  | ||||
| pub type Result<T> = std::result::Result<T, KVError>; | ||||
|  | ||||
| // Allow automatic conversion from idb::Error to KVError | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| impl From<idb::Error> for KVError { | ||||
|     fn from(e: idb::Error) -> Self { | ||||
|         KVError::Other(format!("idb error: {e:?}")) | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -26,8 +26,7 @@ use async_trait::async_trait; | ||||
| use idb::{Database, TransactionMode, Factory}; | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use wasm_bindgen::JsValue; | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use js_sys::Uint8Array; | ||||
| // use wasm-bindgen directly for Uint8Array if needed | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use std::rc::Rc; | ||||
|  | ||||
| @@ -47,6 +46,7 @@ impl WasmStore { | ||||
|         let mut open_req = factory.open(name, None) | ||||
|             .map_err(|e| KVError::Other(format!("IndexedDB factory open error: {e:?}")))?; | ||||
|         open_req.on_upgrade_needed(|event| { | ||||
|             use idb::DatabaseEvent; | ||||
|             let db = event.database().expect("Failed to get database in upgrade event"); | ||||
|             if !db.store_names().iter().any(|n| n == STORE_NAME) { | ||||
|                 db.create_object_store(STORE_NAME, Default::default()).unwrap(); | ||||
| @@ -66,11 +66,13 @@ impl KVStore for WasmStore { | ||||
|         let store = tx.object_store(STORE_NAME) | ||||
|             .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?; | ||||
|         use idb::Query; | ||||
|         let val = store.get(Query::from(JsValue::from_str(key))).await | ||||
|             .map_err(|e| KVError::Other(format!("idb get await error: {e:?}")))?; | ||||
|         let val = store.get(Query::from(JsValue::from_str(key)))?.await | ||||
|             .map_err(|e| KVError::Other(format!("idb get error: {e:?}")))?; | ||||
|         if let Some(jsval) = val { | ||||
|             let arr = Uint8Array::new(&jsval); | ||||
|             Ok(Some(arr.to_vec())) | ||||
|             match jsval.into_serde::<Vec<u8>>() { | ||||
|     Ok(bytes) => Ok(Some(bytes)), | ||||
|     Err(_) => Ok(None), | ||||
| } | ||||
|         } else { | ||||
|             Ok(None) | ||||
|         } | ||||
| @@ -80,8 +82,9 @@ impl KVStore for WasmStore { | ||||
|             .map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?; | ||||
|         let store = tx.object_store(STORE_NAME) | ||||
|             .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?; | ||||
|         store.put(&Uint8Array::from(value).into(), Some(&JsValue::from_str(key))).await | ||||
|             .map_err(|e| KVError::Other(format!("idb put await error: {e:?}")))?; | ||||
|         let js_value = JsValue::from_serde(&value).map_err(|e| KVError::Other(format!("serde error: {e:?}")))?; | ||||
| store.put(&js_value, Some(&JsValue::from_str(key)))?.await | ||||
|     .map_err(|e| KVError::Other(format!("idb put error: {e:?}")))?; | ||||
|         Ok(()) | ||||
|     } | ||||
|     async fn remove(&self, key: &str) -> Result<()> { | ||||
| @@ -90,8 +93,8 @@ impl KVStore for WasmStore { | ||||
|         let store = tx.object_store(STORE_NAME) | ||||
|             .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?; | ||||
|         use idb::Query; | ||||
|         store.delete(Query::from(JsValue::from_str(key))).await | ||||
|             .map_err(|e| KVError::Other(format!("idb delete await error: {e:?}")))?; | ||||
|         store.delete(Query::from(JsValue::from_str(key)))?.await | ||||
|             .map_err(|e| KVError::Other(format!("idb delete error: {e:?}")))?; | ||||
|         Ok(()) | ||||
|     } | ||||
|     async fn contains_key(&self, key: &str) -> Result<bool> { | ||||
| @@ -103,12 +106,11 @@ impl KVStore for WasmStore { | ||||
|             .map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?; | ||||
|         let store = tx.object_store(STORE_NAME) | ||||
|             .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?; | ||||
|         let js_keys = store.get_all_keys(None, None).await | ||||
|         let js_keys = store.get_all_keys(None, None)?.await | ||||
|             .map_err(|e| KVError::Other(format!("idb get_all_keys error: {e:?}")))?; | ||||
|         let arr = js_sys::Array::from(&JsValue::from(js_keys)); | ||||
|         let mut keys = Vec::new(); | ||||
|         for i in 0..arr.length() { | ||||
|             if let Some(s) = arr.get(i).as_string() { | ||||
|         for key in js_keys.iter() { | ||||
|             if let Some(s) = key.as_string() { | ||||
|                 keys.push(s); | ||||
|             } | ||||
|         } | ||||
| @@ -120,7 +122,7 @@ impl KVStore for WasmStore { | ||||
|             .map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?; | ||||
|         let store = tx.object_store(STORE_NAME) | ||||
|             .map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?; | ||||
|         store.clear().await | ||||
|         store.clear()?.await | ||||
|             .map_err(|e| KVError::Other(format!("idb clear error: {e:?}")))?; | ||||
|         Ok(()) | ||||
|     } | ||||
|   | ||||
| @@ -31,3 +31,22 @@ async fn test_native_store_basic() { | ||||
|     let keys = store.keys().await.unwrap(); | ||||
|     assert_eq!(keys.len(), 0); | ||||
| } | ||||
|  | ||||
| #[tokio::test] | ||||
| async fn test_native_store_persistence() { | ||||
|     let tmp_dir = tempfile::tempdir().unwrap(); | ||||
|     let path = tmp_dir.path().join("persistdb"); | ||||
|     let db_path = path.to_str().unwrap(); | ||||
|     // First open, set value | ||||
|     { | ||||
|         let store = NativeStore::open(db_path).unwrap(); | ||||
|         store.set("persist", b"value").await.unwrap(); | ||||
|     } | ||||
|     // Reopen and check value | ||||
|     { | ||||
|         let store = NativeStore::open(db_path).unwrap(); | ||||
|         let val = store.get("persist").await.unwrap(); | ||||
|         assert_eq!(val, Some(b"value".to_vec())); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ wasm_bindgen_test_configure!(run_in_browser); | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| async fn test_set_and_get() { | ||||
|     let store = WasmStore::open("test-db").await.expect("open"); | ||||
|     let store = WasmStore::open("vault").await.expect("open"); | ||||
|     store.set("foo", b"bar").await.expect("set"); | ||||
|     let val = store.get("foo").await.expect("get"); | ||||
|     assert_eq!(val, Some(b"bar".to_vec())); | ||||
| @@ -16,7 +16,7 @@ async fn test_set_and_get() { | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| async fn test_delete_and_exists() { | ||||
|     let store = WasmStore::open("test-db").await.expect("open"); | ||||
|     let store = WasmStore::open("vault").await.expect("open"); | ||||
|     store.set("foo", b"bar").await.expect("set"); | ||||
|     assert_eq!(store.contains_key("foo").await.unwrap(), true); | ||||
|     assert_eq!(store.contains_key("bar").await.unwrap(), false); | ||||
| @@ -26,7 +26,7 @@ async fn test_delete_and_exists() { | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| async fn test_keys() { | ||||
|     let store = WasmStore::open("test-db").await.expect("open"); | ||||
|     let store = WasmStore::open("vault").await.expect("open"); | ||||
|     store.set("foo", b"bar").await.expect("set"); | ||||
|     store.set("baz", b"qux").await.expect("set"); | ||||
|     let keys = store.keys().await.unwrap(); | ||||
| @@ -35,9 +35,26 @@ async fn test_keys() { | ||||
|     assert!(keys.contains(&"baz".to_string())); | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| async fn test_wasm_store_persistence() { | ||||
|     // Use a unique store name to avoid collisions | ||||
|     let store_name = "persist_test_store"; | ||||
|     // First open, set value | ||||
|     { | ||||
|         let store = WasmStore::open(store_name).await.expect("open"); | ||||
|         store.set("persist", b"value").await.expect("set"); | ||||
|     } | ||||
|     // Reopen and check value | ||||
|     { | ||||
|         let store = WasmStore::open(store_name).await.expect("open"); | ||||
|         let val = store.get("persist").await.expect("get"); | ||||
|         assert_eq!(val, Some(b"value".to_vec())); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen_test] | ||||
| async fn test_clear() { | ||||
|     let store = WasmStore::open("test-db").await.expect("open"); | ||||
|     let store = WasmStore::open("vault").await.expect("open"); | ||||
|     store.set("foo", b"bar").await.expect("set"); | ||||
|     store.set("baz", b"qux").await.expect("set"); | ||||
|     store.clear().await.unwrap(); | ||||
|   | ||||
| @@ -12,7 +12,7 @@ tokio = { version = "1.37", features = ["rt", "macros"] } | ||||
| kvstore = { path = "../kvstore" } | ||||
| scrypt = "0.11" | ||||
| sha2 = "0.10" | ||||
| aes-gcm = "0.10" | ||||
| # aes-gcm = "0.10" | ||||
| pbkdf2 = "0.12" | ||||
| signature = "2.2" | ||||
| async-trait = "0.1" | ||||
| @@ -22,17 +22,20 @@ ed25519-dalek = "2.1" | ||||
| rand_core = "0.6" | ||||
| log = "0.4" | ||||
| thiserror = "1" | ||||
| env_logger = "0.11" | ||||
| console_log = "1" | ||||
|  | ||||
| [target.'cfg(not(target_arch = "wasm32"))'.dependencies] | ||||
| env_logger = "0.11" | ||||
| serde = { version = "1", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| hex = "0.4" | ||||
| zeroize = "1.8.1" | ||||
| rhai = "1.21.0" | ||||
|  | ||||
|  | ||||
| [dev-dependencies] | ||||
| wasm-bindgen-test = "0.3" | ||||
| console_error_panic_hook = "0.1" | ||||
| # console_error_panic_hook = "0.1" | ||||
|  | ||||
| [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] | ||||
| tempfile = "3.10" | ||||
| @@ -42,7 +45,16 @@ chrono = "0.4" | ||||
|  | ||||
| [target.'cfg(target_arch = "wasm32")'.dependencies] | ||||
| getrandom = { version = "0.3", features = ["wasm_js"] } | ||||
| getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] } | ||||
| getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] } | ||||
| wasm-bindgen = "0.2" | ||||
| js-sys = "0.3" | ||||
| console_error_panic_hook = "0.1" | ||||
| # console_error_panic_hook = "0.1" | ||||
| serde = { version = "1", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| hex = "0.4" | ||||
| rhai = "1.21.0" | ||||
| zeroize = "1.8.1" | ||||
|  | ||||
| [features] | ||||
| default = [] | ||||
| native = [] | ||||
| @@ -1,5 +1,7 @@ | ||||
| //! Data models for the vault crate | ||||
|  | ||||
| // Only keep serde derives on structs, remove unused imports | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| pub struct VaultMetadata { | ||||
|     pub name: String, | ||||
| @@ -7,7 +9,7 @@ pub struct VaultMetadata { | ||||
|     // ... other vault-level metadata | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct KeyspaceMetadata { | ||||
|     pub name: String, | ||||
|     pub salt: [u8; 16], // Unique salt for this keyspace | ||||
| @@ -17,12 +19,28 @@ pub struct KeyspaceMetadata { | ||||
|     // ... other keyspace metadata | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] | ||||
| pub struct KeyspaceData { | ||||
|     pub keypairs: Vec<KeyEntry>, | ||||
|     // ... other keyspace-level metadata | ||||
| } | ||||
|  | ||||
| impl zeroize::Zeroize for KeyspaceData { | ||||
|     fn zeroize(&mut self) { | ||||
|         for key in &mut self.keypairs { | ||||
|             key.zeroize(); | ||||
|         } | ||||
|         self.keypairs.zeroize(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl zeroize::Zeroize for KeyEntry { | ||||
|     fn zeroize(&mut self) { | ||||
|         self.private_key.zeroize(); | ||||
|         // Optionally, zeroize other fields if needed | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| pub struct KeyEntry { | ||||
|     pub id: String, | ||||
| @@ -39,7 +57,7 @@ pub enum KeyType { | ||||
|     // ... | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] | ||||
| #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] | ||||
| pub struct KeyMetadata { | ||||
|     pub name: Option<String>, | ||||
|     pub created_at: Option<u64>, | ||||
|   | ||||
| @@ -39,7 +39,7 @@ impl<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> RhaiSessionMan | ||||
|         pub fn sign(&self, message: rhai::Blob) -> Result<rhai::Blob, String> { | ||||
|             let sm = self.inner.lock().unwrap(); | ||||
|             // Try to get the current keyspace name from session state if possible | ||||
|             let keypair = sm.current_keypair().ok_or("No keypair selected")?; | ||||
|             let _keypair = sm.current_keypair().ok_or("No keypair selected")?; | ||||
|             // Sign using the session manager; password and keyspace are not needed (already unlocked) | ||||
|             crate::rhai_sync_helpers::sign_sync::<S>( | ||||
|                 &sm, | ||||
| @@ -50,14 +50,46 @@ impl<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> RhaiSessionMan | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| impl<S: kvstore::traits::KVStore + Clone + 'static> RhaiSessionManager<S> { | ||||
|     // WASM-specific implementation (stub for now) | ||||
|     pub fn select_keypair(&self, key_id: String) -> Result<(), String> { | ||||
|         // Use the global singleton for session management | ||||
|         crate::session_singleton::SESSION_MANAGER.with(|cell| { | ||||
|             let mut opt = cell.borrow_mut(); | ||||
|             if let Some(session) = opt.as_mut() { | ||||
|                 session.select_keypair(&key_id).map_err(|e| format!("select_keypair error: {e}")) | ||||
|             } else { | ||||
|                 Err("Session not initialized".to_string()) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn current_keypair(&self) -> Option<String> { | ||||
|         crate::session_singleton::SESSION_MANAGER.with(|cell| { | ||||
|             let opt = cell.borrow(); | ||||
|             opt.as_ref() | ||||
|                 .and_then(|session| session.current_keypair().map(|k| k.id.clone())) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn logout(&self) { | ||||
|         crate::session_singleton::SESSION_MANAGER.with(|cell| { | ||||
|             let mut opt = cell.borrow_mut(); | ||||
|             if let Some(session) = opt.as_mut() { | ||||
|                 session.logout(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     pub fn sign(&self, _message: rhai::Blob) -> Result<rhai::Blob, String> { | ||||
|         // Signing is async in WASM; must be called from JS/wasm-bindgen, not Rhai | ||||
|         Err("sign is async in WASM; use the WASM sign() API from JS instead".to_string()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // WASM-specific API: no Arc/Mutex, just a reference | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| pub fn register_rhai_api<S: kvstore::traits::KVStore + Clone + 'static>( | ||||
|     engine: &mut Engine, | ||||
|     session_manager: &SessionManager<S>, | ||||
|     // session_manager: &SessionManager<S>, | ||||
| ) { | ||||
|     // WASM registration logic (adapt as needed) | ||||
|     // Example: engine.register_type::<RhaiSessionManager<S>>(); | ||||
| @@ -66,7 +98,7 @@ pub fn register_rhai_api<S: kvstore::traits::KVStore + Clone + 'static>( | ||||
|     engine.register_fn("select_keypair", |key_id: String| { | ||||
|         crate::wasm_helpers::select_keypair_global(&key_id) | ||||
|     }); // Calls the shared WASM session singleton | ||||
|     engine.register_fn("sign", |message: rhai::Blob| -> Result<rhai::Blob, String> { | ||||
|     engine.register_fn("sign", |_message: rhai::Blob| -> Result<rhai::Blob, String> { | ||||
|         Err("sign is async in WASM; use the WASM sign() API from JS instead".to_string()) | ||||
|     }); | ||||
|     // No global session object in WASM; use JS/WASM API for session ops | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| //! Synchronous wrappers for async Vault and EVM client APIs for use in Rhai bindings. | ||||
| //! These use block_on for native, and spawn_local for WASM if needed. | ||||
|  | ||||
| use crate::session::SessionManager; | ||||
|  | ||||
| // Synchronous wrappers for async Vault and EVM client APIs for use in Rhai bindings. | ||||
| // These use block_on for native, and spawn_local for WASM if needed. | ||||
|  | ||||
|  | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| use tokio::runtime::Handle; | ||||
|  | ||||
|   | ||||
| @@ -2,79 +2,60 @@ | ||||
| //! Provides ergonomic, stateful access to unlocked keyspaces and keypairs for interactive applications. | ||||
| //! All state is local to the SessionManager instance. No global state. | ||||
|  | ||||
| use crate::{KVStore, KeyEntry, KeyspaceData, Vault, VaultError}; | ||||
| use std::collections::HashMap; | ||||
| use zeroize::Zeroize; | ||||
| use crate::{Vault, KeyspaceData, KeyEntry, VaultError, KVStore}; | ||||
|  | ||||
| /// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API. | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| pub struct SessionManager<S: KVStore + Send + Sync> { | ||||
|     vault: Vault<S>, | ||||
|     unlocked_keyspaces: HashMap<String, (Vec<u8>, KeyspaceData)>, // name -> (password, data) | ||||
|     current_keyspace: Option<String>, | ||||
|     unlocked_keyspace: Option<(String, Vec<u8>, KeyspaceData)>, // (name, password, data) | ||||
|     current_keypair: Option<String>, | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| pub struct SessionManager<S: KVStore> { | ||||
|     vault: Vault<S>, | ||||
|     unlocked_keyspaces: HashMap<String, (Vec<u8>, KeyspaceData)>, // name -> (password, data) | ||||
|     current_keyspace: Option<String>, | ||||
|     unlocked_keyspace: Option<(String, Vec<u8>, KeyspaceData)>, // (name, password, data) | ||||
|     current_keypair: Option<String>, | ||||
| } | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| impl<S: KVStore + Send + Sync> SessionManager<S> { | ||||
|     /// Create a new session manager from a Vault instance. | ||||
|     pub fn new(vault: Vault<S>) -> Self { | ||||
|         Self { | ||||
|             vault, | ||||
|             unlocked_keyspaces: HashMap::new(), | ||||
|             current_keyspace: None, | ||||
|             current_keypair: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| impl<S: KVStore> SessionManager<S> { | ||||
|     /// Create a new session manager from a Vault instance. | ||||
|     pub fn new(vault: Vault<S>) -> Self { | ||||
|         Self { | ||||
|             vault, | ||||
|             unlocked_keyspaces: HashMap::new(), | ||||
|             current_keyspace: None, | ||||
|             current_keypair: None, | ||||
|         } | ||||
|     pub fn get_vault_mut(&mut self) -> &mut Vault<S> { | ||||
|         &mut self.vault | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Native impl for all methods | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| impl<S: KVStore + Send + Sync> SessionManager<S> { | ||||
|     /// Unlock a keyspace and store its decrypted data in memory. | ||||
|     pub fn new(vault: Vault<S>) -> Self { | ||||
|         Self { | ||||
|             vault, | ||||
|             unlocked_keyspace: None, | ||||
|             current_keypair: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> { | ||||
|         self.vault.create_keyspace(name, password, tags).await?; | ||||
|         self.unlock_keyspace(name, password).await | ||||
|     } | ||||
|  | ||||
|     pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> { | ||||
|         let data = self.vault.unlock_keyspace(name, password).await?; | ||||
|         self.unlocked_keyspaces.insert(name.to_string(), (password.to_vec(), data)); | ||||
|         self.current_keyspace = Some(name.to_string()); | ||||
|         self.unlocked_keyspace = Some((name.to_string(), password.to_vec(), data)); | ||||
|         self.current_keypair = None; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     /// Select a previously unlocked keyspace as the current context. | ||||
|     pub fn select_keyspace(&mut self, name: &str) -> Result<(), VaultError> { | ||||
|         if self.unlocked_keyspaces.contains_key(name) { | ||||
|             self.current_keyspace = Some(name.to_string()); | ||||
|             self.current_keypair = None; | ||||
|             Ok(()) | ||||
|         } else { | ||||
|             Err(VaultError::Crypto("Keyspace not unlocked".to_string())) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Select a keypair within the current keyspace. | ||||
|     pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> { | ||||
|         let keyspace = self.current_keyspace.as_ref().ok_or_else(|| VaultError::Crypto("No keyspace selected".to_string()))?; | ||||
|         let (_, data) = self.unlocked_keyspaces.get(keyspace).ok_or_else(|| VaultError::Crypto("Keyspace not unlocked".to_string()))?; | ||||
|         let data = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .map(|(_, _, d)| d) | ||||
|             .ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         if data.keypairs.iter().any(|k| k.id == key_id) { | ||||
|             self.current_keypair = Some(key_id.to_string()); | ||||
|             Ok(()) | ||||
| @@ -83,146 +64,164 @@ impl<S: KVStore + Send + Sync> SessionManager<S> { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get the currently selected keyspace data (if any). | ||||
|     pub fn current_keyspace(&self) -> Option<&KeyspaceData> { | ||||
|         self.current_keyspace.as_ref() | ||||
|             .and_then(|name| self.unlocked_keyspaces.get(name)) | ||||
|             .map(|(_, data)| data) | ||||
|     } | ||||
|  | ||||
|     /// Get the currently selected keypair (if any). | ||||
|     pub fn current_keypair(&self) -> Option<&KeyEntry> { | ||||
|         let keyspace = self.current_keyspace()?; | ||||
|         let key_id = self.current_keypair.as_ref()?; | ||||
|         keyspace.keypairs.iter().find(|k| &k.id == key_id) | ||||
|     } | ||||
|  | ||||
|     /// Sign a message with the currently selected keypair. | ||||
|     pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|         let _keyspace = self.current_keyspace().ok_or(VaultError::Crypto("No keyspace selected".to_string()))?; | ||||
|         let keypair = self.current_keypair().ok_or(VaultError::Crypto("No keypair selected".to_string()))?; | ||||
|         let (password, _) = self.unlocked_keyspaces.get(self.current_keyspace.as_ref().unwrap()).unwrap(); | ||||
|         self.vault.sign( | ||||
|             self.current_keyspace.as_ref().unwrap(), | ||||
|             password, | ||||
|             &keypair.id, | ||||
|             message, | ||||
|         ).await | ||||
|     } | ||||
|  | ||||
|     /// Get a reference to the underlying Vault (for stateless operations in tests). | ||||
|     pub fn get_vault(&self) -> &Vault<S> { | ||||
|         &self.vault | ||||
|     } | ||||
| } | ||||
|  | ||||
| // WASM impl for all methods | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| impl<S: KVStore> SessionManager<S> { | ||||
|     /// Unlock a keyspace and store its decrypted data in memory. | ||||
|     pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> { | ||||
|     pub async fn add_keypair( | ||||
|         &mut self, | ||||
|         key_type: Option<crate::KeyType>, | ||||
|         metadata: Option<crate::KeyMetadata>, | ||||
|     ) -> Result<String, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         let id = self | ||||
|             .vault | ||||
|             .add_keypair(name, password, key_type, metadata.clone()) | ||||
|             .await?; | ||||
|         let data = self.vault.unlock_keyspace(name, password).await?; | ||||
|         self.unlocked_keyspaces.insert(name.to_string(), (password.to_vec(), data)); | ||||
|         self.current_keyspace = Some(name.to_string()); | ||||
|         Ok(()) | ||||
|         self.unlocked_keyspace = Some((name.clone(), password.clone(), data)); | ||||
|         Ok(id) | ||||
|     } | ||||
|  | ||||
|     /// Select a previously unlocked keyspace as the current context. | ||||
|     pub fn select_keyspace(&mut self, name: &str) -> Result<(), VaultError> { | ||||
|         if self.unlocked_keyspaces.contains_key(name) { | ||||
|             self.current_keyspace = Some(name.to_string()); | ||||
|             self.current_keypair = None; | ||||
|             Ok(()) | ||||
|         } else { | ||||
|             Err(VaultError::Crypto("Keyspace not unlocked".to_string())) | ||||
|         } | ||||
|     pub fn list_keypairs(&self) -> Option<&[KeyEntry]> { | ||||
|         self.current_keyspace().map(|ks| ks.keypairs.as_slice()) | ||||
|     } | ||||
|  | ||||
|     /// Select a keypair within the current keyspace. | ||||
|     pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> { | ||||
|         let keyspace = self.current_keyspace.as_ref().ok_or_else(|| VaultError::Crypto("No keyspace selected".to_string()))?; | ||||
|         let (_, data) = self.unlocked_keyspaces.get(keyspace).ok_or_else(|| VaultError::Crypto("Keyspace not unlocked".to_string()))?; | ||||
|         if data.keypairs.iter().any(|k| k.id == key_id) { | ||||
|             self.current_keypair = Some(key_id.to_string()); | ||||
|             Ok(()) | ||||
|         } else { | ||||
|             Err(VaultError::Crypto("Keypair not found".to_string())) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get the currently selected keyspace data (if any). | ||||
|     pub fn current_keyspace(&self) -> Option<&KeyspaceData> { | ||||
|         self.current_keyspace.as_ref() | ||||
|             .and_then(|name| self.unlocked_keyspaces.get(name)) | ||||
|             .map(|(_, data)| data) | ||||
|         self.unlocked_keyspace.as_ref().map(|(_, _, data)| data) | ||||
|     } | ||||
|  | ||||
|     /// Get the currently selected keypair (if any). | ||||
|     pub fn current_keypair(&self) -> Option<&KeyEntry> { | ||||
|         let keyspace = self.current_keyspace()?; | ||||
|         let key_id = self.current_keypair.as_ref()?; | ||||
|         keyspace.keypairs.iter().find(|k| &k.id == key_id) | ||||
|     } | ||||
|  | ||||
|     /// Sign a message with the currently selected keypair. | ||||
|     pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|         let _keyspace = self.current_keyspace().ok_or(VaultError::Crypto("No keyspace selected".to_string()))?; | ||||
|         let keypair = self.current_keypair().ok_or(VaultError::Crypto("No keypair selected".to_string()))?; | ||||
|         let (password, _) = self.unlocked_keyspaces.get(self.current_keyspace.as_ref().unwrap()).unwrap(); | ||||
|         self.vault.sign( | ||||
|             self.current_keyspace.as_ref().unwrap(), | ||||
|             password, | ||||
|             &keypair.id, | ||||
|             message, | ||||
|         ).await | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         let keypair = self | ||||
|             .current_keypair() | ||||
|             .ok_or(VaultError::Crypto("No keypair selected".to_string()))?; | ||||
|         self.vault.sign(name, password, &keypair.id, message).await | ||||
|     } | ||||
|  | ||||
|     /// Get a reference to the underlying Vault (for stateless operations in tests). | ||||
|     pub fn get_vault(&self) -> &Vault<S> { | ||||
|         &self.vault | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Shared impl for methods needed by Drop | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| impl<S: KVStore + Send + Sync> SessionManager<S> { | ||||
|     /// Wipe all unlocked keyspaces and secrets from memory. | ||||
|     pub fn logout(&mut self) { | ||||
|         for (pw, data) in self.unlocked_keyspaces.values_mut() { | ||||
|             pw.zeroize(); | ||||
|             // KeyspaceData and KeyEntry use Vec<u8> for secrets, drop will clear | ||||
|             for k in &mut data.keypairs { | ||||
|                 k.private_key.zeroize(); | ||||
|             } | ||||
|         if let Some((_, mut password, mut data)) = self.unlocked_keyspace.take() { | ||||
|             password.zeroize(); | ||||
|             data.zeroize(); | ||||
|         } | ||||
|         self.unlocked_keyspaces.clear(); | ||||
|         self.current_keyspace = None; | ||||
|         self.current_keypair = None; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| impl<S: KVStore> SessionManager<S> { | ||||
|     /// Wipe all unlocked keyspaces and secrets from memory. | ||||
|     pub fn logout(&mut self) { | ||||
|         for (pw, data) in self.unlocked_keyspaces.values_mut() { | ||||
|             pw.zeroize(); | ||||
|             // KeyspaceData and KeyEntry use Vec<u8> for secrets, drop will clear | ||||
|             for k in &mut data.keypairs { | ||||
|                 k.private_key.zeroize(); | ||||
|             } | ||||
|         } | ||||
|         self.unlocked_keyspaces.clear(); | ||||
|         self.current_keyspace = None; | ||||
|         self.current_keypair = None; | ||||
|     } | ||||
| } | ||||
|  | ||||
| // END wasm32 impl | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| impl<S: KVStore + Send + Sync> Drop for SessionManager<S> { | ||||
|     fn drop(&mut self) { | ||||
|         self.logout(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| impl<S: KVStore> SessionManager<S> { | ||||
|     pub fn new(vault: Vault<S>) -> Self { | ||||
|         Self { | ||||
|             vault, | ||||
|             unlocked_keyspace: None, | ||||
|             current_keypair: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> { | ||||
|         self.vault.create_keyspace(name, password, tags).await?; | ||||
|         self.unlock_keyspace(name, password).await | ||||
|     } | ||||
|  | ||||
|     pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> { | ||||
|         let data = self.vault.unlock_keyspace(name, password).await?; | ||||
|         self.unlocked_keyspace = Some((name.to_string(), password.to_vec(), data)); | ||||
|         self.current_keypair = None; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> { | ||||
|         let data = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .map(|(_, _, d)| d) | ||||
|             .ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         if data.keypairs.iter().any(|k| k.id == key_id) { | ||||
|             self.current_keypair = Some(key_id.to_string()); | ||||
|             Ok(()) | ||||
|         } else { | ||||
|             Err(VaultError::Crypto("Keypair not found".to_string())) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn add_keypair( | ||||
|         &mut self, | ||||
|         key_type: Option<crate::KeyType>, | ||||
|         metadata: Option<crate::KeyMetadata>, | ||||
|     ) -> Result<String, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         let id = self | ||||
|             .vault | ||||
|             .add_keypair(name, password, key_type, metadata.clone()) | ||||
|             .await?; | ||||
|         let data = self.vault.unlock_keyspace(name, password).await?; | ||||
|         self.unlocked_keyspace = Some((name.clone(), password.clone(), data)); | ||||
|         Ok(id) | ||||
|     } | ||||
|  | ||||
|     pub fn list_keypairs(&self) -> Option<&[KeyEntry]> { | ||||
|         self.current_keyspace().map(|ks| ks.keypairs.as_slice()) | ||||
|     } | ||||
|  | ||||
|     pub fn current_keyspace(&self) -> Option<&KeyspaceData> { | ||||
|         self.unlocked_keyspace.as_ref().map(|(_, _, data)| data) | ||||
|     } | ||||
|  | ||||
|     pub fn current_keypair(&self) -> Option<&KeyEntry> { | ||||
|         let keyspace = self.current_keyspace()?; | ||||
|         let key_id = self.current_keypair.as_ref()?; | ||||
|         keyspace.keypairs.iter().find(|k| &k.id == key_id) | ||||
|     } | ||||
|  | ||||
|     pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> { | ||||
|         let (name, password, _) = self | ||||
|             .unlocked_keyspace | ||||
|             .as_ref() | ||||
|             .ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?; | ||||
|         let keypair = self | ||||
|             .current_keypair() | ||||
|             .ok_or(VaultError::Crypto("No keypair selected".to_string()))?; | ||||
|         self.vault.sign(name, password, &keypair.id, message).await | ||||
|     } | ||||
|  | ||||
|     pub fn get_vault(&self) -> &Vault<S> { | ||||
|         &self.vault | ||||
|     } | ||||
|  | ||||
|     pub fn logout(&mut self) { | ||||
|         if let Some((_, mut password, mut data)) = self.unlocked_keyspace.take() { | ||||
|             password.zeroize(); | ||||
|             data.zeroize(); | ||||
|         } | ||||
|         self.current_keypair = None; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| impl<S: KVStore> Drop for SessionManager<S> { | ||||
|     fn drop(&mut self) { | ||||
|         self.logout(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -15,17 +15,21 @@ async fn session_manager_end_to_end() { | ||||
|     let keyspace = "personal"; | ||||
|     let password = b"testpass"; | ||||
|  | ||||
|     // Create keyspace | ||||
|     vault.create_keyspace(keyspace, password, None).await.expect("create_keyspace"); | ||||
|     // Add keypair | ||||
|     let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Secp256k1), Some(KeyMetadata { name: Some("main".to_string()), created_at: None, tags: None })).await.expect("add_keypair"); | ||||
|  | ||||
|     // Create session manager | ||||
|     let mut session = SessionManager::new(vault); | ||||
|     session.unlock_keyspace(keyspace, password).await.expect("unlock_keyspace"); | ||||
|     session.select_keyspace(keyspace).expect("select_keyspace"); | ||||
|     // Create and unlock keyspace in one step | ||||
|     session.create_keyspace(keyspace, password, None).await.expect("create_keyspace via session"); | ||||
|     // Add keypair using session API | ||||
|     let key_id = session.add_keypair(Some(KeyType::Secp256k1), Some(KeyMetadata { name: Some("main".to_string()), created_at: None, tags: None })).await.expect("add_keypair via session"); | ||||
|     session.select_keypair(&key_id).expect("select_keypair"); | ||||
|  | ||||
|     // Test add_keypair with metadata via SessionManager | ||||
|     let meta = KeyMetadata { name: Some("user1-key".to_string()), created_at: None, tags: Some(vec!["tag1".to_string()]) }; | ||||
|     let key_id2 = session.add_keypair(Some(KeyType::Ed25519), Some(meta.clone())).await.expect("add_keypair via session"); | ||||
|     // List keypairs and check metadata | ||||
|     let keypairs = session.list_keypairs().expect("list_keypairs"); | ||||
|     assert!(keypairs.iter().any(|k| k.id == key_id2 && k.metadata.as_ref().unwrap().name.as_deref() == Some("user1-key")), "metadata name should be present"); | ||||
|  | ||||
|     // Sign and verify | ||||
|     let msg = b"hello world"; | ||||
|     let sig = session.sign(msg).await.expect("sign"); | ||||
| @@ -55,7 +59,8 @@ async fn session_manager_errors() { | ||||
|     let vault = Vault::new(store); | ||||
|     let mut session = SessionManager::new(vault); | ||||
|     // No keyspace unlocked | ||||
|     assert!(session.select_keyspace("none").is_err()); | ||||
|     // select_keyspace removed; test unlocking a non-existent keyspace or selecting a keypair from an empty keyspace instead. | ||||
| assert!(session.select_keypair("none").is_err()); | ||||
|     assert!(session.select_keypair("none").is_err()); | ||||
|     assert!(session.sign(b"fail").await.is_err()); | ||||
| } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ async fn test_keypair_management_and_crypto() { | ||||
|     // All imports are WASM-specific and local to the test function | ||||
|     use kvstore::wasm::WasmStore; | ||||
|     use vault::Vault; | ||||
|     let store = WasmStore::open("testdb_wasm_keypair_management").await.unwrap(); | ||||
|     let store = WasmStore::open("vault").await.unwrap(); | ||||
|     let mut vault = Vault::new(store); | ||||
|     vault.create_keyspace("testspace", b"pw", None).await.unwrap(); | ||||
|     let key_id = vault.add_keypair("testspace", b"pw", None, None).await.unwrap(); | ||||
|   | ||||
| @@ -11,19 +11,65 @@ use vault::Vault; | ||||
| wasm_bindgen_test_configure!(run_in_browser); | ||||
|  | ||||
| #[wasm_bindgen_test(async)] | ||||
| async fn test_session_manager_end_to_end() { | ||||
|     // Example: test session manager logic in WASM | ||||
|     // This is a placeholder for your real test logic. | ||||
|     // All imports are WASM-specific and local to the test function | ||||
| async fn test_session_manager_lock_unlock_keypairs_persistence() { | ||||
|     use kvstore::wasm::WasmStore; | ||||
|     use vault::{Vault, KeyType, KeyMetadata}; | ||||
|     use vault::session::SessionManager; | ||||
|     let store = WasmStore::open("testdb_wasm_session_manager").await.unwrap(); | ||||
|     let vault = Vault::new(store); | ||||
|     let mut manager = SessionManager::new(vault); | ||||
|     let keyspace = "testspace"; | ||||
|     // This test can only check session initialization/select_keyspace logic as SessionManager does not create keypairs directly. | ||||
|     // manager.select_keyspace(keyspace) would fail unless the keyspace exists. | ||||
|     // For a true end-to-end test, use Vault to create the keyspace and keypair, then test SessionManager. | ||||
|     // For now, just test that SessionManager can be constructed. | ||||
|     assert!(manager.current_keyspace().is_none()); | ||||
|     let store = WasmStore::open("test-session-manager-lock-unlock").await.unwrap(); | ||||
|     let mut vault = Vault::new(store); | ||||
|     let keyspace = "testspace2"; | ||||
|     let password = b"testpass2"; | ||||
|  | ||||
|     // 1. Create session manager | ||||
|     let mut session = SessionManager::new(vault); | ||||
|     // Create and unlock keyspace in one step | ||||
|     session.create_keyspace(keyspace, password, None).await.expect("create_keyspace via session"); | ||||
|     // 2. Add two keypairs with names using session API | ||||
|     let meta1 = KeyMetadata { name: Some("keypair-one".to_string()), created_at: None, tags: None }; | ||||
|     let meta2 = KeyMetadata { name: Some("keypair-two".to_string()), created_at: None, tags: None }; | ||||
|     let id1 = session.add_keypair(Some(KeyType::Secp256k1), Some(meta1.clone())).await.expect("add_keypair1 via session"); | ||||
|     let id2 = session.add_keypair(Some(KeyType::Ed25519), Some(meta2.clone())).await.expect("add_keypair2 via session"); | ||||
|  | ||||
|     // 3. List, store keys and names | ||||
|     let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>(); | ||||
|     let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>(); | ||||
|     assert_eq!(keypairs_before.len(), 2); | ||||
|     assert!(keypairs_before.iter().any(|k| k.0 == id1 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-one"))); | ||||
|     assert!(keypairs_before.iter().any(|k| k.0 == id2 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-two"))); | ||||
|  | ||||
|     // 4. Lock (logout) | ||||
|     session.logout(); | ||||
|     assert!(session.current_keyspace().is_none()); | ||||
|  | ||||
|     // 5. Unlock again | ||||
|     session.unlock_keyspace(keyspace, password).await.expect("unlock_keyspace again"); | ||||
|     // select_keyspace removed; unlocking a keyspace is sufficient after refactor. | ||||
|  | ||||
|     // 6. List and check keys/names match | ||||
|     let keypairs_after = session.list_keypairs().expect("list_keypairs after").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>(); | ||||
|     assert_eq!(keypairs_before, keypairs_after, "Keypairs before and after lock/unlock should match"); | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen_test(async)] | ||||
| async fn test_session_manager_end_to_end() { | ||||
|     use kvstore::wasm::WasmStore; | ||||
|     use vault::{Vault, KeyType, KeyMetadata}; | ||||
|     use vault::session::SessionManager; | ||||
|     let store = WasmStore::open("test-session-manager").await.unwrap(); | ||||
|     let keyspace = "testspace"; | ||||
|     let password = b"testpass"; | ||||
|  | ||||
|     // Create session manager | ||||
|     let mut session = SessionManager::new(Vault::new(store)); | ||||
|     // Create and unlock keyspace in one step | ||||
|     session.create_keyspace(keyspace, password, None).await.expect("create_keyspace via session"); | ||||
|     // Add keypair using session API | ||||
|     let key_id = session.add_keypair(Some(KeyType::Secp256k1), Some(KeyMetadata { name: Some("main".to_string()), created_at: None, tags: None })).await.expect("add_keypair via session"); | ||||
|  | ||||
|     // Test add_keypair with metadata via SessionManager | ||||
|     let meta = KeyMetadata { name: Some("user1-key".to_string()), created_at: None, tags: Some(vec!["tag1".to_string()]) }; | ||||
|     let key_id2 = session.add_keypair(Some(KeyType::Ed25519), Some(meta.clone())).await.expect("add_keypair via session"); | ||||
|     // List keypairs and check metadata | ||||
|     let keypairs = session.list_keypairs().expect("list_keypairs"); | ||||
|     assert!(keypairs.iter().any(|k| k.id == key_id2 && k.metadata.as_ref().unwrap().name.as_deref() == Some("user1-key")), "metadata name should be present"); | ||||
| } | ||||
|   | ||||
							
								
								
									
										153
									
								
								wasm/src/lib.rs
									
									
									
									
									
								
							
							
						
						| @@ -1,153 +0,0 @@ | ||||
| //! WASM entrypoint for Rhai scripting integration for the extension. | ||||
| //! Composes vault and evm_client Rhai bindings and exposes a secure run_rhai API. | ||||
|  | ||||
| use wasm_bindgen::prelude::*; | ||||
| use once_cell::unsync::Lazy; | ||||
| use std::cell::RefCell; | ||||
|  | ||||
|  | ||||
|  | ||||
| use wasm_bindgen::JsValue; | ||||
|  | ||||
| use rhai::Engine; | ||||
| use vault::session::SessionManager; | ||||
| use vault::rhai_bindings as vault_rhai_bindings; | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| use kvstore::wasm::WasmStore; | ||||
|  | ||||
| #[cfg(not(target_arch = "wasm32"))] | ||||
| use std::sync::{Arc, Mutex}; | ||||
|  | ||||
| // Global singleton engine/session/client (for demonstration; production should scope per user/session) | ||||
| thread_local! { | ||||
|     static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new())); | ||||
|     #[cfg(not(target_arch = "wasm32"))] | ||||
|     static SESSION_MANAGER: RefCell<Option<Arc<Mutex<SessionManager<kvstore::native::NativeStore>>>>> = RefCell::new(None); | ||||
|     static SESSION_PASSWORD: RefCell<Option<Vec<u8>>> = RefCell::new(None); | ||||
| } | ||||
|  | ||||
| #[cfg(target_arch = "wasm32")] | ||||
| pub use vault::session_singleton::SESSION_MANAGER; | ||||
|  | ||||
|  | ||||
| /// Initialize the scripting environment (must be called before run_rhai) | ||||
| #[wasm_bindgen] | ||||
| pub fn init_rhai_env() { | ||||
|     ENGINE.with(|engine_cell| { | ||||
|         let mut engine = engine_cell.borrow_mut(); | ||||
|         // Register APIs with dummy session; will be replaced by real session after init | ||||
|         SESSION_MANAGER.with(|cell| { | ||||
|             #[cfg(target_arch = "wasm32")] | ||||
|             if let Some(ref session) = cell.borrow().as_ref() { | ||||
|                 vault_rhai_bindings::register_rhai_api(&mut engine, session); | ||||
|             } | ||||
|             #[cfg(not(target_arch = "wasm32"))] | ||||
|             if let Some(session) = cell.borrow().as_ref() { | ||||
|                 vault_rhai_bindings::register_rhai_api(&mut engine, session.clone()); | ||||
|             } | ||||
|         }); | ||||
|         // TODO: Register EVM APIs with session if needed | ||||
|  | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /// Initialize session with keyspace and password | ||||
| #[wasm_bindgen] | ||||
| pub fn init_session(keyspace: &str, password: &str) -> Result<(), JsValue> { | ||||
|     #[cfg(target_arch = "wasm32")] | ||||
|     { | ||||
|         use wasm_bindgen_futures::spawn_local; | ||||
|         use kvstore::wasm::WasmStore; | ||||
|         use vault::session::SessionManager; | ||||
|         let keyspace = keyspace.to_string(); | ||||
|         let password_vec = password.as_bytes().to_vec(); | ||||
|         spawn_local(async move { | ||||
|             match WasmStore::open(&keyspace).await { | ||||
|                 Ok(store) => { | ||||
|                     let vault = vault::Vault::new(store); | ||||
|                     let mut manager = SessionManager::new(vault); | ||||
|                     if let Err(e) = manager.unlock_keyspace(&keyspace, &password_vec).await { | ||||
|                         web_sys::console::error_1(&format!("Failed to unlock keyspace: {e}").into()); | ||||
|                         return; | ||||
|                     } | ||||
|                     SESSION_MANAGER.with(|cell| cell.replace(Some(manager))); | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     web_sys::console::error_1(&format!("Failed to open WasmStore: {e}").into()); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     #[cfg(not(target_arch = "wasm32"))] | ||||
|     { | ||||
|         let store = kvstore::native::NativeStore::open("testdb").expect("open native store"); | ||||
|         let vault = vault::Vault::new(store); | ||||
|         let manager = SessionManager::new(vault); | ||||
|         use std::sync::{Arc, Mutex}; | ||||
|         let arc_manager = Arc::new(Mutex::new(manager)); | ||||
|         SESSION_MANAGER.with(|cell| cell.replace(Some(arc_manager))); | ||||
|     } | ||||
|     SESSION_PASSWORD.with(|cell| cell.replace(Some(password.as_bytes().to_vec()))); | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Select keypair for the session | ||||
| #[wasm_bindgen] | ||||
| pub fn select_keypair(key_id: &str) -> Result<(), JsValue> { | ||||
|     let mut result = Err(JsValue::from_str("Session not initialized")); | ||||
|     SESSION_MANAGER.with(|cell| { | ||||
|         #[cfg(target_arch = "wasm32")] | ||||
|         if let Some(session) = cell.borrow_mut().as_mut() { | ||||
|             result = session.select_keypair(key_id) | ||||
|                 .map_err(|e| JsValue::from_str(&format!("select_keypair error: {e}"))); | ||||
|         } | ||||
|         #[cfg(not(target_arch = "wasm32"))] | ||||
|         if let Some(session_arc) = cell.borrow_mut().as_mut() { | ||||
|             let mut session = session_arc.lock().unwrap(); | ||||
|             result = session.select_keypair(key_id) | ||||
|                 .map_err(|e| JsValue::from_str(&format!("select_keypair error: {e}"))); | ||||
|         } | ||||
|     }); | ||||
|     result | ||||
| } | ||||
|  | ||||
| /// Lock the session (zeroize password and session) | ||||
| #[wasm_bindgen] | ||||
| pub fn lock_session() { | ||||
|     SESSION_MANAGER.with(|cell| *cell.borrow_mut() = None); | ||||
|     SESSION_PASSWORD.with(|cell| *cell.borrow_mut() = None); | ||||
| } | ||||
|  | ||||
| /// Sign message with current session | ||||
| #[wasm_bindgen] | ||||
| pub fn sign(message: &[u8]) -> Result<JsValue, JsValue> { | ||||
|     let mut result: Option<Result<JsValue, JsValue>> = None; | ||||
|     SESSION_MANAGER.with(|cell| { | ||||
|         if let Some(session) = cell.borrow().as_ref() { | ||||
|             let password = SESSION_PASSWORD.with(|pw| pw.borrow().clone()); | ||||
|             // ...rest of sign logic here, using session and password... | ||||
|             // For now, just set result = Ok(JsValue::from_str("signed")); as a placeholder | ||||
|             result = Some(Ok(JsValue::from_str("signed"))); | ||||
|         } | ||||
|     }); | ||||
|     result.unwrap_or_else(|| Err(JsValue::from_str("Session not initialized"))) | ||||
| } | ||||
|  | ||||
| /// Securely run a Rhai script in the extension context (must be called only after user approval) | ||||
| #[wasm_bindgen] | ||||
| pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> { | ||||
|     ENGINE.with(|engine_cell| { | ||||
|         let mut engine = engine_cell.borrow_mut(); | ||||
|         SESSION_MANAGER.with(|cell| { | ||||
|             if let Some(ref mut session) = cell.borrow_mut().as_mut() { | ||||
|                 let mut scope = rhai::Scope::new(); | ||||
|                 engine.eval_with_scope::<rhai::Dynamic>(&mut scope, script) | ||||
|                     .map(|result| JsValue::from_str(&result.to_string())) | ||||
|                     .map_err(|e| JsValue::from_str(&format!("Rhai error: {e}"))) | ||||
|             } else { | ||||
|                 Err(JsValue::from_str("Session not initialized")) | ||||
|             } | ||||
|         }) | ||||
|     }) | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| [package] | ||||
| name = "wasm" | ||||
| name = "wasm_app" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| 
 | ||||
| @@ -7,11 +7,13 @@ edition = "2021" | ||||
| crate-type = ["cdylib"] | ||||
| 
 | ||||
| [dependencies] | ||||
| kvstore = { path = "../kvstore" } | ||||
| wasm-bindgen = "0.2" | ||||
| gloo-utils = "0.1" | ||||
| js-sys = "0.3" | ||||
| web-sys = { version = "0.3", features = ["console"] } | ||||
| kvstore = { path = "../kvstore" } | ||||
| hex = "0.4" | ||||
| wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } | ||||
| gloo-utils = "0.1" | ||||
| 
 | ||||
| #  | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| rhai = { version = "1.16", features = ["serde"] } | ||||
| @@ -25,4 +27,4 @@ wasm-bindgen-test = "0.3" | ||||
| 
 | ||||
| [target.'cfg(target_arch = "wasm32")'.dependencies] | ||||
| getrandom = { version = "0.3", features = ["wasm_js"] } | ||||
| getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] } | ||||
| getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] } | ||||
							
								
								
									
										172
									
								
								wasm_app/src/debug_bindings.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,172 @@ | ||||
| //! WASM-only debug bindings for the vault extension | ||||
| #![cfg(target_arch = "wasm32")] | ||||
|  | ||||
| use wasm_bindgen::prelude::*; | ||||
| use crate::{SESSION_MANAGER, SESSION_PASSWORD}; | ||||
|  | ||||
| /// Debugging function to check if keypairs can be listed | ||||
| #[wasm_bindgen] | ||||
| pub async fn list_keypairs_debug() -> Result<JsValue, JsValue> { | ||||
|     use js_sys::{Array, Object}; | ||||
|     use web_sys::console; | ||||
|     console::log_1(&"Debug listing keypairs...".into()); | ||||
|     let session_ptr = SESSION_MANAGER.with(|cell| { | ||||
|         let has_session = cell.borrow().is_some(); | ||||
|         console::log_1(&format!("Has session: {}", has_session).into()); | ||||
|         cell.borrow().as_ref().map(|s| s as *const _) | ||||
|     }); | ||||
|     let password_opt = SESSION_PASSWORD.with(|pw| { | ||||
|         let has_pw = pw.borrow().is_some(); | ||||
|         console::log_1(&format!("Has password: {}", has_pw).into()); | ||||
|         pw.borrow().clone() | ||||
|     }); | ||||
|     if session_ptr.is_none() { | ||||
|         return Err(JsValue::from_str("Session not initialized in debug function")); | ||||
|     } | ||||
|     if password_opt.is_none() { | ||||
|         return Err(JsValue::from_str("Session password not set in debug function")); | ||||
|     } | ||||
|     let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = unsafe { &*session_ptr.unwrap() }; | ||||
|     let password = password_opt.unwrap(); | ||||
|     match session.current_keyspace_name() { | ||||
|         Some(ks) => { | ||||
|             let vault = session.get_vault(); | ||||
|             match vault.list_keypairs(ks, &password).await { | ||||
|                 Ok(keypairs) => { | ||||
|                     console::log_1(&format!("Found {} keypairs", keypairs.len()).into()); | ||||
|                     let array = Array::new(); | ||||
|                     for (id, key_type) in keypairs { | ||||
|                         let obj = Object::new(); | ||||
|                         js_sys::Reflect::set(&obj, &JsValue::from_str("id"), &JsValue::from_str(&id)).unwrap(); | ||||
|                         js_sys::Reflect::set(&obj, &JsValue::from_str("type"), &JsValue::from_str(&format!("{:?}", key_type))).unwrap(); | ||||
|                         array.push(&obj); | ||||
|                     } | ||||
|                     return Ok(array.into()); | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     console::error_1(&format!("Error listing keypairs in debug function: {}", e).into()); | ||||
|                     return Err(JsValue::from_str(&format!("Error listing keypairs: {}", e))); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         None => { | ||||
|             console::error_1(&"No keyspace selected in debug function".into()); | ||||
|             return Err(JsValue::from_str("No keyspace selected")); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[wasm_bindgen] | ||||
| pub async fn check_indexeddb() -> Result<JsValue, JsValue> { | ||||
|     #[cfg(not(target_arch = "wasm32"))] | ||||
|     { | ||||
|         return Err(JsValue::from_str( | ||||
|             "IndexedDB check only available in browser context", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         use js_sys::Object; | ||||
|         use kvstore::traits::KVStore; | ||||
|         use kvstore::wasm::WasmStore; | ||||
|         use web_sys::console; // Import the trait so we can use its methods | ||||
|  | ||||
|         console::log_1(&"Checking IndexedDB availability...".into()); | ||||
|  | ||||
|         // Check if window.indexedDB is available | ||||
|         if js_sys::eval("typeof window.indexedDB") | ||||
|             .map_err(|e| { | ||||
|                 console::error_1(&format!("Error checking IndexedDB: {:?}", e).into()); | ||||
|                 JsValue::from_str(&format!("Error checking IndexedDB: {:?}", e)) | ||||
|             })? | ||||
|             .as_string() | ||||
|             .unwrap_or_default() | ||||
|             == "undefined" | ||||
|         { | ||||
|             console::error_1(&"IndexedDB is not available in this browser".into()); | ||||
|             return Err(JsValue::from_str( | ||||
|                 "IndexedDB is not available in this browser", | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         // Try to create a test database | ||||
|         match WasmStore::open("db_test").await { | ||||
|             Ok(store) => { | ||||
|                 console::log_1(&"Successfully opened test database".into()); | ||||
|  | ||||
|                 // Try to write and read a value to ensure it works | ||||
|                 let test_key = "test_key"; | ||||
|                 let test_value = "test_value"; | ||||
|  | ||||
|                 // Use the KVStore trait methods | ||||
|                 if let Err(e) = store.set(test_key, test_value.as_bytes()).await { | ||||
|                     console::error_1(&format!("Failed to write to IndexedDB: {}", e).into()); | ||||
|                     return Err(JsValue::from_str(&format!( | ||||
|                         "Failed to write to IndexedDB: {}", | ||||
|                         e | ||||
|                     ))); | ||||
|                 } | ||||
|  | ||||
|                 // Get the value and handle the Option<Vec<u8>> properly | ||||
|                 match store.get(test_key).await { | ||||
|                     Ok(maybe_value) => match maybe_value { | ||||
|                         Some(value) => { | ||||
|                             let value_str = String::from_utf8_lossy(&value); | ||||
|                             if value_str == test_value { | ||||
|                                 console::log_1( | ||||
|                                     &"Successfully read test value from IndexedDB".into(), | ||||
|                                 ); | ||||
|                             } else { | ||||
|                                 console::error_1( | ||||
|                                     &format!( | ||||
|                                         "IndexedDB test value mismatch: expected {}, got {}", | ||||
|                                         test_value, value_str | ||||
|                                     ) | ||||
|                                     .into(), | ||||
|                                 ); | ||||
|                                 return Err(JsValue::from_str("IndexedDB test value mismatch")); | ||||
|                             } | ||||
|                         } | ||||
|                         None => { | ||||
|                             console::error_1(&"IndexedDB test key not found after writing".into()); | ||||
|                             return Err(JsValue::from_str( | ||||
|                                 "IndexedDB test key not found after writing", | ||||
|                             )); | ||||
|                         } | ||||
|                     }, | ||||
|                     Err(e) => { | ||||
|                         console::error_1(&format!("Failed to read from IndexedDB: {}", e).into()); | ||||
|                         return Err(JsValue::from_str(&format!( | ||||
|                             "Failed to read from IndexedDB: {}", | ||||
|                             e | ||||
|                         ))); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Return success with the available database names | ||||
|                 let result = Object::new(); | ||||
|                 js_sys::Reflect::set( | ||||
|                     &result, | ||||
|                     &JsValue::from_str("status"), | ||||
|                     &JsValue::from_str("success"), | ||||
|                 ) | ||||
|                 .unwrap(); | ||||
|                 js_sys::Reflect::set( | ||||
|                     &result, | ||||
|                     &JsValue::from_str("message"), | ||||
|                     &JsValue::from_str("IndexedDB is working properly"), | ||||
|                 ) | ||||
|                 .unwrap(); | ||||
|  | ||||
|                 return Ok(result.into()); | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 console::error_1(&format!("Failed to open IndexedDB test database: {}", e).into()); | ||||
|                 return Err(JsValue::from_str(&format!( | ||||
|                     "Failed to open test database: {}", | ||||
|                     e | ||||
|                 ))); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										77
									
								
								wasm_app/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,77 @@ | ||||
| //! WASM entrypoint for Rhai scripting integration for the extension. | ||||
| //! Composes vault and evm_client Rhai bindings and exposes a secure run_rhai API. | ||||
| #![cfg(target_arch = "wasm32")] | ||||
|  | ||||
| use once_cell::unsync::Lazy; | ||||
| use std::cell::RefCell; | ||||
| use wasm_bindgen::prelude::*; | ||||
|  | ||||
| use wasm_bindgen::JsValue; | ||||
|  | ||||
| use rhai::Engine; | ||||
| use vault::rhai_bindings as vault_rhai_bindings; | ||||
| use vault::session::SessionManager; | ||||
|  | ||||
| use kvstore::wasm::WasmStore; | ||||
|  | ||||
| // Global singleton engine/session/client (for demonstration; production should scope per user/session) | ||||
| thread_local! { | ||||
|     static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new())); | ||||
|     static SESSION_PASSWORD: RefCell<Option<Vec<u8>>> = RefCell::new(None); | ||||
| } | ||||
|  | ||||
| pub use vault::session_singleton::SESSION_MANAGER; | ||||
|  | ||||
| // Include the keypair bindings module | ||||
| mod vault_bindings; | ||||
| pub use vault_bindings::*; | ||||
|  | ||||
| /// Initialize the scripting environment (must be called before run_rhai) | ||||
| #[wasm_bindgen] | ||||
| pub fn init_rhai_env() { | ||||
|     ENGINE.with(|engine_cell| { | ||||
|         let mut engine = engine_cell.borrow_mut(); | ||||
|         // Register APIs with dummy session; will be replaced by real session after init | ||||
|         SESSION_MANAGER.with(|cell| { | ||||
|             if let Some(ref session) = cell.borrow().as_ref() { | ||||
|                 vault_rhai_bindings::register_rhai_api::<WasmStore>(&mut engine); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /// Securely run a Rhai script in the extension context (must be called only after user approval) | ||||
| #[wasm_bindgen] | ||||
| pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> { | ||||
|     ENGINE.with(|engine_cell| { | ||||
|         let mut engine = engine_cell.borrow_mut(); | ||||
|         SESSION_MANAGER.with(|cell| { | ||||
|             if let Some(ref mut session) = cell.borrow_mut().as_mut() { | ||||
|                 let mut scope = rhai::Scope::new(); | ||||
|                 engine | ||||
|                     .eval_with_scope::<rhai::Dynamic>(&mut scope, script) | ||||
|                     .map(|res| JsValue::from_str(&format!("{:?}", res))) | ||||
|                     .map_err(|e| JsValue::from_str(&format!("{}", e))) | ||||
|             } else { | ||||
|                 Err(JsValue::from_str("Session not initialized")) | ||||
|             } | ||||
|         }) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| pub mod wasm_helpers { | ||||
|     use super::*; | ||||
|  | ||||
|     /// Global function to select keypair (used in Rhai) | ||||
|     pub fn select_keypair_global(key_id: &str) -> Result<(), String> { | ||||
|         SESSION_MANAGER.with(|cell| { | ||||
|             if let Some(session) = cell.borrow_mut().as_mut() { | ||||
|                 session | ||||
|                     .select_keypair(key_id) | ||||
|                     .map_err(|e| format!("select_keypair error: {e}")) | ||||
|             } else { | ||||
|                 Err("Session not initialized".to_string()) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										185
									
								
								wasm_app/src/vault_bindings.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,185 @@ | ||||
| //! WebAssembly bindings for accessing vault operations (session, keypairs, signing, scripting, etc) | ||||
| #![cfg(target_arch = "wasm32")] | ||||
|  | ||||
| use kvstore::wasm::WasmStore; | ||||
| use once_cell::unsync::Lazy; | ||||
| use rhai::Engine; | ||||
| use std::cell::RefCell; | ||||
| use vault::rhai_bindings as vault_rhai_bindings; | ||||
| use vault::session::SessionManager; | ||||
| use wasm_bindgen::prelude::*; | ||||
| use wasm_bindgen::JsValue; | ||||
|  | ||||
| thread_local! { | ||||
|     static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new())); | ||||
|     static SESSION_PASSWORD: RefCell<Option<Vec<u8>>> = RefCell::new(None); | ||||
| } | ||||
|  | ||||
| pub use vault::session_singleton::SESSION_MANAGER; | ||||
|  | ||||
| // ===================== | ||||
| // Session Lifecycle | ||||
| // ===================== | ||||
|  | ||||
| /// Initialize session with keyspace and password | ||||
| #[wasm_bindgen] | ||||
| pub async fn init_session(keyspace: &str, password: &str) -> Result<(), JsValue> { | ||||
|     let keyspace = keyspace.to_string(); | ||||
|     let password_vec = password.as_bytes().to_vec(); | ||||
|     match WasmStore::open(&keyspace).await { | ||||
|         Ok(store) => { | ||||
|             let vault = vault::Vault::new(store); | ||||
|             let mut manager = SessionManager::new(vault); | ||||
|             match manager.unlock_keyspace(&keyspace, &password_vec).await { | ||||
|                 Ok(_) => { | ||||
|                     SESSION_MANAGER.with(|cell| cell.replace(Some(manager))); | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     web_sys::console::error_1(&format!("Failed to unlock keyspace: {e}").into()); | ||||
|                     return Err(JsValue::from_str(&format!("Failed to unlock keyspace: {e}"))); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Err(e) => { | ||||
|             web_sys::console::error_1(&format!("Failed to open WasmStore: {e}").into()); | ||||
|             return Err(JsValue::from_str(&format!("Failed to open WasmStore: {e}"))); | ||||
|         } | ||||
|     } | ||||
|     SESSION_PASSWORD.with(|cell| cell.replace(Some(password.as_bytes().to_vec()))); | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Lock the session (zeroize password and session) | ||||
| #[wasm_bindgen] | ||||
| pub fn lock_session() { | ||||
|     SESSION_MANAGER.with(|cell| *cell.borrow_mut() = None); | ||||
|     SESSION_PASSWORD.with(|cell| *cell.borrow_mut() = None); | ||||
| } | ||||
|  | ||||
| // ===================== | ||||
| // Keypair Management | ||||
| // ===================== | ||||
|  | ||||
| /// Get all keypairs from the current session | ||||
| /// Returns an array of keypair objects with id, type, and metadata | ||||
| // #[wasm_bindgen] | ||||
| // pub async fn list_keypairs() -> Result<JsValue, JsValue> { | ||||
| //     // [Function body commented out to resolve duplicate symbol error] | ||||
| //     // (Original implementation moved to keypair_bindings.rs) | ||||
| //     unreachable!("This function is disabled. Use the export from keypair_bindings.rs."); | ||||
| // } | ||||
|  | ||||
| // [Function body commented out to resolve duplicate symbol error] | ||||
| // } | ||||
|  | ||||
|  | ||||
| /// Select keypair for the session | ||||
| #[wasm_bindgen] | ||||
| pub fn select_keypair(key_id: &str) -> Result<(), JsValue> { | ||||
|     let mut result = Err(JsValue::from_str("Session not initialized")); | ||||
|     SESSION_MANAGER.with(|cell| { | ||||
|         if let Some(session) = cell.borrow_mut().as_mut() { | ||||
|             result = session | ||||
|                 .select_keypair(key_id) | ||||
|                 .map_err(|e| JsValue::from_str(&format!("select_keypair error: {e}"))); | ||||
|         } | ||||
|     }); | ||||
|     result | ||||
| } | ||||
|  | ||||
|  | ||||
| /// List keypairs in the current session's keyspace | ||||
| #[wasm_bindgen] | ||||
| pub async fn list_keypairs() -> Result<JsValue, JsValue> { | ||||
|     SESSION_MANAGER.with(|cell| { | ||||
|         if let Some(session) = cell.borrow().as_ref() { | ||||
|             if let Some(keyspace) = session.current_keyspace() { | ||||
|                 let keypairs = &keyspace.keypairs; | ||||
|                 serde_json::to_string(keypairs) | ||||
|                     .map(|s| JsValue::from_str(&s)) | ||||
|                     .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}"))) | ||||
|             } else { | ||||
|                 Err(JsValue::from_str("No keyspace unlocked")) | ||||
|             } | ||||
|         } else { | ||||
|             Err(JsValue::from_str("Session not initialized")) | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| /// Add a keypair to the current keyspace | ||||
| #[wasm_bindgen] | ||||
| pub async fn add_keypair( | ||||
|     key_type: Option<String>, | ||||
|     metadata: Option<String>, | ||||
| ) -> Result<JsValue, JsValue> { | ||||
|     use vault::{KeyMetadata, KeyType}; | ||||
|     let password = SESSION_PASSWORD | ||||
|         .with(|pw| pw.borrow().clone()) | ||||
|         .ok_or_else(|| JsValue::from_str("Session password not set"))?; | ||||
|     let (keyspace_name, session_exists) = SESSION_MANAGER.with(|cell| { | ||||
|         if let Some(ref session) = cell.borrow().as_ref() { | ||||
|             let keyspace_name = session.current_keyspace().map(|_| "".to_string()); // TODO: replace with actual keyspace name if available; | ||||
|             (keyspace_name, true) | ||||
|         } else { | ||||
|             (None, false) | ||||
|         } | ||||
|     }); | ||||
|     let keyspace_name = keyspace_name.ok_or_else(|| JsValue::from_str("No keyspace selected"))?; | ||||
|     if !session_exists { | ||||
|         return Err(JsValue::from_str("Session not initialized")); | ||||
|     } | ||||
|     let key_type = key_type | ||||
|         .as_deref() | ||||
|         .map(|s| match s { | ||||
|             "Ed25519" => KeyType::Ed25519, | ||||
|             "Secp256k1" => KeyType::Secp256k1, | ||||
|             _ => KeyType::Secp256k1, | ||||
|         }) | ||||
|         .unwrap_or(KeyType::Secp256k1); | ||||
|     let metadata = match metadata { | ||||
|         Some(ref meta_str) => Some( | ||||
|             serde_json::from_str::<KeyMetadata>(meta_str) | ||||
|                 .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {e}")))?, | ||||
|         ), | ||||
|         None => None, | ||||
|     }; | ||||
|     // Take session out, do async work, then put it back | ||||
|     let mut session_opt = SESSION_MANAGER.with(|cell| cell.borrow_mut().take()); | ||||
|     let session = session_opt.as_mut().ok_or_else(|| JsValue::from_str("Session not initialized"))?; | ||||
|     let key_id = session | ||||
|         .get_vault_mut() | ||||
|         .add_keypair(&keyspace_name, &password, Some(key_type), metadata) | ||||
|         .await | ||||
|         .map_err(|e| JsValue::from_str(&format!("add_keypair error: {e}")))?; | ||||
|     // Put session back | ||||
|     SESSION_MANAGER.with(|cell| *cell.borrow_mut() = Some(session_opt.take().unwrap())); | ||||
|     Ok(JsValue::from_str(&key_id)) | ||||
| } | ||||
|  | ||||
| /// Sign message with current session | ||||
| #[wasm_bindgen] | ||||
| pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> { | ||||
|     { | ||||
|         // SAFETY: We only use this pointer synchronously within this function, and SESSION_MANAGER outlives this scope. | ||||
|         let session_ptr = | ||||
|             SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _)); | ||||
|         let password_opt = SESSION_PASSWORD.with(|pw| pw.borrow().clone()); | ||||
|         let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr { | ||||
|             Some(ptr) => unsafe { &*ptr }, | ||||
|             None => return Err(JsValue::from_str("Session not initialized")), | ||||
|         }; | ||||
|         let password = match password_opt { | ||||
|             Some(p) => p, | ||||
|             None => return Err(JsValue::from_str("Session password not set")), | ||||
|         }; | ||||
|         match session.sign(message).await { | ||||
|             Ok(sig_bytes) => { | ||||
|                 let hex_sig = hex::encode(&sig_bytes); | ||||
|                 Ok(JsValue::from_str(&hex_sig)) | ||||
|             } | ||||
|             Err(e) => Err(JsValue::from_str(&format!("Sign error: {e}"))), | ||||
|         } | ||||
|     } | ||||
| } | ||||