feat: Add WASM support and browser extension infrastructure
- Add WASM build target and dependencies for all crates. - Implement IndexedDB-based persistent storage for WASM. - Create browser extension infrastructure (UI, scripting, etc.). - Integrate Rhai scripting engine for secure automation. - Implement user stories and documentation for the extension.
This commit is contained in:
28
wasm/Cargo.toml
Normal file
28
wasm/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
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"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
rhai = { version = "1.16", features = ["serde"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
once_cell = "1.21"
|
||||
vault = { path = "../vault" }
|
||||
evm_client = { path = "../evm_client" }
|
||||
|
||||
[dev-dependencies]
|
||||
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"] }
|
153
wasm/src/lib.rs
Normal file
153
wasm/src/lib.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
//! WASM entrypoint for Rhai scripting integration for the extension.
|
||||
//! Composes vault and evm_client Rhai bindings and exposes a secure run_rhai API.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use once_cell::unsync::Lazy;
|
||||
use std::cell::RefCell;
|
||||
|
||||
|
||||
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
use rhai::Engine;
|
||||
use vault::session::SessionManager;
|
||||
use vault::rhai_bindings as vault_rhai_bindings;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use kvstore::wasm::WasmStore;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
// Global singleton engine/session/client (for demonstration; production should scope per user/session)
|
||||
thread_local! {
|
||||
static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new()));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
static SESSION_MANAGER: RefCell<Option<Arc<Mutex<SessionManager<kvstore::native::NativeStore>>>>> = RefCell::new(None);
|
||||
static SESSION_PASSWORD: RefCell<Option<Vec<u8>>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use vault::session_singleton::SESSION_MANAGER;
|
||||
|
||||
|
||||
/// Initialize the scripting environment (must be called before run_rhai)
|
||||
#[wasm_bindgen]
|
||||
pub fn init_rhai_env() {
|
||||
ENGINE.with(|engine_cell| {
|
||||
let mut engine = engine_cell.borrow_mut();
|
||||
// Register APIs with dummy session; will be replaced by real session after init
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(ref session) = cell.borrow().as_ref() {
|
||||
vault_rhai_bindings::register_rhai_api(&mut engine, session);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(session) = cell.borrow().as_ref() {
|
||||
vault_rhai_bindings::register_rhai_api(&mut engine, session.clone());
|
||||
}
|
||||
});
|
||||
// TODO: Register EVM APIs with session if needed
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/// Initialize session with keyspace and password
|
||||
#[wasm_bindgen]
|
||||
pub fn init_session(keyspace: &str, password: &str) -> Result<(), JsValue> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use kvstore::wasm::WasmStore;
|
||||
use vault::session::SessionManager;
|
||||
let keyspace = keyspace.to_string();
|
||||
let password_vec = password.as_bytes().to_vec();
|
||||
spawn_local(async move {
|
||||
match WasmStore::open(&keyspace).await {
|
||||
Ok(store) => {
|
||||
let vault = vault::Vault::new(store);
|
||||
let mut manager = SessionManager::new(vault);
|
||||
if let Err(e) = manager.unlock_keyspace(&keyspace, &password_vec).await {
|
||||
web_sys::console::error_1(&format!("Failed to unlock keyspace: {e}").into());
|
||||
return;
|
||||
}
|
||||
SESSION_MANAGER.with(|cell| cell.replace(Some(manager)));
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::error_1(&format!("Failed to open WasmStore: {e}").into());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let store = kvstore::native::NativeStore::open("testdb").expect("open native store");
|
||||
let vault = vault::Vault::new(store);
|
||||
let manager = SessionManager::new(vault);
|
||||
use std::sync::{Arc, Mutex};
|
||||
let arc_manager = Arc::new(Mutex::new(manager));
|
||||
SESSION_MANAGER.with(|cell| cell.replace(Some(arc_manager)));
|
||||
}
|
||||
SESSION_PASSWORD.with(|cell| cell.replace(Some(password.as_bytes().to_vec())));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select keypair for the session
|
||||
#[wasm_bindgen]
|
||||
pub fn select_keypair(key_id: &str) -> Result<(), JsValue> {
|
||||
let mut result = Err(JsValue::from_str("Session not initialized"));
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(session) = cell.borrow_mut().as_mut() {
|
||||
result = session.select_keypair(key_id)
|
||||
.map_err(|e| JsValue::from_str(&format!("select_keypair error: {e}")));
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(session_arc) = cell.borrow_mut().as_mut() {
|
||||
let mut session = session_arc.lock().unwrap();
|
||||
result = session.select_keypair(key_id)
|
||||
.map_err(|e| JsValue::from_str(&format!("select_keypair error: {e}")));
|
||||
}
|
||||
});
|
||||
result
|
||||
}
|
||||
|
||||
/// Lock the session (zeroize password and session)
|
||||
#[wasm_bindgen]
|
||||
pub fn lock_session() {
|
||||
SESSION_MANAGER.with(|cell| *cell.borrow_mut() = None);
|
||||
SESSION_PASSWORD.with(|cell| *cell.borrow_mut() = None);
|
||||
}
|
||||
|
||||
/// Sign message with current session
|
||||
#[wasm_bindgen]
|
||||
pub fn sign(message: &[u8]) -> Result<JsValue, JsValue> {
|
||||
let mut result: Option<Result<JsValue, JsValue>> = None;
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
if let Some(session) = cell.borrow().as_ref() {
|
||||
let password = SESSION_PASSWORD.with(|pw| pw.borrow().clone());
|
||||
// ...rest of sign logic here, using session and password...
|
||||
// For now, just set result = Ok(JsValue::from_str("signed")); as a placeholder
|
||||
result = Some(Ok(JsValue::from_str("signed")));
|
||||
}
|
||||
});
|
||||
result.unwrap_or_else(|| Err(JsValue::from_str("Session not initialized")))
|
||||
}
|
||||
|
||||
/// Securely run a Rhai script in the extension context (must be called only after user approval)
|
||||
#[wasm_bindgen]
|
||||
pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> {
|
||||
ENGINE.with(|engine_cell| {
|
||||
let mut engine = engine_cell.borrow_mut();
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
if let Some(ref mut session) = cell.borrow_mut().as_mut() {
|
||||
let mut scope = rhai::Scope::new();
|
||||
engine.eval_with_scope::<rhai::Dynamic>(&mut scope, script)
|
||||
.map(|result| JsValue::from_str(&result.to_string()))
|
||||
.map_err(|e| JsValue::from_str(&format!("Rhai error: {e}")))
|
||||
} else {
|
||||
Err(JsValue::from_str("Session not initialized"))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
0
wasm/src/session_singleton.rs
Normal file
0
wasm/src/session_singleton.rs
Normal file
0
wasm/tests/wasm_rhai.rs
Normal file
0
wasm/tests/wasm_rhai.rs
Normal file
Reference in New Issue
Block a user