6.6 KiB
vault items
(no submodules)
- migrations — (no submodules)- helpers —
items-helpers.tsextracts the pure transformation helpers used by the items repository: row-to-item projection (with… - items — (no submodules)- migrations — _(no submodule…
- migrations — (no submodules)- helpers —
items-helpers.tsextracts the pure transformation helpers used by the items repository: row-to-item projection (with… - items — (no submodules)- migrations — (no submodules)- helpers —
items-helpers.tsextracts the pure transformation helpers used by the items repository: row-to-item projection (with… - items — (no submodules)- migrations — <!-- begin:auto…
- migrations — (no submodules)- helpers —
items-helpers.tsextracts the pure transformation helpers used by the items repository: row-to-item projection (with… - items — (no submodules)- migrations — _(no submodul…
- migrations — (no submodules)- helpers —
items-helpers.tsextracts the pure transformation helpers used by the items repository: row-to-item projection (with… - items — (no submodules)
Purpose
items.ts exposes the VaultItems class, the in-memory CRUD repository over the vault container's item map. It is the boundary between the public vault.items surface and the underlying state — every public method validates input via Zod, mutates the container Map<string, VaultItemRow>, appends an audit entry to the container history, encrypts concealed-field values per item, and notifies the orchestrator that a flush is due.
The repository operates on the in-memory state loaded from the encrypted container blob. It does NOT touch drive directly; persistence is the orchestrator's job (debounced flush + on lock). Concealed-field values (passwords / PINs / secure notes) are encrypted with per-item keys derived from the vault key, so even a leaked container snapshot keeps concealed values as ciphertext until decrypted on demand by rowToItem.
Features
create(fields) → VaultItem— Zod-validated insert with concealed-field encryption.get(id) → VaultItem | null— lazy decryption; returns null for missing or tombstoned ids.update(id, patch, expectedVersion?) → VaultItem— optimistic concurrency viaversion; throwsVAULT.ITEM.VERSION_CONFLICTon stale writes.delete(id)— tombstone (not physical delete) so sync can propagate the deletion.list(options?) → VaultItem[]— filters by category / favorite, with offset / limit pagination.allRows()— snapshot of non-deleted rows for the search index.- Every operation records a histogram sample and an outcome counter; OTel attributes are limited to
{ op, status }— no item ids, no field names, no plaintext. - Mutations call
onMutation()so the orchestrator can debounce flushes.
Usage: Basic Example
import { VaultItems } from "@suaveplan/vault/items/items";
const items = new VaultItems({
state,
vaultKey,
clock: Date.now,
rng: undefined,
onMutation: () => scheduleFlush(),
});
const created = items.create({
title: "GitHub",
category: "login",
fields: { username: "alice", url: "https://github.com" },
concealedFields: { password: "p@ssw0rd" },
tags: ["work"],
favorite: true,
});
Usage: Advanced Example
import { VaultItems } from "@suaveplan/vault/items/items";
import { VaultError, VAULT_CODES } from "@suaveplan/vault/errors";
// Optimistic concurrency: capture version, retry on conflict.
function updateWithRetry(items: VaultItems, id: string, patch: Partial<ItemFields>) {
for (let attempt = 0; attempt < 3; attempt++) {
const current = items.get(id);
if (current === null) throw new Error("missing");
try {
return items.update(id, patch, current.version);
} catch (err) {
if (
VaultError.is(err) &&
err.code === VAULT_CODES["ITEM.VERSION_CONFLICT"] &&
attempt < 2
) continue;
throw err;
}
}
}
API Reference
class VaultItems— public surface forvault.items.constructor(options: ItemsOptions).create(fields: ItemFields): VaultItem— throwsITEM.VALIDATION_FAILED.get(id: string): VaultItem | null.update(id: string, patch: Partial<ItemFields>, expectedVersion?: number): VaultItem— throwsITEM.NOT_FOUND/ITEM.VERSION_CONFLICT/ITEM.VALIDATION_FAILED.delete(id: string): void— throwsITEM.NOT_FOUND.list(options?: ListOptions): VaultItem[]— throwsITEM.VALIDATION_FAILED.allRows(): VaultItemRow[]— read-only snapshot for search.
interface ItemsOptions—{ state, vaultKey, clock, rng, onMutation }.
Implementation Notes
- The repository is stateful — it holds references to the container state map, the vault key, and the orchestrator's mutation callback. The vault layer creates a new
VaultItemson every unlock; the state map is replaced when the container reloads from drive. updatealways incrementsversionby 1 per successful call. Concurrent writers usingexpectedVersionwill see exactly one winner; the others receiveVAULT.ITEM.VERSION_CONFLICT.deleteis a tombstone — the row stays in the map withdeleted: trueso the sync layer can propagate the deletion. A future garbage-collection pass (out of scope for this version) would prune tombstones older than a retention window.- The history log is append-only: every successful
create/update/deleteappends one row with(itemId, version, timestamp, operation). This feeds the audit-trail and CRDT merge paths. listwalks the map insertion order — the same order in which rows were created or last updated. Callers that need stable cross-process ordering should sort bycreatedAtorupdatedAtafter fetch.- The OTel
outcomecounter uses a fixed allowlist (success | not_found | conflict | rejected) — adding a new value requires an OpenSpec change in lockstep withgetVaultInstruments.