Add configurable masking denylist and regex whitelist (#101)
* Add configurable masking denylist
* Align whitelist pattern config
* Harden denylist/whitelist masking
- Merge denylist matches additively so they never shrink detector coverage
- Skip denylist matches inside existing placeholders to avoid corrupting secret/PII masks
- Anchor regex whitelist to the full entity so a partial match can't unmask larger PII
- Skip detection when PII detection is off and no denylist is configured
- Reject regex patterns that match the empty string at config load
- Reuse the conflict-resolver overlap helper; update docs and tests
* Fix streaming unmask when a placeholder delimiter splits across chunks
findPartialPlaceholderStart only buffered when the full "[[" delimiter appeared
within one chunk. If a stream chunk ended with a lone "[" (the first half of
"[["), it was emitted as safe, so the placeholder was never reassembled and
leaked to the client un-restored. Buffer a trailing partial of the start
delimiter too. Restores PII and secrets placeholders that the upstream model
tokenizes across the "[[" boundary.
* Remove ReDoS caveat from PII detection docs
* Simplify partial-placeholder detection and cover the closing-delimiter split
Replace the single-iteration loop in findPartialPlaceholderStart with a direct
trailing-bracket check, and add tests for a placeholder whose closing "]]" is
split across stream chunks.
* Exclude denylist matches by known placeholders; de-magic the match score
Replace the placeholder-shape regex heuristic with exact exclusion against the
real placeholders carried over from secrets masking: secretPlaceholders() is
threaded through detectPII/analyzeRequest and passed in /api/mask, so a denylist
pattern can no longer match (and corrupt) the internals of an existing
secret/PII placeholder. Also replace the vestigial denylist match score 2 with a
named DENYLIST_MATCH_SCORE constant.
* Require knownPlaceholders on detectPII so routes can't skip placeholder protection
* Clarify whitelist/denylist docs and drop Claude Code wording from the default