/** * Time-machine queries — point-in-time reads reconstructed from the * existing history + ledger infrastructure. * * ## Usage * * ```ts * const vault = await db.openVault('acme', { passphrase }) * const q1End = vault.at('2026-03-31T23:59:59Z') * const invoice = await q1End.collection('invoices').get('inv-001') * // → the record as it stood at the close of Q1 2026 * ``` * * ## How it works * * Every write path already fans out into two persistence lanes: * * 1. `saveHistory(...)` persists a **full encrypted envelope snapshot** * per version under the `_history` collection (one envelope per * version, keyed by `{collection}:{id}:{paddedVersion}`). Each * envelope carries its own `_ts` (the write timestamp). * 2. `ledger.append(...)` appends a hash-chained audit entry that * records the `op` (put / delete), `version`, and `ts`. * * Reconstruction at a target timestamp T is therefore: * * - Find the newest history envelope for `(collection, id)` whose * `_ts ≤ T` — that's the state the record was in at T. * - Check the ledger for any `op: 'delete'` entry for the same * `(collection, id)` with `entry.ts` in `(latestEnvelope._ts, T]` — * if present, the record was deleted before T, so return `null`. * - Decrypt the surviving envelope with the current collection DEK * (DEKs are per-collection but stable across versions — the same * key encrypts v1 and v15 of a record). * * No delta replay. The existing `history.ts` module already stores * complete snapshots; we just pick the right one. * * ## Read-only contract * * Every write method on `CollectionInstant` throws * {@link ReadOnlyAtInstantError}. A historical view is a *read* * surface — mutating the past would require either a branch/shadow * mechanism (tracked under shadow vaults) or a rewrite of * history, which breaks the ledger's tamper-evidence guarantee. * * @module */ import type { NoydbStore } from '../../kernel/types.js'; import type { LedgerStore } from './ledger/store.js'; import { type EnclaveKey } from '../../kernel/enclave/index.js'; /** * Narrow view of a {@link Vault}'s internals that * {@link VaultInstant} needs. Passed in by `Vault.at()` rather than * constructed here so all crypto + adapter access stays inside the * Vault class. * * Not exported from the public barrel — consumers should get a * `VaultInstant` via `vault.at(ts)`, never by constructing one * directly. */ export interface VaultEngine { readonly adapter: NoydbStore; /** Vault name (the compartment). */ readonly name: string; /** * `true` when the vault was opened with a passphrase (the normal * case). `false` in plaintext-mode vaults (`encrypt: false`) — in * that case `envelope._data` is raw JSON and we skip the DEK lookup. */ readonly encrypted: boolean; /** * Resolves the DEK used to decrypt a given collection's envelopes. * Not called when `encrypted` is false. */ getDEK(collection: string): Promise; /** * Lazily-initialised ledger. We consult it to detect deletes that * happened between the latest history snapshot and the target * timestamp. `null` when history is disabled for this vault — in * that case time-machine reads fall back to history-only * reconstruction (which may miss deletes). */ getLedger(): LedgerStore | null; } /** * A vault at a fixed instant. Produced by `vault.at(timestamp)`. * Carries no session state of its own — every read is a fresh * lookup through the vault's adapter. * * Cheap to construct; safe to throw away. Create one per query. */ export declare class VaultInstant { private readonly engine; /** Fully-resolved target timestamp (ISO-8601 UTC). */ readonly timestamp: string; constructor(engine: VaultEngine, /** Fully-resolved target timestamp (ISO-8601 UTC). */ timestamp: string); /** Get a point-in-time view of a collection. */ collection(name: string): CollectionInstant; } /** * A read-only collection view anchored to a past instant. * * Every write method throws {@link ReadOnlyAtInstantError} — see the * module docstring for why. The read surface is intentionally smaller * than the live {@link Collection}: `get` and `list` cover the * "what did the books look like on date X" use case without pulling * in the full query DSL / joins / aggregates at this stage. Follow-up * work tracked under. */ export declare class CollectionInstant { private readonly engine; private readonly targetTs; readonly name: string; constructor(engine: VaultEngine, targetTs: string, name: string); /** * Return the record as it existed at the target timestamp, or * `null` if the record had not been created yet or had already been * deleted by then. * * Gated on the LIVE record's current tier — #730, mirroring the #712 * read-gate `history()`/`getVersion()` already apply: an elevated record * is invisible through the whole time-machine surface, not just a * decrypt failure. See {@link resolveVisibleEnvelope}. * * Decrypts through {@link openEnvelopeJson}, the `_cek`-aware envelope * body opener — a snapshot from a `perRecordKeys` collection is encrypted * under its own per-record CEK (wrapped in `_cek`), not the collection * DEK directly. */ get(id: string): Promise; /** * IDs of records that existed (had at least one `put` and were not * subsequently deleted) at the target timestamp. * * Implemented as a linear scan over history + ledger. Performance * is bounded by total history size (not live-vault size), so the * memory-first vault-scale cap (1K–50K records × average history * depth) still applies. */ list(): Promise; put(_id: string, _record: T): Promise; delete(_id: string): Promise; update(_id: string, _patch: Partial): Promise; /** * {@link resolveEnvelope}, additionally gated on the record's LIVE * (current, not historical) tier — #730. Mirrors the #712 read-gate * `history()`/`getVersion()` apply: history snapshots keep their * tier-0-wrapped CEKs and carry no `_tier` of their own, so an elevated * record's prior versions would otherwise stay tier-0-decryptable here. * `null` for a resolved envelope carrying its own `_tier > 0` (a * tier-aware snapshot reached some other way) or whose LIVE record is * currently elevated — both `get()` and `list()` route through this so * the invisibility law holds on the whole time-machine read surface, not * just a decrypt failure. */ private resolveVisibleEnvelope; /** * Return the envelope that represents the record's state at * `targetTs`, accounting for deletes. `null` if the record didn't * exist at that instant. * * ## Why we use the ledger as the authoritative timeline * * The per-version history snapshots saved by `saveHistory()` do * carry a `_ts` field, but that timestamp is the moment the * snapshot was *captured* (i.e. the instant right before the * subsequent overwrite), not the original write time. The ledger, * by contrast, records `ts` at the moment of each `put` / `delete` * — it's the only source that tracks the real timeline. So: * * 1. Walk the ledger; find the latest entry for `(collection, id)` * with `ts ≤ targetTs`. * 2. If that entry is a `delete`, the record was gone at the * target instant — return null. * 3. Otherwise it's a `put` with a specific `version`. Load the * envelope for that version from history, falling back to the * live collection for the most recent version. * * ## Fallback when the ledger is disabled * * If the vault has history disabled, `getLedger()` returns null and * we fall back to comparing envelope `_ts` fields. This is * approximate and gets the *last write* right but may confuse the * intermediate versions; adopters needing accurate time-machine * reads should leave history enabled. */ private resolveEnvelope; private resolveViaLedger; private resolveViaEnvelopeTs; /** * Fetch the envelope for a specific version. The live record (most * recent put) lives in the main collection; prior versions live in * `_history`. We check live first because the common case after a * delete is that we're trying to load the last-live version from * history, and skipping live for the current-version case avoids a * redundant lookup. */ private loadVersion; }