import { Injectable, isDevMode } from '@angular/core'; import { Result, ok, err } from '@shared/kernel/fp'; import { currentRole } from '@shared/infrastructure/role'; import { problemDetail } from '@shared/infrastructure/api-error'; import { environment } from '@shared/environments/environment'; /** Exported so specs can assert against the same message id instead of retyping the Dutch sentence (matches `letter-preview.adapter.ts`'s `PREVIEW_FAILED`). */ export const REVEAL_FAILED = $localize`:@@brief.reveal.failed:Het BIG-nummer kon niet worden getoond.`; /** * Field-level PII reveal (PRD-0002 §5c). The case screen ships the BIG-nummer masked; * this unmasks it, gated server-side by the reveal capability AND a step-up. The * step-up is stubbed as the `X-Step-Up` header, sent only when the caller passes * `stepUp: true` — `BriefStore.revealBigNummer()` is the only caller and it is only * ever reachable after `behandel-scherm.component.ts`'s `onReveal()` confirm gesture, * so the header now reflects that gesture instead of being a constant baked into this * adapter (BIO-006a — a call that skips confirmation sends no step-up at all). * * Hand-written fetch (not the `ApiClient`) because the call needs a per-request header; * `.ExcludeFromDescription()` on the endpoint keeps the generated client JSON-only, the * same seam as `/brief/preview` and uploads. `X-Role` is a dev-only identity stand-in * (see `role.ts`) and is therefore only sent under `isDevMode()`, mirroring the * `roleInterceptor` registration in `app.config.ts` — a production build never sends it * from this hand-written call either (BIO-012). */ @Injectable({ providedIn: 'root' }) export class RevealBigNummerAdapter { async reveal(stepUp: boolean): Promise> { let res: Response; try { res = await fetch(`${environment.apiBaseUrl}/api/v1/brief/reveal-bignummer`, { method: 'POST', headers: { ...(isDevMode() ? { 'X-Role': currentRole() } : {}), ...(stepUp ? { 'X-Step-Up': 'true' } : {}), }, }); } catch { return err(REVEAL_FAILED); } if (!res.ok) return err(await errorMessage(res)); return parseRevealed(await res.json().catch(() => null)); } } /** Trust boundary: validate the untrusted response shape before handing back a plain string (TE-002) — exported so a spec can call it without stubbing `globalThis.fetch`. */ export function parseRevealed(body: unknown): Result { if ( typeof body === 'object' && body !== null && typeof (body as { bigNummer?: unknown }).bigNummer === 'string' ) { return ok((body as { bigNummer: string }).bigNummer); } return err(REVEAL_FAILED); } async function errorMessage(res: Response): Promise { try { return problemDetail(await res.json(), REVEAL_FAILED); } catch { return REVEAL_FAILED; } }