fix(brief): keep the dev hatches out of production builds (RB-11)
BIO-012: roleInterceptor/subjectInterceptor are correctly registered only under isDevMode(), but three hand-written fetch adapters (reveal-bignummer, letter-preview, org-template's proefbrief) bypass HttpClient and set X-Role/X-Subject themselves with no guard. The readers underneath, role.ts and subject.ts, were ungated too: they read ?role=/?subject= and wrote it into sessionStorage on any navigation, in any build -- for ?subject= that value is a BSN, which is exactly what SessionStore's G1 comment promises never happens. Gate both layers: currentRole()/currentSubject() return their safe default immediately outside isDevMode() (no query-param read, no sessionStorage write), and the three adapters additionally wrap their headers in isDevMode() so a production request carries neither header at all, matching what an HttpClient request already does once the interceptors aren't registered. TE-002: reveal-bignummer's response-shape validation was a "Trust boundary" a spec could only reach by stubbing globalThis.fetch. Exported it as parseRevealed(body), matching the other 30 parse* boundaries in the repo. Same treatment for letter-preview's errorMessage and org-template's proefbrief error mapping (extracted from an inline try/catch into a named, exported function first, since it wasn't already separate). BIO-006(a): reveal-bignummer sent X-Step-Up: 'true' unconditionally, so the backend's step-up precondition constrained nothing. reveal() now takes a stepUp flag; BriefStore.revealBigNummer() -- reachable only after the UI's confirm() gesture -- is the one that supplies it, so the literal no longer lives in the transport adapter. BIO-006(b): documented in roles-and-access.md that drafter is also the backend's fallback identity (StubIdentityProvider's catch-all arm), not just the dev switcher's initial choice -- so the least-privilege consequence of it also being the only role that may reveal a BSN is visible. Doc correction, same diff: roles-and-access.md's "wired only under isDevMode()" claim was false for the three hand-written fetch paths; it now says where the gate lives (interceptor registration and the reader functions) so it doesn't go stale the same way again. CLAUDE.md's dev-only claims needed no correction -- they already noted these three calls bypass the interceptor. Every fix has a test confirmed red by temporarily reverting the source change and rerunning the suite before restoring it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, isDevMode } from '@angular/core';
|
||||
import { Result, ok, err } from '@shared/kernel/fp';
|
||||
import { currentRole } from '@shared/infrastructure/role';
|
||||
import { currentSubject } from '@shared/infrastructure/subject';
|
||||
@@ -15,7 +15,10 @@ export const PREVIEW_FAILED = $localize`:@@brief.preview.failed:De voorvertoning
|
||||
* hand-written fetch, not the `ApiClient`. That also means it bypasses `HttpClient`'s
|
||||
* `roleInterceptor` AND `subjectInterceptor`, so both `X-Role` and `X-Subject` are set
|
||||
* here explicitly (WP-74 — without `X-Subject` this always previewed
|
||||
* `DocumentStore.DemoOwner`'s letter regardless of who was actually logged in).
|
||||
* `DocumentStore.DemoOwner`'s letter regardless of who was actually logged in). Both are
|
||||
* dev-only identity stand-ins (`role.ts`/`subject.ts`) and are sent only under
|
||||
* `isDevMode()`, mirroring how the interceptors themselves are only registered in dev
|
||||
* (`app.config.ts`) — a production build sends neither header from this call (BIO-012).
|
||||
*
|
||||
* `cache: 'no-store'` (WP-74): the endpoint has no `Cache-Control`, only a CORS-driven
|
||||
* `Vary: Origin`, and its content changes at the SAME URL as the letter moves
|
||||
@@ -43,7 +46,9 @@ export class LetterPreviewAdapter {
|
||||
const subject = currentSubject();
|
||||
res = await fetch(`${environment.apiBaseUrl}/api/v1/brief/preview`, {
|
||||
cache: 'no-store',
|
||||
headers: { 'X-Role': currentRole(), ...(subject ? { 'X-Subject': subject } : {}) },
|
||||
headers: isDevMode()
|
||||
? { 'X-Role': currentRole(), ...(subject ? { 'X-Subject': subject } : {}) }
|
||||
: {},
|
||||
});
|
||||
} catch {
|
||||
return err(PREVIEW_FAILED);
|
||||
@@ -53,7 +58,9 @@ export class LetterPreviewAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async function errorMessage(res: Response): Promise<string> {
|
||||
/** Trust boundary (TE-002): maps a non-OK response to a message. Exported so a spec
|
||||
can call it directly instead of stubbing `globalThis.fetch`. */
|
||||
export async function errorMessage(res: Response): Promise<string> {
|
||||
try {
|
||||
return problemDetail(await res.json(), PREVIEW_FAILED);
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user