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
@@ -0,0 +1,32 @@
import { Component, computed, input, output } from '@angular/core';
import { ButtonComponent } from '@shared/ui/button/button.component';
/**
* Atom: a possibly-masked sensitive value (BSN, BIG-nummer, …) with an optional, audited
* reveal affordance (WP-40). The value arrives masked from the server (data-minimisation)
* and is swapped for the full value on reveal; the reveal button shows only when the value
* is still masked AND the caller says the principal may reveal it. Centralises the
* masked-detection that consumers used to sniff inline. The atom only emits `reveal`; the
* caller owns the step-up gesture + the audited fetch (see behandel-scherm).
*
* ponytail: masked-detection is the mask character (`*`) — a POC heuristic. A server-sent
* `masked` boolean would remove the sniff; wire it here without touching consumers.
*/
@Component({
selector: 'app-masked-value',
imports: [ButtonComponent],
template: `
<span class="value">{{ value() }}</span>
@if (canReveal() && masked()) {
<app-button variant="subtle" (click)="reveal.emit()">{{ revealLabel() }}</app-button>
}
`,
})
export class MaskedValueComponent {
value = input.required<string>();
canReveal = input(false);
revealLabel = input($localize`:@@maskedValue.reveal:Tonen`);
reveal = output<void>();
protected masked = computed(() => this.value().includes('*'));
}