/** * The encrypted subject index. * * GDPR crypto-shred needs to answer "which records belong to data subject * X?" portably (the index must travel with the vault/bundle) WITHOUT leaking * subject-equivalence to the store. The rejected alternative — an unencrypted * subject tag in envelope metadata — would let anyone with store access see * which records share a subject. Instead we keep a reserved `_subject_index` * collection, encrypted under its OWN DEK (`getDEK('_subject_index')`): * * - record id = `HMAC-SHA256(indexDEK, subjectId)` (M-2). A bare * `sha256Hex(subjectId)` would be offline-computable: an attacker with * store access and a candidate list (emails / customer ids are low-entropy) * could dictionary the hash to confirm "subject X is present here." Keying * the id with the vault-only index DEK removes that capability — without the * DEK the id cannot be derived. (Legacy entries written before M-2 used the * bare sha256 id; the read/remove paths dual-look-up both forms.) * - record body = AES-GCM(JSON `{ r: [{ collection, id }], p }`) under the * index DEK, where `p` pads the plaintext to a bucketed length so the * ciphertext `_data` length does not leak the approximate record count. * Legacy bodies were a bare `[{ collection, id }]` array; reads accept both. * * ## Concurrency (RISK #3 — known v1 limitation) * * `addSubjectRef` / `removeSubjectRef` are read-modify-write with no CAS. The * design assumes a SINGLE WRITER (the noy-db single-process write model). Two * concurrent writers racing on the SAME subject can lose an entry (last-write * wins on the ref list). This is documented, not fixed in v1: * `rebuildSubjectIndex` performs a full scan to recover a correct index from * the canonical records, so a lost ref is recoverable. A CAS-backed index is * deferred to a later slice. * * @module */ import { type EnclaveKey } from '../../kernel/enclave/index.js'; import type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'; /** Reserved collection holding the encrypted subject → records index. */ export declare const SUBJECT_INDEX_COLLECTION = "_subject_index"; /** A single record reference held in a subject's index entry. */ export interface SubjectRef { readonly collection: string; readonly id: string; } type GetDEK = (collectionName: string) => Promise; /** * Add a `{ collection, id }` ref to a subject's index entry (idempotent — * a duplicate ref is not appended). Read-modify-write; see the concurrency * note in the module docstring. */ export declare function addSubjectRef(adapter: NoydbStore, vault: string, getDEK: GetDEK, encrypted: boolean, subjectId: string, ref: SubjectRef): Promise; /** * Remove a `{ collection, id }` ref from a subject's index entry. When the * last ref is removed the (now empty) entry is deleted so the store holds no * residual key for an erased subject. */ export declare function removeSubjectRef(adapter: NoydbStore, vault: string, getDEK: GetDEK, encrypted: boolean, subjectId: string, ref: SubjectRef): Promise; /** * Look up every record ref for a subject. Returns `[]` when none exist. Unions * the keyed (M-2) and legacy sha256 entries (dual-lookup), deduplicated, so a * subject indexed before M-2 is still fully found. */ export declare function lookupSubject(adapter: NoydbStore, vault: string, getDEK: GetDEK, encrypted: boolean, subjectId: string): Promise; /** * Rebuild the entire subject index from the canonical records (the recovery * path for the documented read-modify-write race). Scans each declared * collection, reads `record[subjectField]` (dotted path), and rewrites the * index from scratch. Tombstoned (already-shredded) records contribute no * ref — their body is gone, so they cannot be re-indexed. * * `decodeRecord` decrypts an envelope to a plain object (or returns null for * a tombstone / unreadable record); supplied by the caller so this module * stays free of Collection internals. */ export declare function rebuildSubjectIndex(adapter: NoydbStore, vault: string, getDEK: GetDEK, encrypted: boolean, subjects: Readonly>, decodeRecord: (collection: string, id: string, env: EncryptedEnvelope) => Promise | null>): Promise; /** * Coerce a read subject-field value to a stable string id. Primitives use * their natural string form; objects/arrays are JSON-stringified so structural * subjects still get a deterministic key (avoids the `[object Object]` trap). */ export declare function coerceSubjectId(value: unknown): string; /** Read a (possibly dotted) field path from a plain record. */ export declare function readDottedPath(record: Record, field: string): unknown; export {};