Two backlog trees are complete: `docs/project/backlog/` (75 files, every WP done) and `docs/project/refactor-backlog-setup/` (the arc before it). Move both under `docs/project/archive/` with `git mv`, so history stays intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them, because it points at the now-archived backlog README. Add `docs/project/archive/README.md`. It states that these trees are historical and names the two directories that are still live. Repoint every inbound reference named in RD-30's Files table: CLAUDE.md, the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the `document-feature` and `new-ssp` skills, and the readable-codebase PLAN, README, and RD-19 ticket. Fix two upward-relative links inside the moved WP files (WP-68, WP-69) that gained a directory level and would otherwise break. Repoint `.prettierignore`'s two agent-prompt exclusions to their new path, so prettier keeps leaving those files' exact wording alone. Mark RD-30 done and check off its acceptance criteria; flip its README row to done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.9 KiB
RB-10 — extract parseStoredSession (both apps) and spec redactProfile
Status: implemented · 2026-08-27 · Source findings: 02-testability.md TE-001 (ssp/auth and bhp/auth) · 07-bio2-compliance.md BIO-017 · 99-backlog.md RB-10
What was wrong
SessionStore.restore() — identical in apps/ssp/src/app/auth/application/session.store.ts
and apps/behandelportal/src/app/auth/application/session.store.ts — called
localStorage.getItem(STORAGE_KEY) itself and did the parse + shape validation in the same
module-private function. It was invoked from a field initializer
(private _session = signal<Session | null>(restore())), so the storage read happened the
instant the singleton was constructed; a spec could not feed it a raw string without
stubbing the localStorage global before the injector built the store.
The logic behind that guard is a trust boundary, not incidental validation — the comment
above it names two guarantees: G1 (never persist the BSN) and G2 (validate the shape
before trusting it). CLAUDE.md §5 mandates a spec for boundary parse* adapters, and none
existed. Baseline evidence: 02-testability.md §3a cites ssp/auth and bhp/auth at
42.9% line / 46.2% branch — jointly the worst line coverage in the frontend table — with
this file's own lcov at LH 2/LF 20 (10.0% line), BRH 3/BRF 13 (23.1% branch).
BIO-017 read the same code and confirmed G1 holds on every path by inspection (restore()
returns { bsn: '', naam }, the persistence effect() writes only naam, login()/
logout() never touch storage with a BSN) — but "correct, unverified by a test" is exactly
the gap TE-001 already targeted, so BIO-017 folds into it and adds one required assertion:
a stored {"bsn":"…","naam":"…"} must yield a session whose bsn is ''.
Separately, apps/ssp/src/app/shell/debug-state/mask.ts — confirmed at the path the finding
cites — has redactProfile, a pure, exported, directly callable PII-redaction function with
no spec. It redacts name, birthdate and address and masks the BIG-nummer; BIO-017 verified it
correct by reading, same "no regression net" gap.
What changed
| File | Change |
|---|---|
apps/ssp/src/app/auth/domain/session.ts |
added export function parseStoredSession(raw: string | null): Session | null — the exact parse+validate body restore() used to hold |
apps/ssp/src/app/auth/application/session.store.ts |
restore() collapses to parseStoredSession(localStorage.getItem(STORAGE_KEY)) |
apps/ssp/src/app/auth/domain/session.spec.ts |
4 new cases: absent, non-JSON, wrong shape, and the G1 assertion |
apps/behandelportal/src/app/auth/domain/session.ts |
identical extraction, second app |
apps/behandelportal/src/app/auth/application/session.store.ts |
identical collapse, second app |
apps/behandelportal/src/app/auth/domain/session.spec.ts |
identical 4 cases, second app |
apps/ssp/src/app/shell/debug-state/mask.spec.ts |
new file — spec for redactProfile: masks the BIG-nummer, redacts name/geboortedatum/adres on both registration and person, leaves beroep/registratiedatum/status untouched |
The extracted function's body is a byte-for-byte move — same try/catch, same
JSON.parse cast, same typeof parsed?.naam === 'string' guard, same { bsn: '', naam }
construction. Only its location and the doc comment (rewritten to explain the why of G1/G2
for a function now read on its own, rather than inline next to the effect() it used to sit
beside) changed.
The seam lands twice, on purpose
auth is deliberately unshared per ADR-0002 / CLAUDE.md §1: Zorgverlener and Medewerker are
different Principal variants with different login flows, and the two session.ts files are
expected to diverge. TE-001 says this outright, and BL-002 flags any extract-to-libs/shared
here as contradicting an accepted ADR. parseStoredSession was therefore written twice, once
per app's own domain/session.ts — not factored into a shared helper, and not resisted only
in this note; the two functions are word-for-word identical today and that is expected to
change the moment RB-13 (Session → Principal) lands.
Judgement calls
redactProfile's spec asserts on the concrete shape, not just "not equal to the input" — it pinsbigNummerto'********901', checksREDACTEDon each PII field individually, and separately asserts the non-PII fields (beroep,registratiedatum,status) survive unchanged. A looser "no PII substring appears" assertion would have been weaker at catching the regression this ticket exists to prevent (e.g. a future field added toredactProfile's output that is left unmasked by accident).- The G1 spec case uses
toEqual, nottoBe, since the parser constructs a new object; this matches the existingisAuthenticatedspec's style in the same file. - No production code beyond the
restore()one-liner in eachsession.store.tschanged —login(),logout(), and the persistenceeffect()were already correct and are unaffected.
Verification
Confirmed both new specs are red without the fix:
- Temporarily changed
parseStoredSessionto keep a storedbsn(bsn: parsed.bsn ?? ''instead ofbsn: '') — the new G1 test failed withexpected { bsn: '19012345601', naam: 'Test' } to deeply equal { bsn: '', naam: 'Test' }, all 243 other tests stayed green. Reverted;git diffon the file is empty afterward. - Temporarily changed
redactProfileto passnaamthrough unmasked — the new "redacts the name" test failed withexpected 'J. Jansen' to be '‹redacted›'. Reverted;git diffon the file is empty afterward.
npm run ci: green (see PR/commit for the run this doc ships with).