Removed devices: DeviceRevoked, identity reset and one retry with the ticket

kv-sync: SyncError::DeviceRevoked for 403 device_revoked (drops the session);
the test server answers it after OPAQUE (before the ticket) and for valid
signatures by a revoked device's key.

kv-ffi: CoreError::DeviceRevoked; the foreign DeviceSigner gains reset() (new key
and id). On device_revoked the core resets the identity, forgets the session
and the kept OPAQUE input (no silent re-enrolment) and keeps the vault data; a
login carrying a ticket retries once as the new device. reset_device_identity()
runs the reset on demand. Signed calls (sync, devices, tickets, join) reset too.

kv-wasm: DeviceRevoked; the browser binding replaces the WebCrypto key and id in
IndexedDB (same name and platform), loginWithSetup retries once with a ticket;
resetDeviceIdentity().

Tests: end to end revoked -> reset -> re-enrol with the same ticket (kv-ffi and
web core), no-ticket path, in-browser IndexedDB reset.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-20 22:25:03 +09:00
co-authored by Claude Opus 5.5
parent 460580bcc8
commit 83d56d43e2
16 changed files with 381 additions and 48 deletions
+14 -6
View File
@@ -80,7 +80,7 @@ parseSetupPayload(s): SetupInfo; // {serverUrl, s
Errors reject with an `Error` whose `name` is `"CoreError"` and whose `code` is one of kv-ffi's
`CoreError` cases (`Locked`, `NoAccount`, `BadCredentials`, `Invalid`, `NotFound`, `Internal`,
`NotConfigured`, `NotAuthenticated`, `PendingApproval`, `Network`, `Server` + `status`,
`NotConfigured`, `NotAuthenticated`, `PendingApproval`, `DeviceRevoked`, `Network`, `Server` + `status`,
`IntegrityAlarm` + `vaultId`/`reason`), plus `Busy` (a sync is already running) and, from the worker
client, `InUse` (another tab has the vault open).
@@ -125,14 +125,22 @@ two replicas overwriting each other.
`NotAuthenticated` (ask for the password and `login` again), `PendingApproval`,
`IntegrityAlarm` (stop and alert: the server rolled back or tampered with a vault) and
`restoredItemIds` (tell the user an edit brought a deleted item back).
7. **Storage.** Call `navigator.storage.persist()`. If the browser evicts IndexedDB, unsynced edits
7. **Removed browser** (kestrove-spec docs/03 "Removed devices"). `DeviceRevoked` from any call means
the server removed this browser's device id for good. The vault has already replaced the WebCrypto
key and device id in IndexedDB (`deviceId()` changes), dropped the session and kept the local data.
`loginWithSetup` with a ticket retries once by itself as the new device and normally resolves `"ok"`.
Otherwise show "This device was removed from your account. Set it up again." and the sign-in: the
next `login` enrols the new identity (`"pendingApproval"`). `resetDeviceIdentity()` does the same
reset on demand. When the user removes a device, warn "To add it back later, set it up again as a new
device."
8. **Storage.** Call `navigator.storage.persist()`. If the browser evicts IndexedDB, unsynced edits
and the device key are lost and the browser must be approved again.
8. **Sign out of this browser:** `logout()` (ends the server session), then `wipe()` (removes the
9. **Sign out of this browser:** `logout()` (ends the server session), then `wipe()` (removes the
replica and device key; the next login is a new device).
9. **Import/export:** pass `new Uint8Array(await file.arrayBuffer())`; `exportEncrypted` resolves with
10. **Import/export:** pass `new Uint8Array(await file.arrayBuffer())`; `exportEncrypted` resolves with
the `.kvex` bytes for a `Blob` download.
10. `itemsForUrl` / `matchUrl` match the same host only (no Public Suffix List in this build).
11. **Onboarding** (docs/10). Same flows and names as kv-ffi; `expiresAt` is Unix **seconds**
11. `itemsForUrl` / `matchUrl` match the same host only (no Public Suffix List in this build).
12. **Onboarding** (docs/10). Same flows and names as kv-ffi; `expiresAt` is Unix **seconds**
and codes look like `"123 456"`, as in kv-ffi.
* *Sign in with a setup code*: `parseSetupPayload(text)` (scanned or pasted; it is a secret,
so never put it in a URL or register `kestrove://` as a handler), then