first commit
This commit is contained in:
70
docs/security-stack/security/vault/src/search/search.md
Normal file
70
docs/security-stack/security/vault/src/search/search.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# vault search
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user