/** * Blob retention + compaction. * * Declarative per-collection / per-slot eviction policy. Two * triggers: * * - **`retainDays`** — age-based TTL. A slot uploaded more than N * days ago is evicted. * - **`evictWhen(record)`** — predicate over the **decrypted** * record. Lets consumers express "the image is safe to drop once * the structured invoice has been reviewed and confirmed." * * Either trigger (or both) causes the slot to evict. Eviction removes * the slot entry from `_blob_slots_{collection}`, decrements the * blob's refCount (so unreferenced chunks can be GC'd by the next * sweep), and writes one entry to the `_blob_eviction_audit` * collection for tamper-evident record-keeping. * * The audit entry carries the eTag of the evicted blob (opaque HMAC * of plaintext under the vault's `_blob` DEK) — no plaintext leakage, * per the SPEC non-correlation invariant. Consumers reconstructing * "what used to be attached" can look up the audit entry by record * id. * * Compaction is **consumer-scheduled** — noy-db never runs a * background daemon. Call `vault.compact()` whenever your workflow * allows (cron, manual "tidy" button, cold-storage export prep, …). * * @module */ import type { NoydbStore, SlotInfo } from '../../kernel/types.js'; import { type EnclaveKey } from '../../kernel/enclave/index.js'; export interface BlobFieldPolicy { /** * Via port brand marker — lets a `BlobFieldPolicy` satisfy the kernel's * opaque `ViaDescriptor` (#629 Task 7). Optional (the * `ClassifiedFieldSpec._viaBrand` precedent): `blobFields` policies are * plain object literals with no declaration factory to stamp it, so a * mandatory field would break every existing declaration. */ readonly _viaBrand?: 'blob'; /** * Age-based TTL in days. A slot whose `uploadedAt` is older than * `now - retainDays × 86400s` evicts on the next `vault.compact()`. * Omit to disable age-based eviction. */ readonly retainDays?: number; /** * Predicate evaluated against the decrypted record. When it returns * `true`, every matching slot on that record evicts. Omit to * disable predicate-based eviction. */ readonly evictWhen?: (record: T) => boolean; /** * **Legal hold.** When this predicate returns `true`, the slot is * never evicted — `retainDays`/`evictWhen` are overridden. Use for a * litigation / audit hold on a fiscal document: the blob stays until * the predicate returns `false` (the hold is released). Fail-closed: * if the predicate throws, the slot is treated as held. */ readonly legalHold?: (record: T) => boolean; /** * **Period-bound retention.** Returns the date (Date / ISO string / * epoch ms) until which the slot must be retained — typically derived * from the record's fiscal period (e.g. period end + 10 years). While * `now < retainUntil`, the slot is never evicted, regardless of * `retainDays`. Return `null`/`undefined` to impose no floor. * Fail-closed: a throwing function holds the slot. */ readonly retainUntil?: (record: T) => Date | string | number | null | undefined; /** * **External projection.** When `true`, this field's bytes are stored in the * vault's `ObjectProjection` (`createNoydb({ objectStore })`) as a single raw, * **unencrypted** object — servable directly from S3/CDN and processable by * native tooling — instead of the encrypted-chunk path. The encrypted slot * record remains the catalog (anchoring invariant). Requires an `objectStore`; * **outside the zero-knowledge guarantee** — use only for assets meant to * leave the vault. See the as-aws-s3 design spec. */ readonly external?: boolean; /** * For an `external` field: make the object world-readable (CDN origin) rather * than presigned-only. Default `false` (presigned). Ignored unless `external`. */ readonly public?: boolean; /** * For an `external` field: how to stamp a **backlink** (this record's * vault/collection/id/field) onto the object's metadata — the self-describing * "secondary store" that powers reconcile / DR / import re-pairing. * - `'opaque-token'` (default): a random id; preserves the opaque-bucket * property (no names leak); the token is also recorded on the slot. * - `'encrypted'`: the reference encrypted under the blob DEK (ZK-preserving; * falls back to `'opaque-token'` on a plaintext vault). * - `'plain'`: the reference in cleartext metadata — **leaks structure** to * bucket readers; only for non-sensitive deployments. * - `'none'`: no backlink. */ readonly backlink?: 'opaque-token' | 'encrypted' | 'plain' | 'none'; } export type BlobFieldsConfig = Record>; export declare const BLOB_EVICTION_AUDIT_COLLECTION = "_blob_eviction_audit"; export interface BlobEvictionEntry { readonly id: string; readonly collection: string; readonly recordId: string; readonly slotName: string; readonly blobHash: string; readonly reason: 'ttl' | 'predicate' | 'both'; readonly evictedAt: string; readonly actor: string; } export interface CompactionResult { /** Number of blob slots evicted across all collections. */ readonly evicted: number; /** Number of records touched (iterated + policy checked). */ readonly records: number; /** Number of collections with `blobFields` configured. */ readonly collections: number; /** Number of audit entries written. Equal to `evicted`. */ readonly auditEntries: number; /** * Number of slots that would have evicted (TTL/predicate triggered) * but were retained by a `legalHold` or `retainUntil` floor. */ readonly held: number; /** Per-collection breakdown for diagnostics. */ readonly byCollection: Record; } export interface CompactRunOptions { /** Override "now" for deterministic testing. */ readonly now?: Date; /** * Stop after this many evictions. Useful for capped batches / cron * jobs that need to fit in a time window. `undefined` = unbounded. */ readonly maxEvictions?: number; /** * Dry-run — evaluate policies and return the counts, but do NOT * delete slots or write audit entries. Lets a consumer preview * what would happen. */ readonly dryRun?: boolean; } export interface CompactionContext { readonly adapter: NoydbStore; readonly vault: string; readonly actor: string; readonly encrypted: boolean; readonly getDEK: (collection: string) => Promise; /** * Resolve a collection's declared `blobFields` config. Returns an * empty map for collections without the config — the walk skips * those. */ readonly getBlobFields: (collection: string) => BlobFieldsConfig | null; /** List collection names in the vault. */ readonly listCollections: () => Promise; /** List record ids in a collection. */ readonly listRecords: (collection: string) => Promise; /** Decrypt and return the record. Null when absent. */ readonly getRecord: (collection: string, id: string) => Promise; /** Return the BlobSet-like handle for a record's slots. */ readonly listSlots: (collection: string, id: string) => Promise; /** Delete a slot and decrement its blob's refCount. */ readonly deleteSlot: (collection: string, id: string, slotName: string) => Promise; } export declare function runCompaction(ctx: CompactionContext, options?: CompactRunOptions): Promise;