Add kv-wasm: the core compiled to WebAssembly for the web vault

New crate crates/kv-wasm runs the same crypto, item model and sync engine in the browser:
- WebStore: in-memory replica of kv-store's Store, each changed entry written to IndexedDB
  (ciphertext only) before the JS call resolves.
- Device key: non-extractable WebCrypto ECDSA P-256 key in IndexedDB (raw r||s signatures).
- WebCore mirrors kv-ffi's VaultCore; wasm-bindgen surface in camelCase with TypeScript types,
  plus a Web Worker wrapper (connectVault) so Argon2id never blocks the page.
- Tests: Chrome (IndexedDB, WebCrypto, RFC 9421 verify), shared envelope fixtures both ways
  with kv-ffi, native sync with kv-ffi via the test server, Node smoke of the built package.

Small changes to shared crates so they build for wasm32:
- kv-store: SqliteStore moved to a native-only module.
- kv-sync: rustls/pinning/tokio native only (fetch on wasm); DeviceSigner::sign_async (default
  wraps sign) used by ApiClient; unix_now() instead of SystemTime/now_utc.
- kv-model: Public Suffix List behind a default `psl` feature (kv-ffi keeps it; the web build
  drops ~0.9 MB of code and matches same-host only).
- wasm-release profile (opt-level z, Argon2 at 3) and a wasm CI job.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-17 23:58:07 +09:00
co-authored by Claude Opus 5.5
parent f20380d293
commit b5ced32be3
35 changed files with 4488 additions and 363 deletions
+10 -9
View File
@@ -1,9 +1,6 @@
//! Typed HTTP client for the sync API (docs/03, api/openapi.yaml).
use std::{
sync::{Arc, Mutex},
time::{SystemTime, UNIX_EPOCH},
};
use std::sync::{Arc, Mutex};
use kv_proto::{
AccountInfo, ApiError, Changes, DeviceEnroll, DeviceInfo, ItemPut, ItemRecord, KdfParams, LoginFinish,
@@ -39,10 +36,6 @@ pub struct ApiClient {
session: Mutex<Option<String>>,
}
fn now_unix() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
impl ApiClient {
pub fn new(cfg: &ServerConfig, signer: Arc<dyn DeviceSigner>) -> Result<Self, SyncError> {
Ok(Self {
@@ -88,7 +81,15 @@ impl ApiClient {
}
if signed {
let session = self.session().ok_or(SyncError::NotAuthenticated)?;
let h = signing::sign_request(self.signer.as_ref(), method.as_str(), &url, &body, &session, now_unix())?;
let h = signing::sign_request_async(
self.signer.as_ref(),
method.as_str(),
&url,
&body,
&session,
crate::unix_now(),
)
.await?;
for (k, v) in h.pairs() {
req = req.header(k, v);
}