/** * Per-subject data keys — the mechanism under `piiClass` (#37, master-plan §5.3). * * The classification has been enforced at the type level since the contracts package * existed: an event carrying PII cannot be declared without a `subjectId`, on the stated * grounds that *"crypto-shredding must be able to key the erasure"*. This is the erasure. * * **Why crypto at all, when a database has DELETE.** It is not needed for the live scope * — Tier 1 is mutable, so erasing there is an ordinary redaction. It is needed for the * copies the platform KEEPS and cannot rewrite: a reap backup, a stored dump, and (when it * lands) the Tier-2 event lake. Those are full-fidelity on purpose — *"a backup that cannot * restore is a false promise"* (`backups.ts`) — which is exactly why a DELETE can never * reach into one. Sealing each subject's payloads under their own key at the moment the * copy is written turns "erase from every copy we hold" into "destroy one key". * * **The two-key structure.** A per-subject DEK does the sealing; the host's `SecretBox` * wraps the DEK for storage. So the directory holds wrapped keys, the master key lives * wherever the deployment's `SecretBox` is bound (Web Crypto locally, Secrets Store or a * KMS when hosted), and a stolen directory dump yields neither plaintext nor usable keys. * `SecretBox` already gives us AES-256-GCM, a fresh IV per seal, and `keyId` rotation — * this file adds the per-subject layer and nothing else. * * **The tombstone is the load-bearing part.** Destroying a key is only an erasure if * nothing mints a replacement. `destroy` keeps the row with `wrappedDek` cleared, and * `sealMany` refuses any subject holding one. A key store that forgets who was erased can * erase them exactly once, and the second export undoes the first shred. */ import { type SealedSecret, type SecretBox } from './secret-box.js'; /** One subject's key row, as the adapter's directory holds it. */ export interface SubjectKeyRow { keyId: string | null; wrappedDek: string | null; /** Non-null ⇒ tombstoned. The key is gone and no new one may be minted. */ shreddedAt: string | null; } /** * The storage port each adapter fills with its own directory table. * * Split out so the CRYPTO lives here once and the two adapters differ only in how they * read and write a row — the same division `SecretBox` itself draws. Sync or async: a * SQLite adapter answers immediately, a Durable Object does not. */ export interface SubjectKeyRecords { read(subjectId: string): Promise | SubjectKeyRow | undefined; insert(subjectId: string, row: { keyId: string; wrappedDek: string; createdAt: string; }): Promise | void; /** Clear the key, keep the row, stamp the time. `existed` reports whether a key was there to destroy. */ tombstone(subjectId: string, at: string): Promise<{ existed: boolean; }> | { existed: boolean; }; } export interface SubjectKeys { /** Positional: result `i` belongs to `items[i]`. `null` ⇒ the subject is tombstoned; refuse. */ sealMany(items: readonly { subjectId: string; plaintext: string; }[]): Promise<(SealedSecret | null)[]>; /** Positional. `null` ⇒ no key (shredded, or never minted); the payload stays unreadable. */ openMany(items: readonly { subjectId: string; sealed: SealedSecret; }[]): Promise<(string | null)[]>; destroy(subjectId: string, at: string): Promise<{ existed: boolean; }>; } export declare function createSubjectKeys(box: SecretBox, records: SubjectKeyRecords): SubjectKeys; //# sourceMappingURL=subject-keys.d.ts.map