Files
skills/docs/security-stack/security/vault/src/search/search.md
2026-05-29 08:20:56 +01:00

3.8 KiB
Raw Blame History

vault search

(no submodules)

Purpose

search.ts implements free-text search across vault items with category / favorite / tag filters. The result projection deliberately strips concealedFields, so search results NEVER contain plaintext passwords / PINs / secure notes — even if a caller forgets to do its own filtering.

The implementation is a linear scan over the non-deleted rows; with the schema-enforced caps (1000-row limit, 256-char query, 32-tag filter) this remains O(N × Q) bounded well under millisecond latency for typical vaults. Future versions may swap in an FTS5 in-memory index without changing the public surface.

Features

  • search(query) → SearchResult[] — Zod-validated free-text query with optional category / tags / favorite filters and result limit.
  • Scoring weights — title = 4, plaintext fields = 2, tags = 1; ties break by insertion order via stable sort.
  • Tag filter is intersection (every tag in query.tags must appear on the row).
  • Empty query string returns every matching row (filtered) — useful for filter-only listing.
  • Concealed-field plaintext is structurally excluded from the result type — the projection uses Omit<VaultItem, "concealedFields">.
  • Every search records a histogram sample and an op: "search" outcome counter.

Usage: Basic Example

import { VaultSearch } from "@suaveplan/vault/search/search";

const search = new VaultSearch({
    rows: () => items.allRows(),
    vaultKey,
    clock: Date.now,
});

const results = search.search({ query: "github", category: "login" });
for (const item of results) {
    console.log(item.title, item.fields.username);
    // item.concealedFields is `never` here — type-checked exclusion.
}

Usage: Advanced Example

import { VaultSearch } from "@suaveplan/vault/search/search";

const search = new VaultSearch({ rows, vaultKey, clock });

// Tag intersection — only items with BOTH "work" AND "dev".
const workDev = search.search({ tags: ["work", "dev"], limit: 50 });

// Favorite-only listing with no text filter.
const favorites = search.search({ favorite: true });

// Empty query, category filter, custom limit.
const allLogins = search.search({ category: "login", limit: 20 });

API Reference

  • class VaultSearch.
    • constructor(options: SearchOptions).
    • search(query: SearchQuery): SearchResult[] — throws ITEM.VALIDATION_FAILED on schema violation.
  • interface SearchOptions — { rows: () => VaultItemRow[], vaultKey, clock }.
  • type SearchResult = Omit<VaultItem, "concealedFields"> — search projection without concealed plaintext.

Implementation Notes

  • The result projection strips concealedFields via destructuring; this guarantees no concealed-field plaintext can flow to a search caller, even if a future code path adds new concealed fields. The SearchResult type alias propagates the exclusion to TypeScript consumers.
  • Scoring is deterministic and field-weighted; titles dominate so a query like "github" ranks { title: "GitHub" } ahead of an item that merely mentions GitHub in a username field. Ties retain insertion order via the stable Array.prototype.sort.
  • The rows callback is invoked on every search so the search index always reflects the latest state. Building a persistent index across multiple searches is intentionally deferred to a future version.
  • The injected vaultKey is needed by the projection to decrypt nothing — concealed-field values are stripped before decryption — but rowToItem still derives the per-item key during projection. This keeps the projection pure and avoids special-casing search inside rowToItem.
  • The OTel counter uses op: "search" — search is treated as a read-side item op for telemetry purposes, sharing the items histogram + counter to keep the metric surface narrow.