Files
kestrove-core/crates/kv-wasm/js/client.js
T
samiandClaude Opus 5.5 b5ced32be3 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]>
2026-10-17 23:58:07 +09:00

61 lines
2.0 KiB
JavaScript

// 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);
},
});
}