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]>
61 lines
2.0 KiB
JavaScript
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);
|
|
},
|
|
});
|
|
}
|