Step 3 (production-readiness): PII storage, validated reads, seams

Implement-now:
- G1: keep PII out of persistent storage — never persist BSN (only `naam`);
  move both wizard drafts (address/email, work data) localStorage → sessionStorage
  so they clear on tab close.
- G2: validate storage reads before trusting the cast — shape/tag guard in every
  restore() (mirrors the parse* HTTP boundary); corrupt/foreign shape → start fresh.
- G3: already satisfied (debug-state redacts via mask.ts).

Show-the-seam (hook + doc, not fully built):
- G4: problemFieldErrors() maps a server validation envelope (ASP.NET
  ValidationProblemDetails `errors`) to the field-keyed map the wizards already
  render; returns {} until the backend sends it. +spec.
- G5: documented the retry/backoff seam at the adapter GET loader; reads may
  retry, mutating submits never do.

Out of scope (named): unsaved-changes warning (persistence prevents data loss),
real auth/tokens, axe-core in CI.

Gate green: lint, check:tokens, build, test 79/79.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-06-27 14:07:10 +02:00
co-authored by Claude Opus 4.8
parent 474c040410
commit 9c2a80451f
6 changed files with 69 additions and 18 deletions
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { problemDetail } from './api-error';
import { problemDetail, problemFieldErrors } from './api-error';
describe('problemDetail', () => {
it('extracts the detail from an RFC-7807 ProblemDetails', () => {
@@ -12,3 +12,16 @@ describe('problemDetail', () => {
expect(problemDetail(undefined, 'fallback')).toBe('fallback');
});
});
describe('problemFieldErrors (G4 seam)', () => {
it('maps a ValidationProblemDetails errors dict to first-message-per-field', () => {
expect(problemFieldErrors({ errors: { straat: ['Verplicht.'], postcode: ['Ongeldig.', 'x'] } }))
.toEqual({ straat: 'Verplicht.', postcode: 'Ongeldig.' });
});
it('returns {} when there is no errors envelope (the current backend shape)', () => {
expect(problemFieldErrors({ detail: 'one banner' })).toEqual({});
expect(problemFieldErrors(new Error('boom'))).toEqual({});
expect(problemFieldErrors(undefined)).toEqual({});
});
});