/** * Per-source-row fanout sidecar for `shape: 'array'` derivations. * * Each `(sourceCollection, sourceId, outputKey)` triple gets its own * envelope at: * * _meta/derivations-fanout/// * * The envelope records the last-emitted derived row ids so the * dispatcher can compute the diff on every source-row update in O(1): * read prior keys, compute `toDelete = prev \ new`, write new, persist * back. * * The body is encrypted (AES-GCM under the `_meta` collection DEK) when * the vault is encrypted — the `keys[]` are derived-row ids produced by a * user-supplied key extractor and can be content-bearing (SKU, tag, email), * so a ciphertext-only store must not read them, the derivation graph, or * `emittedAt`. Back-compat: sidecars written before this fix are plaintext * (`_iv === ''`); `loadFanoutSidecar` dual-reads them. * * @module */ import type { NoydbStore } from '../../kernel/types.js'; import { type EnclaveKey } from '../../kernel/enclave/index.js'; type GetDEK = (collectionName: string) => Promise; /** Magic-prefixed JSON payload at `_meta/`. */ export interface FanoutSidecar { readonly _noydb_fanout: 1; /** Source collection name. */ readonly source: string; /** Source record id. */ readonly sourceId: string; /** Strategy output key (the key in `strategy.outputs`). */ readonly outputKey: string; /** Output collection name (audit / forensics). */ readonly outputCollection: string; /** Derived-row ids last emitted for this (source, output) pair. */ readonly keys: ReadonlyArray; /** ISO timestamp of last dispatch. */ readonly emittedAt: string; } /** * Read the sidecar; returns `undefined` only when it's legitimately absent * (no envelope at that id) — the correct signal for callers to skip * orphan-row reconciliation because there's nothing to reconcile against. * * A PRESENT envelope that fails to decrypt or parse is a data-integrity * problem, not an absence, and is deliberately NOT caught here: swallowing * it to `undefined` would look identical to "no sidecar" and make callers * (the array-derivation dispatch in `vault.ts`/`collection.ts`) silently * skip deleting stale derived rows on a shrink. Let `openEnvelopeJson`'s * `DecryptionError`/`TamperedError` and `JSON.parse` failures propagate, * matching the no-catch convention of sibling decrypt call-sites (e.g. * `with-audit/consent/consent.ts`'s `decryptEntry`). * * Dual-reads for back-compat: an envelope with `_iv === ''` is a legacy * plaintext sidecar (parse `_data` directly); otherwise the body was * encrypted under the `_meta` DEK and is decrypted first. */ export declare function loadFanoutSidecar(store: NoydbStore, vault: string, source: string, sourceId: string, outputKey: string, getDEK: GetDEK, encrypted: boolean): Promise; /** * Persist (insert/replace) the sidecar with a fresh key set. The body is * encrypted under the `_meta` DEK when the vault is encrypted (the `keys[]` * can be content-bearing); plaintext only in debug/unencrypted vaults. */ export declare function saveFanoutSidecar(store: NoydbStore, vault: string, payload: { readonly source: string; readonly sourceId: string; readonly outputKey: string; readonly outputCollection: string; readonly keys: ReadonlyArray; }, getDEK: GetDEK, encrypted: boolean): Promise; /** Delete the sidecar (used on source-row delete cascade). */ export declare function deleteFanoutSidecar(store: NoydbStore, vault: string, source: string, sourceId: string, outputKey: string): Promise; export {};