diff --git a/crates/kv-wasm/README.md b/crates/kv-wasm/README.md index 150ae1a..ed6c1d9 100644 --- a/crates/kv-wasm/README.md +++ b/crates/kv-wasm/README.md @@ -45,7 +45,7 @@ The generated `pkg/kv_wasm.d.ts` is the reference. It mirrors kv-ffi's `VaultCor records are plain objects with the same fields as kv-ffi's records (`ItemDto`, `ItemSummary`, `SyncReport`, `DeviceDto`, `PasskeySummary`, `ImportPreview`, …); enums are camelCase strings (`"login" | "secureNote" | "card" | "identity"`, `"ok" | "pendingApproval"`); bytes are `Uint8Array`; -absent optionals come back as `null`. **Every `VaultCore` method returns a `Promise`.** +absent optionals come back as `null`. `ItemSummary` also carries `tags` and `modifiedAt` (web only). **Every `VaultCore` method returns a `Promise`.** ```ts class VaultCore { diff --git a/crates/kv-wasm/js/smoke.mjs b/crates/kv-wasm/js/smoke.mjs index 27dd9d5..07c07f2 100644 --- a/crates/kv-wasm/js/smoke.mjs +++ b/crates/kv-wasm/js/smoke.mjs @@ -82,6 +82,8 @@ assert.deepEqual( ], ); assert.equal((await a.itemHistory(bankId))[0].password, "first password"); +assert.deepEqual(list[0].tags, ["finance", "ünïcode ✓"]); +assert.ok(list[0].modifiedAt >= bank.modifiedAt); await rejects(a.getItem("not-a-uuid"), "Invalid"); const code = kv.totpCode("JBSWY3DPEHPK3PXP", Math.floor(Date.now() / 1000)); assert.match(code.code, /^\d{6}$/); diff --git a/crates/kv-wasm/src/core.rs b/crates/kv-wasm/src/core.rs index 0f65e06..b60ea1a 100644 --- a/crates/kv-wasm/src/core.rs +++ b/crates/kv-wasm/src/core.rs @@ -796,6 +796,8 @@ fn summary(i: &Item) -> ItemSummary { urls: i.urls.clone(), favorite: i.favorite, has_totp: i.totp.is_some(), + tags: i.tags.clone(), + modified_at: i.modified_at, } } diff --git a/crates/kv-wasm/src/dto.rs b/crates/kv-wasm/src/dto.rs index 8fcd5b7..d12c501 100644 --- a/crates/kv-wasm/src/dto.rs +++ b/crates/kv-wasm/src/dto.rs @@ -42,6 +42,9 @@ pub struct ItemSummary { pub urls: Vec, pub favorite: bool, pub has_totp: bool, + /// Web only (not in kv-ffi's record): for tag filters and "recently modified" without `getItem`. + pub tags: Vec, + pub modified_at: i64, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/kv-wasm/src/wasm/mod.rs b/crates/kv-wasm/src/wasm/mod.rs index 34344ae..66fadac 100644 --- a/crates/kv-wasm/src/wasm/mod.rs +++ b/crates/kv-wasm/src/wasm/mod.rs @@ -39,6 +39,9 @@ export interface ItemSummary { urls: string[]; favorite: boolean; hasTotp: boolean; + tags: string[]; + /** Unix seconds. */ + modifiedAt: number; } /** `id: ""` creates a new item. Omitted optional fields default to null / [] / false / 0. */ diff --git a/crates/kv-wasm/src/wasm/tests.rs b/crates/kv-wasm/src/wasm/tests.rs index d365764..4c23000 100644 --- a/crates/kv-wasm/src/wasm/tests.rs +++ b/crates/kv-wasm/src/wasm/tests.rs @@ -72,6 +72,7 @@ async fn account_lifecycle_persists_in_indexeddb() { .unwrap(); let list: Vec = de(ok(core.list_items()).await); assert_eq!(list.iter().map(|i| i.title.as_str()).collect::>(), ["Bank", "Note"]); + assert!(list[0].tags.is_empty() && list[0].modified_at > 1_700_000_000); let mut item: ItemDto = de(ok(core.get_item(id.clone())).await); assert_eq!(item.password.as_deref(), Some("hunter2-secret")); item.password = Some("hunter3-secret".into());