ADR-0002 SS3 models Zorgverlener/Medewerker as different Principal variants with different login flows. Actor #2 (apps/behandelportal) landed in WP-61/67 and the union never followed: grep -rn "Principal" returned one hit, a comment. Both apps' auth/domain/session.ts stayed byte-identical (`{ bsn, naam }`), so the backoffice's Behandelaar carried a BSN and logged into the backoffice as a citizen, by DigiD, under a fabricated citizen's name (login.page.ts). The divergence ADR-0002 predicted took an orthogonal side door instead (medewerker.interceptor.ts's X-Medewerker/X-Rollen stamp, which never touches SessionStore) -- which is why ssp/auth and bhp/auth still measured as 100%/84% duplicated after ADR-C-006 shared the route guards. RB-09 (landed the day before) made the backend's IIdentityProvider able to say "no identity" and fail closed; this ticket is its named FE half. Each app's auth/domain/session.ts becomes principal.ts, holding the one Principal variant that app actually has an actor for: ssp keeps `{ kind: 'zorgverlener', bsn, naam }` (G1 still strips the BSN before persisting); behandelportal gets `{ kind: 'medewerker', medewerkerId, naam, rollen }` (no BSN to strip -- G2 shape validation only). A new MedewerkerAdapter replaces DigidAdapter in behandelportal, resolving the existing MEDEWERKER_ID/currentRollen() dev stand-in into a Principal; because there is no credential to check, it returns Principal directly rather than a Result whose error variant could never occur. login.page.ts stops being a BSN/wachtwoord form -- one explainer line and an "Inloggen met SSO" button -- and its dead error-handling branch goes with the Result wrapper that justified it. Measured with tools/baseline-scan.mjs --dup: auth duplication drops from 168/168 (ssp) and 168/200 (bhp) to 32/179 and 32/259 -- under the backlog's <40 target. What remains is the ADR-C-006 route-guard re-export (deliberately identical), generic test/story-file boilerplate, and one shared fragment of the root-singleton-store idiom -- not re-converged identity or login-flow logic. SS3's prediction that the two actors would authenticate differently enough to justify not sharing auth has now actually been tested, not just asserted, and held. Also: renamed Session.bsn to Principal.bsn in two doc comments (libs/shared/src/infrastructure/subject.ts, subject.interceptor.ts) that cited the old type name; regenerated libs/shared/docs/behaviour-spec.mdx (generated file, per its own banner); recorded the resolution in ADR-0002 as a new amendment, replacing its "Known debt" section. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
40 lines
1.5 KiB
TypeScript
40 lines
1.5 KiB
TypeScript
/**
|
|
* Who is logged in. Framework-free domain type.
|
|
*
|
|
* The `zorgverlener` variant of ADR-0002 §3's `Principal` union — the SSP has exactly
|
|
* one actor kind (a citizen, authenticated via DigiD/BSN), so this app's own copy of
|
|
* the union only ever holds this one member. `kind` is still a discriminant, not
|
|
* decoration: it is what makes `apps/behandelportal`'s `medewerker` variant a
|
|
* genuinely different type rather than a same-shaped coincidence, and what a future
|
|
* third actor (§4 — admin/auditor/institution-rep) would add a member to.
|
|
*/
|
|
export interface Principal {
|
|
readonly kind: 'zorgverlener';
|
|
readonly bsn: string;
|
|
readonly naam: string;
|
|
}
|
|
|
|
export function isAuthenticated(p: Principal | null): p is Principal {
|
|
return p !== null;
|
|
}
|
|
|
|
/**
|
|
* Parse a persisted principal out of a raw `localStorage` string (best-effort;
|
|
* anything that isn't a well-shaped record → logged out). G2: validate the
|
|
* shape before trusting it. G1: even if a stored entry carries a `bsn`, the
|
|
* restored principal's `bsn` is always `''` — the BSN is never persisted (see
|
|
* the `SessionStore` effect that writes it), so a legacy or tampered entry
|
|
* cannot resurrect one.
|
|
*/
|
|
export function parseStoredPrincipal(raw: string | null): Principal | null {
|
|
try {
|
|
if (!raw) return null;
|
|
const parsed = JSON.parse(raw) as Partial<Principal>;
|
|
return typeof parsed?.naam === 'string'
|
|
? { kind: 'zorgverlener', bsn: '', naam: parsed.naam }
|
|
: null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|