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
@@ -2,6 +2,7 @@ import { Component, ElementRef, computed, input, output, viewChild } from '@angu
import { PlaceholderOption } from '@shared/ui/rich-text-editor/rich-text-editor.component';
import { ButtonComponent } from '@shared/ui/button/button.component';
import { HeadingComponent } from '@shared/ui/heading/heading.component';
import { MaskedValueComponent } from '@shared/ui/masked-value/masked-value.component';
import { StepperComponent } from '@shared/ui/stepper/stepper.component';
import { Besluit, Brief, CaseContext, LibraryPassage } from '@brief/domain/brief';
import { inferSelection } from '@brief/domain/besluit';
@@ -25,6 +26,7 @@ import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.com
imports: [
ButtonComponent,
HeadingComponent,
MaskedValueComponent,
StepperComponent,
LetterCanvasComponent,
DiagnosticsPanelComponent,
@@ -92,10 +94,13 @@ import { BesluitPanelComponent } from '@brief/ui/besluit-panel/besluit-panel.com
<span>{{ caseContext().aanvraagReferentie }}</span>
<span>{{ caseContext().zorgverlenerNaam }}</span>
<span>
{{ bigLabel() }} {{ caseContext().bigNummer }}
@if (canRevealBigNummer() && isMasked()) {
<app-button variant="subtle" (click)="onReveal()">{{ revealLabel() }}</app-button>
}
{{ bigLabel() }}
<app-masked-value
[value]="caseContext().bigNummer"
[canReveal]="canRevealBigNummer()"
[revealLabel]="revealLabel()"
(reveal)="onReveal()"
/>
</span>
<span>{{ caseContext().beroep }}</span>
</div>
@@ -172,10 +177,6 @@ export class BehandelSchermComponent {
locate = output<Diagnostic>();
revealBigNummer = output<void>();
/** The BIG-nummer arrives masked (contains `*`); once revealed the swapped value has
no `*`, so the reveal action hides itself — no separate "revealed" flag needed. */
protected isMasked = computed(() => this.caseContext().bigNummer.includes('*'));
/** Step-up (PRD-0002 §5d) stubbed as a native confirm — the extra verification gesture
before an audited PII reveal. ponytail: real systems prompt MFA / recent re-auth. */
protected onReveal() {