feat(privacy): WP-40 — PII kernel (Bsn value object + masked-value atom)

Branded Bsn value object with the elfproef (11-test) checksum in shared/kernel/bsn.ts,
wired into the DigiD login boundary so login does real BSN validation (hint + e2e BSNs
updated to a valid 123456782). Consolidate the pure maskers into shared/kernel/pii.ts
(maskBsn/maskTail/REDACTED); debug-state keeps redactProfile (needs the registratie
BigProfile — boundary). New <app-masked-value> atom (+story) centralises the masked
`.includes('*')` detection + reveal affordance; behandel-scherm refactored onto it.
Session.bsn stays string (persistence boundary drops it for privacy). +specs for bsn/pii.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-23 15:24:59 +02:00
co-authored by Claude Opus 4.8
parent 19f2e9b734
commit 62cb34b60f
18 changed files with 246 additions and 83 deletions
+22 -4
View File
@@ -1,9 +1,27 @@
# WP-40 — PII kernel: branded `Bsn` VO + masked-value atom
Status: todo
Status: done
Phase: 8 — platform/DX/showcase
Priority: P2
## Outcome
- `shared/kernel/bsn.ts``Bsn` branded VO + `parseBsn` with the **elfproef** (11-test) checksum
(+spec). Wired into the DigiD login boundary (`digid.adapter`): login now does real BSN
validation instead of "any 9 digits" — login hint + e2e BSNs updated to a valid one (`123456782`).
- `shared/kernel/pii.ts` — pure `maskBsn`/`maskTail`/`REDACTED` (+spec), the functional core. Moved
out of `debug-state/mask.ts`, which keeps only `redactProfile` (it depends on the registratie
`BigProfile`, so it can't live in `shared/kernel` — boundary; debug-state is the sanctioned
cross-context devtool). `mask.spec` ported to `pii.spec`.
- `<app-masked-value>` atom (`shared/ui/masked-value`) + story — masked-by-default value with an
optional reveal affordance; **centralises** the `.includes('*')` masked-detection that
`behandel-scherm` used to sniff inline (now refactored to the atom). The atom only emits
`reveal`; the caller keeps the step-up confirm + audited fetch.
- Deviation (flagged): `Session.bsn` stays typed `string` — the brand guards the login ENTRY, but
Session crosses a persistence boundary where the restore path deliberately drops the bsn
(privacy) and JSON can't carry a brand, so re-typing it would add a `''`/nullable ripple for no
real gain. Backend `MaskTail` twin unchanged (still in sync).
## Why
Masking is real but ad-hoc: pure helpers live in `debug-state/mask.ts`, the BSN is a bare `string`
@@ -29,6 +47,6 @@ AVG art. 9 (BSN = special category) territory; the pieces should be first-class
## Acceptance criteria
- [ ] `parseBsn` accepts valid elfproef numbers, rejects bad checksum/length (spec).
- [ ] `<app-masked-value>` renders masked by default with an accessible reveal; behandel-scherm uses it.
- [ ] Pure maskers have specs (no TestBed); backend `MaskTail` parity noted; `npm run ci` green.
- [x] `parseBsn` accepts valid elfproef numbers, rejects bad checksum/length/all-zeros (spec).
- [x] `<app-masked-value>` renders masked by default with an optional reveal; behandel-scherm uses it (no more inline sniff).
- [x] Pure maskers have specs (no TestBed); backend `MaskTail` parity noted; `npm run ci` green.