kv-wasm: onboarding (setup codes, enrollment tickets, scan-to-join) for the web vault

Mirrors kv-ffi's docs/10 §5 API in WebCore and binds it under the names the web app codes
against: setupPayloadForKit, addDevicePayload, revokeEnrollmentTicket, loginWithSetup,
joinStart/joinPoll/joinCancel, joinApprove, joinSend, and the free function parseSetupPayload
(also callable through the worker client).

Unlike kv-ffi, the Secret Key is not persisted: every unlock on the web takes it, so it is
kept zeroized in the unlocked state and dropped on lock. loginWithSetup refuses a code for a
different server than the configured origin (the browser only talks to its own origin).

Tests: ticket enrollment and scan-to-join in both roles against kv-ffi via the kv-sync test
server, a swapped-approver rejection, a Chrome test (IndexedDB never holds the Secret Key,
JS shapes, error codes) and a Node smoke check of the built package.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-18 06:58:13 +09:00
co-authored by Claude Opus 5.5
parent d70beef92c
commit 2e2c8cd26f
11 changed files with 779 additions and 14 deletions
+15
View File
@@ -92,6 +92,21 @@ assert.equal(kv.generatePassword(20, false).length, 20);
const exported = await a.exportEncrypted("export passphrase");
assert.ok(exported instanceof Uint8Array && exported.length > 100);
// Onboarding (docs/10): the Kit's Setup Code round-trips through the free parser.
const k = await kv.VaultCore.newInMemory();
const kSecret = await k.createAccount(PW);
await k.configureServer("https://vault.example.org", []);
const setup = kv.parseSetupPayload(await k.setupPayloadForKit());
assert.deepEqual(setup, {
serverUrl: "https://vault.example.org",
spkiPins: [],
accountId: await k.accountId(),
secretKey: kSecret,
ticket: null,
});
await rejects(k.joinPoll(), "Invalid");
k.free();
if (process.argv.includes("--write-fixture")) {
const items = [];
for (const s of await a.listItems()) items.push(await a.getItem(s.id));