/** * The PII filter pipeline: the single entry point a chat app wires in. * * user message * → heuristics (sync, structured PII) * → NER (optional, async, contextual PII) * → merge + default-deny policy (keep {city, state, zip}) * → session table: replace with stable placeholders * → send placeholdered text to the LLM * assistant reply * → outbound scrub (model can echo/emit PII too) * → rehydrate placeholders → render to user * * Heuristics run synchronously so the structured PII (SSNs in any separator * form, cards, phones, emails) is gone immediately even before the model loads. * The NER detector is injected, so callers can run it on a worker, skip it in * tests, or swap models without touching this orchestration. */ import { type PlaceholderAliases, type ScrubResult } from "./session"; import type { Span } from "./types"; /** An async contextual detector. Matches both the in-process and worker forms. */ export type NerDetector = (text: string) => Promise; export interface FilterOptions { /** Optional contextual detector. Omit to run heuristics-only. */ readonly ner?: NerDetector; /** Display aliases for placeholder tokens, e.g. `{ GIVEN_NAME: "NAME" }`. */ readonly aliases?: PlaceholderAliases; /** * When `true`, skip the structured-PII premask before the model. Use this with * a model trained without prefilter (no-prefilter ablation), whose classes * include SSN / CREDIT_CARD / IP_ADDRESS — the model expects raw digits, not * sentinels. Heuristic spans for those types are still emitted and merged, so * they act as belt-and-suspenders for the model's predictions. */ readonly noPrefilter?: boolean; } /** * Stateful filter bound to one conversation. Holds the session entity table so * placeholders stay consistent across turns and rehydration can reverse them. */ export declare class PiiFilter { private readonly options; private readonly table; constructor(options?: FilterOptions); /** * Detect all PII in a message: heuristics always, NER when configured. * * The heuristic layer (structured PII: SSN / CREDIT_CARD / IP_ADDRESS) runs * first and its spans are substituted with neutral sentinels before the text * reaches the model — the same masking the model was trained on, so it never * sees raw structured digits. The model's spans, reported in masked * coordinates, are projected back to raw offsets and merged with the * (raw-offset) heuristic spans for redaction. */ private detect; /** * Scrub an outgoing user message. The returned `text` is what may be sent to * the LLM and written to logs — it contains no PII outside the keep-set. */ scrubOutbound(text: string): Promise; /** * Restore real values in an assistant reply before showing it to the user. * Run on every model response: the model reasons over placeholders and may * repeat them, and we must not surface a bare `[PERSON_1]`. */ rehydrateInbound(reply: string): string; /** * Defense in depth: scrub the *model's* output too. A model can emit PII the * user never typed (hallucinated or inferred), so re-run detection on the * reply and placeholder anything new before it is logged or persisted. */ scrubInbound(reply: string): Promise; } /** Stateless one-shot scrub for non-conversational callers (e.g. log sinks). */ export declare function scrubOnce(text: string, options?: FilterOptions): Promise; //# sourceMappingURL=pipeline.d.ts.map