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:
2025-05-16 15:31:53 +03:00
parent 19f46d6edb
commit 13945a8725
25 changed files with 672 additions and 183 deletions

28
wasm/Cargo.toml Normal file
View 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
View 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"))
}
})
})
}

View File

0
wasm/tests/wasm_rhai.rs Normal file
View File