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:
@@ -0,0 +1,60 @@
|
||||
// Page-side handle to the vault worker. Every VaultCore method (and the free functions) becomes an
|
||||
// async call into the worker; errors come back as `CoreError`s with the same `code`s.
|
||||
//
|
||||
// import { connectVault } from "@kestrove/core/client.js";
|
||||
// const vault = await connectVault(); // IndexedDB "kestrove"
|
||||
// if (!(await vault.hasAccount())) { ... }
|
||||
// await vault.unlock(password, secretKey); // Argon2id runs in the worker
|
||||
// const items = await vault.listItems();
|
||||
|
||||
export async function connectVault(options = {}) {
|
||||
const worker =
|
||||
options.worker ?? new Worker(new URL("./worker.js", import.meta.url), { type: "module", name: "kestrove-vault" });
|
||||
let next = 1;
|
||||
const pending = new Map();
|
||||
|
||||
worker.onmessage = ({ data: { id, ok, err } }) => {
|
||||
const p = pending.get(id);
|
||||
if (!p) return;
|
||||
pending.delete(id);
|
||||
if (err) p.reject(Object.assign(new Error(err.message), err));
|
||||
else p.resolve(ok);
|
||||
};
|
||||
worker.onerror = (event) => {
|
||||
const e = Object.assign(new Error(event.message || "vault worker failed"), { name: "CoreError", code: "Internal" });
|
||||
for (const p of pending.values()) p.reject(e);
|
||||
pending.clear();
|
||||
};
|
||||
|
||||
const call = (method, args) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const id = next++;
|
||||
pending.set(id, { resolve, reject });
|
||||
worker.postMessage({ id, method, args });
|
||||
});
|
||||
|
||||
try {
|
||||
await call("open", [options.dbName ?? null, Boolean(options.inMemory)]);
|
||||
} catch (e) {
|
||||
worker.terminate(); // e.g. "InUse": the vault is open in another tab
|
||||
throw e;
|
||||
}
|
||||
|
||||
const handle = {
|
||||
/** Frees the core, releases the tab lock and stops the worker (key material goes with it). */
|
||||
async close() {
|
||||
try {
|
||||
await call("close", []);
|
||||
} finally {
|
||||
worker.terminate();
|
||||
}
|
||||
},
|
||||
};
|
||||
return new Proxy(handle, {
|
||||
get(target, prop) {
|
||||
if (prop in target) return target[prop];
|
||||
if (typeof prop !== "string" || prop === "then") return undefined;
|
||||
return (...args) => call(prop, args);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user