diff --git a/.gitignore b/.gitignore index 3a9cbb8..7bc95e3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ .vscode/ # Ignore test databases -/vault/vault_native_test/ \ No newline at end of file +/vault/vault_native_test/ +node_modules/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 7bcebb6..1d6e4d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,6 @@ members = [ "kvstore", "vault", "evm_client", - "wasm", + "wasm_app", ] diff --git a/Makefile b/Makefile index 342ee66..49493e3 100644 --- a/Makefile +++ b/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 + diff --git a/docs/extension_architecture.md b/docs/extension_architecture.md index d533065..d7c792d 100644 --- a/docs/extension_architecture.md +++ b/docs/extension_architecture.md @@ -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 { + // ... + } + + // 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 { + // ... +} +// 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. diff --git a/docs/kvstore.md b/docs/kvstore.md index 8da651c..2dd7efb 100644 --- a/docs/kvstore.md +++ b/docs/kvstore.md @@ -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 diff --git a/docs/repo_structure.md b/docs/repo_structure.md index 8e03957..c51d0d3 100644 --- a/docs/repo_structure.md +++ b/docs/repo_structure.md @@ -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. --- diff --git a/evm_client/Cargo.toml b/evm_client/Cargo.toml index 4f50526..b45fff0 100644 --- a/evm_client/Cargo.toml +++ b/evm_client/Cargo.toml @@ -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"] } diff --git a/evm_client/src/error.rs b/evm_client/src/error.rs new file mode 100644 index 0000000..82efa09 --- /dev/null +++ b/evm_client/src/error.rs @@ -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), +} diff --git a/evm_client/src/lib.rs b/evm_client/src/lib.rs index b407f36..264deae 100644 --- a/evm_client/src/lib.rs +++ b/evm_client/src/lib.rs @@ -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, +} + 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 { - // 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 { - // 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 { + // 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 { + 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) } } diff --git a/evm_client/src/provider.rs b/evm_client/src/provider.rs index 214bb00..4391697 100644 --- a/evm_client/src/provider.rs +++ b/evm_client/src/provider.rs @@ -30,13 +30,13 @@ pub async fn send_rpc(url: &str, body: &str) -> Result> { } } 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, + pub gas_price: Option, + pub nonce: Option, + pub chain_id: Option, } impl Transaction { @@ -94,5 +94,4 @@ pub async fn get_balance(url: &str, address: Address) -> Result Result { - // 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 { + 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 { - // 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::(); 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. diff --git a/evm_client/src/rhai_sync_helpers.rs b/evm_client/src/rhai_sync_helpers.rs index 6d80fab..af4165b 100644 --- a/evm_client/src/rhai_sync_helpers.rs +++ b/evm_client/src/rhai_sync_helpers.rs @@ -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 { 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 { 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}")) }) } diff --git a/evm_client/src/signer.rs b/evm_client/src/signer.rs index 1ebd8a9..5412393 100644 --- a/evm_client/src/signer.rs +++ b/evm_client/src/signer.rs @@ -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, 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, EvmError>; - fn address(&self) -> String; -} - - - -// --- Implementation for vault::SessionManager --- - -#[cfg(target_arch = "wasm32")] -#[async_trait::async_trait(?Send)] -impl Signer for vault::SessionManager { - async fn sign(&self, message: &[u8]) -> Result, 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 Signer for vault::SessionManager { - async fn sign(&self, message: &[u8]) -> Result, 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() - } -} diff --git a/evm_client/tests/balance.rs b/evm_client/tests/balance.rs index b3c1193..84115ef 100644 --- a/evm_client/tests/balance.rs +++ b/evm_client/tests/balance.rs @@ -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"); } diff --git a/evm_client/tests/wasm.rs b/evm_client/tests/wasm.rs index c998c89..2f014f9 100644 --- a/evm_client/tests/wasm.rs +++ b/evm_client/tests/wasm.rs @@ -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]; diff --git a/extension/README.md b/extension/README.md new file mode 100644 index 0000000..e8cd45c --- /dev/null +++ b/extension/README.md @@ -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) diff --git a/extension/assets/icon-128.png b/extension/assets/icon-128.png new file mode 100644 index 0000000..e5869e4 Binary files /dev/null and b/extension/assets/icon-128.png differ diff --git a/extension/assets/icon-16.png b/extension/assets/icon-16.png new file mode 100644 index 0000000..5348679 Binary files /dev/null and b/extension/assets/icon-16.png differ diff --git a/extension/assets/icon-32.png b/extension/assets/icon-32.png new file mode 100644 index 0000000..d761cc8 Binary files /dev/null and b/extension/assets/icon-32.png differ diff --git a/extension/assets/icon-48.png b/extension/assets/icon-48.png new file mode 100644 index 0000000..3d34ec4 Binary files /dev/null and b/extension/assets/icon-48.png differ diff --git a/extension/background/index.js b/extension/background/index.js new file mode 100644 index 0000000..9f62297 --- /dev/null +++ b/extension/background/index.js @@ -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 + } +}); diff --git a/extension/build.js b/extension/build.js new file mode 100644 index 0000000..795a889 --- /dev/null +++ b/extension/build.js @@ -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.'); diff --git a/extension/dist/assets/icon-128.png b/extension/dist/assets/icon-128.png new file mode 100644 index 0000000..e5869e4 Binary files /dev/null and b/extension/dist/assets/icon-128.png differ diff --git a/extension/dist/assets/icon-16.png b/extension/dist/assets/icon-16.png new file mode 100644 index 0000000..5348679 Binary files /dev/null and b/extension/dist/assets/icon-16.png differ diff --git a/extension/dist/assets/icon-32.png b/extension/dist/assets/icon-32.png new file mode 100644 index 0000000..d761cc8 Binary files /dev/null and b/extension/dist/assets/icon-32.png differ diff --git a/extension/dist/assets/icon-48.png b/extension/dist/assets/icon-48.png new file mode 100644 index 0000000..3d34ec4 Binary files /dev/null and b/extension/dist/assets/icon-48.png differ diff --git a/extension/dist/assets/popup.js b/extension/dist/assets/popup.js new file mode 100644 index 0000000..d8669cb --- /dev/null +++ b/extension/dist/assets/popup.js @@ -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=""})(); diff --git a/extension/dist/background/index.js b/extension/dist/background/index.js new file mode 100644 index 0000000..9f62297 --- /dev/null +++ b/extension/dist/background/index.js @@ -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 + } +}); diff --git a/extension/dist/manifest.json b/extension/dist/manifest.json new file mode 100644 index 0000000..31480f1 --- /dev/null +++ b/extension/dist/manifest.json @@ -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": [""] + } + ] +} diff --git a/extension/dist/popup/index.html b/extension/dist/popup/index.html new file mode 100644 index 0000000..264c95f --- /dev/null +++ b/extension/dist/popup/index.html @@ -0,0 +1,13 @@ + + + + + + Modular Vault Extension + + + +
+ + + diff --git a/extension/dist/popup/popup.css b/extension/dist/popup/popup.css new file mode 100644 index 0000000..64a05e8 --- /dev/null +++ b/extension/dist/popup/popup.css @@ -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; +} diff --git a/extension/dist/popup/popup.js b/extension/dist/popup/popup.js new file mode 100644 index 0000000..8335e64 --- /dev/null +++ b/extension/dist/popup/popup.js @@ -0,0 +1,306 @@ +// Simple non-module JavaScript for browser extension popup +document.addEventListener('DOMContentLoaded', async function() { + const root = document.getElementById('root'); + root.innerHTML = ` +
+

Modular Vault Extension

+
Loading WASM module...
+ +
+
+

Session

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + // 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 = ` + ${keypair.label || keypair.id} + + `; + 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!'; + }); +}); diff --git a/extension/dist/wasm/wasm_app.js b/extension/dist/wasm/wasm_app.js new file mode 100644 index 0000000..10f8ade --- /dev/null +++ b/extension/dist/wasm/wasm_app.js @@ -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} + */ +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} + */ +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} + */ +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} + */ +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; diff --git a/extension/dist/wasm/wasm_app_bg.wasm b/extension/dist/wasm/wasm_app_bg.wasm new file mode 100644 index 0000000..d2d2dc7 Binary files /dev/null and b/extension/dist/wasm/wasm_app_bg.wasm differ diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..31480f1 --- /dev/null +++ b/extension/manifest.json @@ -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": [""] + } + ] +} diff --git a/extension/package-lock.json b/extension/package-lock.json new file mode 100644 index 0000000..c11b36f --- /dev/null +++ b/extension/package-lock.json @@ -0,0 +1,1474 @@ +{ + "name": "modular-vault-extension", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "modular-vault-extension", + "version": "0.1.0", + "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" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@swc/core": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", + "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.24", + "@swc/core-darwin-x64": "1.11.24", + "@swc/core-linux-arm-gnueabihf": "1.11.24", + "@swc/core-linux-arm64-gnu": "1.11.24", + "@swc/core-linux-arm64-musl": "1.11.24", + "@swc/core-linux-x64-gnu": "1.11.24", + "@swc/core-linux-x64-musl": "1.11.24", + "@swc/core-win32-arm64-msvc": "1.11.24", + "@swc/core-win32-ia32-msvc": "1.11.24", + "@swc/core-win32-x64-msvc": "1.11.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", + "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", + "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", + "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", + "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", + "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", + "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", + "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", + "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", + "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", + "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.5.0.tgz", + "integrity": "sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-virtual": "^3.0.2", + "@swc/core": "^1.10.16", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.4.1.tgz", + "integrity": "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/extension/package.json b/extension/package.json new file mode 100644 index 0000000..20e4478 --- /dev/null +++ b/extension/package.json @@ -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" + } +} diff --git a/extension/popup/App.jsx b/extension/popup/App.jsx new file mode 100644 index 0000000..d1bc887 --- /dev/null +++ b/extension/popup/App.jsx @@ -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 ( +
+

Modular Vault Extension

+ {wasmState.error && ( +
+ WebAssembly Error: {wasmState.error} +
+ )} + + {!locked && ( + <> + + {selectedKeypair && ( + + )} + + )} +
+ {status} +
+
+ ); +} + +export default App; diff --git a/extension/popup/KeypairManager.jsx b/extension/popup/KeypairManager.jsx new file mode 100644 index 0000000..a589d78 --- /dev/null +++ b/extension/popup/KeypairManager.jsx @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; + +export default function KeypairManager({ keypairs, onSelect, onCreate, selectedKeypair }) { + const [creating, setCreating] = useState(false); + + return ( +
+ + + + {creating && ( +
+ + +
+ )} + {selectedKeypair && ( +
+ Public Key: {keypairs.find(kp => kp.id === selectedKeypair)?.publicKey} + +
+ )} +
+ ); +} diff --git a/extension/popup/KeyspaceManager.jsx b/extension/popup/KeyspaceManager.jsx new file mode 100644 index 0000000..577a00a --- /dev/null +++ b/extension/popup/KeyspaceManager.jsx @@ -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 ( +
+ + + +
+ setNewKeyspace(e.target.value)} /> + setPassword(e.target.value)} /> + +
+
+ ); + } + return ( +
+ Keyspace: {currentKeyspace} + +
+ ); +} diff --git a/extension/popup/SignMessage.jsx b/extension/popup/SignMessage.jsx new file mode 100644 index 0000000..1859009 --- /dev/null +++ b/extension/popup/SignMessage.jsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; + +export default function SignMessage({ onSign, signature, loading }) { + const [message, setMessage] = useState(''); + + return ( +
+ + setMessage(e.target.value)} + style={{width: '100%', marginBottom: 8}} + /> + + {signature && ( +
+ Signature: {signature} + +
+ )} +
+ ); +} diff --git a/extension/popup/WasmHelper.js b/extension/popup/WasmHelper.js new file mode 100644 index 0000000..f3f3e94 --- /dev/null +++ b/extension/popup/WasmHelper.js @@ -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} + */ +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} 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} 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} 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} 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} + */ +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} + */ +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} 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 }); + }); + }); +} diff --git a/extension/popup/WasmLoader.jsx b/extension/popup/WasmLoader.jsx new file mode 100644 index 0000000..6381067 --- /dev/null +++ b/extension/popup/WasmLoader.jsx @@ -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
Loading WebAssembly module...
; + } + + if (error) { + return
Error: {error}
; + } + + return ( + + {children} + + ); +} diff --git a/extension/popup/debug_rhai.js b/extension/popup/debug_rhai.js new file mode 100644 index 0000000..48c09e4 --- /dev/null +++ b/extension/popup/debug_rhai.js @@ -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; +} diff --git a/extension/popup/index.html b/extension/popup/index.html new file mode 100644 index 0000000..264c95f --- /dev/null +++ b/extension/popup/index.html @@ -0,0 +1,13 @@ + + + + + + Modular Vault Extension + + + +
+ + + diff --git a/extension/popup/index.jsx b/extension/popup/index.jsx new file mode 100644 index 0000000..2620404 --- /dev/null +++ b/extension/popup/index.jsx @@ -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(); diff --git a/extension/popup/popup.css b/extension/popup/popup.css new file mode 100644 index 0000000..64a05e8 --- /dev/null +++ b/extension/popup/popup.css @@ -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; +} diff --git a/extension/popup/popup.js b/extension/popup/popup.js new file mode 100644 index 0000000..8335e64 --- /dev/null +++ b/extension/popup/popup.js @@ -0,0 +1,306 @@ +// Simple non-module JavaScript for browser extension popup +document.addEventListener('DOMContentLoaded', async function() { + const root = document.getElementById('root'); + root.innerHTML = ` +
+

Modular Vault Extension

+
Loading WASM module...
+ +
+
+

Session

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + // 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 = ` + ${keypair.label || keypair.id} + + `; + 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!'; + }); +}); diff --git a/extension/popup/style.css b/extension/popup/style.css new file mode 100644 index 0000000..91d4571 --- /dev/null +++ b/extension/popup/style.css @@ -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; +} diff --git a/extension/popup/wasm.js b/extension/popup/wasm.js new file mode 100644 index 0000000..daa357c --- /dev/null +++ b/extension/popup/wasm.js @@ -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'); + } + }); +} diff --git a/extension/public/background/index.js b/extension/public/background/index.js new file mode 100644 index 0000000..781c532 --- /dev/null +++ b/extension/public/background/index.js @@ -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; + } +}); diff --git a/extension/public/wasm/wasm_app.js b/extension/public/wasm/wasm_app.js new file mode 100644 index 0000000..10f8ade --- /dev/null +++ b/extension/public/wasm/wasm_app.js @@ -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} + */ +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} + */ +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} + */ +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} + */ +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; diff --git a/extension/public/wasm/wasm_app_bg.wasm b/extension/public/wasm/wasm_app_bg.wasm new file mode 100644 index 0000000..d2d2dc7 Binary files /dev/null and b/extension/public/wasm/wasm_app_bg.wasm differ diff --git a/extension/vite.config.js b/extension/vite.config.js new file mode 100644 index 0000000..d293970 --- /dev/null +++ b/extension/vite.config.js @@ -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: ['../'] + } + } +}); diff --git a/kvstore/Cargo.toml b/kvstore/Cargo.toml index fed9340..65528e4 100644 --- a/kvstore/Cargo.toml +++ b/kvstore/Cargo.toml @@ -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] diff --git a/kvstore/src/error.rs b/kvstore/src/error.rs index fbcfa32..74ffa67 100644 --- a/kvstore/src/error.rs +++ b/kvstore/src/error.rs @@ -22,3 +22,12 @@ pub enum KVError { } pub type Result = std::result::Result; + +// Allow automatic conversion from idb::Error to KVError +#[cfg(target_arch = "wasm32")] +impl From for KVError { + fn from(e: idb::Error) -> Self { + KVError::Other(format!("idb error: {e:?}")) + } +} + diff --git a/kvstore/src/wasm.rs b/kvstore/src/wasm.rs index 8ff4f56..7d9d9df 100644 --- a/kvstore/src/wasm.rs +++ b/kvstore/src/wasm.rs @@ -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::>() { + 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 { @@ -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(()) } diff --git a/kvstore/tests/native.rs b/kvstore/tests/native.rs index 77a709c..15e03a2 100644 --- a/kvstore/tests/native.rs +++ b/kvstore/tests/native.rs @@ -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())); + } +} + diff --git a/kvstore/tests/web.rs b/kvstore/tests/web.rs index 75b1c29..1198738 100644 --- a/kvstore/tests/web.rs +++ b/kvstore/tests/web.rs @@ -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(); diff --git a/vault/Cargo.toml b/vault/Cargo.toml index d3c7a9f..46e340d 100644 --- a/vault/Cargo.toml +++ b/vault/Cargo.toml @@ -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 = [] \ No newline at end of file diff --git a/vault/src/data.rs b/vault/src/data.rs index 3ecaa88..1ce675b 100644 --- a/vault/src/data.rs +++ b/vault/src/data.rs @@ -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, // ... 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, pub created_at: Option, diff --git a/vault/src/rhai_bindings.rs b/vault/src/rhai_bindings.rs index 7d260d3..0c64ce9 100644 --- a/vault/src/rhai_bindings.rs +++ b/vault/src/rhai_bindings.rs @@ -39,7 +39,7 @@ impl RhaiSessionMan pub fn sign(&self, message: rhai::Blob) -> Result { 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::( &sm, @@ -50,14 +50,46 @@ impl RhaiSessionMan #[cfg(target_arch = "wasm32")] impl RhaiSessionManager { - // 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 { + 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 { + // 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( engine: &mut Engine, - session_manager: &SessionManager, + // session_manager: &SessionManager, ) { // WASM registration logic (adapt as needed) // Example: engine.register_type::>(); @@ -66,7 +98,7 @@ pub fn register_rhai_api( 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 { + engine.register_fn("sign", |_message: rhai::Blob| -> Result { 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 diff --git a/vault/src/rhai_sync_helpers.rs b/vault/src/rhai_sync_helpers.rs index a408673..cf4a872 100644 --- a/vault/src/rhai_sync_helpers.rs +++ b/vault/src/rhai_sync_helpers.rs @@ -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; diff --git a/vault/src/session.rs b/vault/src/session.rs index b9b370c..d1ecc9b 100644 --- a/vault/src/session.rs +++ b/vault/src/session.rs @@ -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 { vault: Vault, - unlocked_keyspaces: HashMap, KeyspaceData)>, // name -> (password, data) - current_keyspace: Option, + unlocked_keyspace: Option<(String, Vec, KeyspaceData)>, // (name, password, data) current_keypair: Option, } #[cfg(target_arch = "wasm32")] pub struct SessionManager { vault: Vault, - unlocked_keyspaces: HashMap, KeyspaceData)>, // name -> (password, data) - current_keyspace: Option, + unlocked_keyspace: Option<(String, Vec, KeyspaceData)>, // (name, password, data) current_keypair: Option, } -#[cfg(not(target_arch = "wasm32"))] -impl SessionManager { - /// Create a new session manager from a Vault instance. - pub fn new(vault: Vault) -> Self { - Self { - vault, - unlocked_keyspaces: HashMap::new(), - current_keyspace: None, - current_keypair: None, - } - } -} - #[cfg(target_arch = "wasm32")] impl SessionManager { - /// Create a new session manager from a Vault instance. - pub fn new(vault: Vault) -> Self { - Self { - vault, - unlocked_keyspaces: HashMap::new(), - current_keyspace: None, - current_keypair: None, - } + pub fn get_vault_mut(&mut self) -> &mut Vault { + &mut self.vault } } -// Native impl for all methods #[cfg(not(target_arch = "wasm32"))] impl SessionManager { - /// Unlock a keyspace and store its decrypted data in memory. + pub fn new(vault: Vault) -> Self { + Self { + vault, + unlocked_keyspace: None, + current_keypair: None, + } + } + + pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option>) -> 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 SessionManager { } } - /// 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, 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 { - &self.vault - } -} - -// WASM impl for all methods -#[cfg(target_arch = "wasm32")] -impl SessionManager { - /// 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, + metadata: Option, + ) -> Result { + 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, 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 { &self.vault } -} -// Shared impl for methods needed by Drop -#[cfg(not(target_arch = "wasm32"))] -impl SessionManager { - /// 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 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 SessionManager { - /// 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 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 Drop for SessionManager { fn drop(&mut self) { self.logout(); } } + +#[cfg(target_arch = "wasm32")] +impl SessionManager { + pub fn new(vault: Vault) -> Self { + Self { + vault, + unlocked_keyspace: None, + current_keypair: None, + } + } + + pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option>) -> 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, + metadata: Option, + ) -> Result { + 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, 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 { + &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 Drop for SessionManager { + fn drop(&mut self) { + self.logout(); + } +} diff --git a/vault/tests/session_manager.rs b/vault/tests/session_manager.rs index 52c034e..e37b524 100644 --- a/vault/tests/session_manager.rs +++ b/vault/tests/session_manager.rs @@ -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()); } diff --git a/vault/tests/wasm_keypair_management.rs b/vault/tests/wasm_keypair_management.rs index 4cd3835..78d1fe4 100644 --- a/vault/tests/wasm_keypair_management.rs +++ b/vault/tests/wasm_keypair_management.rs @@ -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(); diff --git a/vault/tests/wasm_session_manager.rs b/vault/tests/wasm_session_manager.rs index 5893ed8..ca744a0 100644 --- a/vault/tests/wasm_session_manager.rs +++ b/vault/tests/wasm_session_manager.rs @@ -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::>(); + 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::>(); + 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::>(); + 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"); } diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs deleted file mode 100644 index 3210f3a..0000000 --- a/wasm/src/lib.rs +++ /dev/null @@ -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> = Lazy::new(|| RefCell::new(Engine::new())); - #[cfg(not(target_arch = "wasm32"))] - static SESSION_MANAGER: RefCell>>>> = RefCell::new(None); - static SESSION_PASSWORD: RefCell>> = 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 { - let mut result: Option> = 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 { - 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::(&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")) - } - }) - }) -} diff --git a/wasm/src/session_singleton.rs b/wasm/src/session_singleton.rs deleted file mode 100644 index e69de29..0000000 diff --git a/wasm/tests/wasm_rhai.rs b/wasm/tests/wasm_rhai.rs deleted file mode 100644 index e69de29..0000000 diff --git a/wasm/Cargo.toml b/wasm_app/Cargo.toml similarity index 76% rename from wasm/Cargo.toml rename to wasm_app/Cargo.toml index 0ac23b2..f055212 100644 --- a/wasm/Cargo.toml +++ b/wasm_app/Cargo.toml @@ -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"] } diff --git a/wasm_app/src/debug_bindings.rs b/wasm_app/src/debug_bindings.rs new file mode 100644 index 0000000..b727c9d --- /dev/null +++ b/wasm_app/src/debug_bindings.rs @@ -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 { + 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 = 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 { + #[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> 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 + ))); + } + } + } +} \ No newline at end of file diff --git a/wasm_app/src/lib.rs b/wasm_app/src/lib.rs new file mode 100644 index 0000000..6a57bcd --- /dev/null +++ b/wasm_app/src/lib.rs @@ -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> = Lazy::new(|| RefCell::new(Engine::new())); + static SESSION_PASSWORD: RefCell>> = 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::(&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 { + 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::(&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()) + } + }) + } +} diff --git a/wasm_app/src/vault_bindings.rs b/wasm_app/src/vault_bindings.rs new file mode 100644 index 0000000..1ce274f --- /dev/null +++ b/wasm_app/src/vault_bindings.rs @@ -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> = Lazy::new(|| RefCell::new(Engine::new())); + static SESSION_PASSWORD: RefCell>> = 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 { +// // [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 { + 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, + metadata: Option, +) -> Result { + 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::(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 { + { + // 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 = 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}"))), + } + } +}