/** * The value generator behind the masked scope export (#1034). * * A masked pull used to write the literal `[masked]` into every PII cell, which made * the copy structurally valid and factually useless: every screen in a pulled scope * read `[masked]`, so a preview, a demo or a local repro could not be driven from * one. This turns the same cells into **plausible, deterministic, fake** values — * still no real personal data leaves the governed environment, but the copy reads * like a tenant. * * Three properties, and the whole design follows from them: * * 1. **Deterministic.** The same real value becomes the same fake value everywhere it * appears — its own row, every fat event payload that quoted it, the invoice that * snapshotted it. Otherwise joins and timelines stop lining up and the copy stops * reading as real. The seed is `HMAC-SHA-256(salt, value)`, keyed on the ORIGINAL * VALUE ONLY: the column's kind picks the *rendering*, so one email rendered as an * `email` and as an `external_id` still agrees with itself. * 2. **Irreversible.** A keyed hash, and the salt is never written into the dump. No * mapping is stored anywhere; nothing in the output can be walked back. * 3. **Still shaped like what it replaced.** An email stays `.email()`-parseable, a * phone keeps its country prefix and its digit layout, a postal code keeps its * length. A pulled scope whose every read throws a seam `.parse` failure is no * better than `[masked]`. * * Uniqueness comes from the hash rather than from a collision pass: every generated * value carries hash-derived characters, so two distinct inputs practically never land * on one output and a natural key on `email` survives `importScope`. "Practically * never" is sized, not hoped for — an address carries a 48-bit tag, so a scope holding * a million distinct addresses collides with probability ~1e-7. * * **This is pseudonymization, not anonymization.** Rare combinations, amounts and * dates can still re-identify a subject, so nothing about §6's gate relaxes because * the output looks fake — the pull stays staff-only, audited and jurisdiction-checked. * * Deliberately NOT faked, stated rather than hidden: * - **Free text** (`note`, `description`, `body`, `comment`, `message`, `subject`) * stays `[masked]`. Lorem would be a lie about the content; a real sentence cannot * be generated from a hash without inventing meaning. * - **National identifiers** (`ssn`, `personnummer`) stay `[masked]`. A generated * checksum-valid number may belong to a real person; the correct source is * Skatteverket's published test range, which is not in this repo. Part of #1034. * - **Locale** is one neutral value list. Following the tenant's locale needs a * tenant-locale field that does not exist yet — inventing one here would be a * guess dressed as a feature. * * Generated values are drawn from RFC 2606 reserved domains (`example.com`, …), so a * pseudonymized address is guaranteed not to reach anyone. */ /** What a column holds, as far as the name heuristic can tell. */ export type PiiKind = 'email' | 'phone' | 'postal' | 'street' | 'city' | 'person' | 'given' | 'family' | 'label' | 'external_id' | 'redact'; /** The literal a cell keeps when there is nothing honest to generate for it. */ export declare const MASKED = "[masked]"; /** Which kind a column or JSON key is, or `undefined` when it is not PII at all. */ export declare function kindOf(name: string): PiiKind | undefined; /** * Which kind a JSON key is, given the key of the object it sits directly inside. * * `kindOf` is the whole answer for a SQL column, which has no enclosing key. Inside a * payload a key can also be read by its neighbourhood, and exactly one is: `label` * under a person-ish container (#1369). Everything else defers to `kindOf`. */ export declare function kindUnder(name: string, container: string | undefined): PiiKind | undefined; /** * A generator bound to one export's salt. * * `prepare` is separate from `valueFor` on purpose: HMAC is async and a dump is a lot * of cells, so the caller collects every distinct value first, computes the digests in * one batch, and then does the substitution walk synchronously. Digests are per * DISTINCT VALUE, so a customer's email quoted in two hundred event payloads costs one. */ export interface Pseudonymizer { prepare(values: Iterable): Promise; valueFor(kind: PiiKind, original: string): string; } export declare function createPseudonymizer(salt: string): Promise; //# sourceMappingURL=pseudonymize.d.ts.map