# WP-40 — PII kernel: branded `Bsn` VO + masked-value atom 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`. - `` 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` in `session.ts` (no branded VO / checksum, unlike `BigNummer`/`Postcode`), and masked state is detected by `.includes('*')` sniffing — no reusable atomic-design component. For a register this is AVG art. 9 (BSN = special category) territory; the pieces should be first-class and reusable. ## Decisions - `Bsn` branded value object + `parseBsn` with the **elfproef** (9 digits, weighted mod-11) checksum — parity with the other value objects. Replace the bare `string` in `session.ts`. - Consolidate the pure maskers into `shared/kernel/pii.ts` (functional core): `maskBsn`, `maskTail`, `redactProfile`, … Keep the backend `MaskTail` twin in sync. - `` atom (`shared/ui`) — masked by default, optional reveal affordance + a11y; replaces `.includes('*')` sniffing and composes into the behandel-scherm reveal. ## Files - New `src/app/shared/kernel/bsn.ts` (+spec, elfproef cases) and `shared/kernel/pii.ts` (+spec). - `src/app/auth/domain/session.ts` — `bsn: Bsn`. - New `src/app/shared/ui/masked-value/` atom (+ story). - `src/app/shared/ui/debug-state/mask.ts` — re-export/move to the kernel module. ## Acceptance criteria - [x] `parseBsn` accepts valid elfproef numbers, rejects bad checksum/length/all-zeros (spec). - [x] `` 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.