The backend half of the sweep RD-18 did for the front end. git blame
holds the provenance and stays correct when the code moves; the
comment names a closed ticket and tells the reader nothing the
sentence around it does not.
public/letter.css and LetterHtml.golden.html change together, because
the renderer inlines the CSS and the golden file snapshots the
result.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The wire said Application, the domain said Aanvraag — one aggregate with
two names at every hop. Rename the backend DTOs and the /applications
route to /aanvragen, regenerate the typed client, and rename the frontend
adapter/store to match.
Renamed: ApplicationSummaryDto/DetailDto, CreateApplicationRequest,
SubmitApplicationRequest/Response → Aanvraag* equivalents;
ApplicationsAdapter/Store → AanvragenAdapter/Store;
applications.adapter.ts/applications.store.ts → aanvragen.*.
Left untouched: the admin Case/Zaak vocabulary (/admin/cases,
AdminCasesStore) — a separate read model, not part of this rename; the
internal BigRegister.Domain.Applications namespace and the Applications
EF table (renaming those needs a new EF migration, out of scope here).
Part of the dashboard-readability refactor (see the approved plan).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DocumentStore wrote one audit row per upload and per user delete carrying the
acting citizen's raw BSN as AuditEntry.Actor, persisted to SQLite — on a
store whose own doc comment says it holds metadata only, never file content
"or other PII". Same shape as RB-02, in a second store.
Masked at the two citizen call sites rather than inside Audit, because the
third actor is the literal "admin" and MaskTail("admin", 3) is "**min";
masking centrally would mean guessing which actors are BSNs and which are
role names. Audit's doc comment now states that actors arrive redacted.
StoredDocument.Owner is untouched: it is the authorization key that
DeleteOwned, ForeignIds and RB-01's content check all compare against, so the
BSN stays where it is load-bearing and leaves the trail where it was only
decoration. No endpoint exposes AuditLog, so no response shape changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GET /uploads/{documentId}/content took only (string documentId) — no
HttpContext, so no authorization was possible. It streams diploma and
identity scans, protected by GUID unguessability alone, while DELETE on the
same resource has always been owner-scoped. GET /uploads/status had the same
shape and confirmed whether any client-chosen localId exists, plus its
documentId.
Both now take HttpContext. Content is readable by the owning
ZorgverlenerCaller or a caller passing Authz.CanBeoordelen — matched on the
caller kind rather than branched on a boolean, because ctx.Zorgverlener()
throws for a MedewerkerCaller and the behandelportal's beoordeling screen is
a legitimate reader. Status is scoped to ctx.Zorgverlener().Bsn via a new
owner parameter on DocumentStore.ByLocalIds (one call site).
404, not 403, on both: a foreign document id must not be distinguishable
from one that never existed, and a foreign localId reads back as "unknown".
Residual, recorded in the implementation note: both callers reach the URL as
a plain browser navigation (<a href> / previewUrl), which carries no identity
header and no interceptor, so StubIdentityProvider resolves it to the seeded
citizen. That is BIO-002 and belongs to RB-09; the links keep working today
only because one citizen owns every document in the POC.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
submit and draft-sync took document ids straight from the request body with no
ownership check: a caller who knew a foreign document's id could attach another
citizen's upload to their own aanvraag (surfacing on the behandelaar's beoordeling
screen, POSTed to OpenZaak as their zaakinformatieobject) and permanently block the
victim's own delete by flipping Linked=true. ADR-0001 holds the FE has no authority;
this trusted it anyway.
Adds DocumentStore.ForeignIds(ids, owner) and calls it from both write paths before
any write, 400 ProblemDetails on a mismatch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New GET /beoordeling/{id} shows one aanvraag's status, linked documents, and a
canBesluiten decision flag, gated by the same CanBeoordelen capability as the
werkvoorraad list. Reads through IZaakSource.ListCases rather than a new seam
method (WP-66 needs one anyway for the real write); owner BSN is masked.
Fixes a real gap found while wiring this up: the behandelportal's login was still
WP-61's copied citizen/BSN DigiD flow, so nothing ever sent X-Medewerker and the
werkvoorraad screen (WP-64) always denied in a real browser. A dev-only
medewerkerInterceptor (mirrors the existing ?role= stand-in as ?rollen=) fixes that.
WP-65's own Risks note authorized splitting read from write across sessions given
its size; this is the read half. The decision-recording mutation is next (65b).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
scripts/openzaak-ui-up.sh brings up the root app and the OpenZaak harness
together, wires them onto one docker network, seeds the catalogus, grants
the container-alias zaaktype scope, and verifies a real aanvraag submitted
through the UI lands in OpenZaak.
Along the way: DocumentStore.DemoOwner was reusing the seeded doctor's
11-digit BIG-nummer as a stand-in BSN, which isn't a valid 9-digit BSN shape
— OpenZaak rejects it, breaking both submit's rol-creation step and the
citizen's own applications list under Zgw:Enabled=true. Fixed to a real
elfproef-valid BSN.
Also adds mitigation for a still-unexplained per-container flake (every
outbound ZGW POST fails as if the body were empty, for that container's
whole lifetime) that correlates with host memory pressure: the script now
warns when host swap is heavily used, and an opt-in ZgwDiagnosticHandler
(ZGW_DEBUG_HTTP=1) logs Content-Length vs. actual bytes sent so the next
reproduction can confirm or rule out client-side body corruption.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the OpenZaak seam with IDocumentSource, sibling of IZaakSource
(WP-49/50): an upload always lands locally first (DocumentStore stays
the record of truth for preview/download/audit) and, when
Zgw:Enabled=true, is also registered as a DRC enkelvoudiginformatie-
object; once a zaak exists (IZaakSource.CreateZaak now also returns
its ZaakUrl), submit links each document to it via zaakinformatie-
object. FE upload/list DTOs are unchanged.
- ZgwOptions gains DrcBaseUrl + a category->informatieobjecttype URL
map (the document analogue of ZaaktypeUrls).
- LocalDocumentSource is the same DocumentStore.Add/Link calls the
endpoints used to make inline — zero behaviour change offline.
- OpenZaakDocumentSource POSTs the eio then the zaak link, persisting
the DRC url (DocumentStore.SetDrcUrl) so linking doesn't re-upload.
- Factored the GET/POST-with-bearer-JWT plumbing shared with
OpenZaakZaakSource into ZgwHttpClient; shared the stub handler
between the two source test classes as ZgwStubHandler.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Applications, documents (+ audit log) and the brief move off static in-memory
Dictionaries onto a real SQLite file via EF Core, so demo data survives a
process restart or `docker compose restart api` for the first time. The three
stores (ApplicationStore/DocumentStore/BriefStore) keep their exact public
signatures and static-class shape — no DI, no async ripple into Program.cs's
minimal-API handlers — each method just opens a short-lived AppDbContext via
Db.Create() under the same lock it already had. Opaque nested shapes (a
wizard's draft snapshot, a brief's sections/placeholders/status) are stored as
JSON text columns rather than redesigned into relational tables, matching the
existing "don't interpret it" posture.
Found two things the WP's own text got wrong, corrected in
docs/backlog/WP-22-durable-persistence.md's Deviations section: SeedData never
seeded these three stores (only the read-only BRP/DUO-mimicking GETs, which
stay in-memory) so there's no seed step; and no new docker-compose volume is
needed since the existing bind mount already covers the SQLite file — verified
against this environment's real podman-backed compose stack, not just by
reading the file.
Also: pinned SQLitePCLRaw.bundle_e_sqlite3 to 3.0.3 (EF Core Sqlite's own
transitive default bundles a pre-3.50.2 SQLite with a known high-severity
memory-corruption advisory); found and fixed a real xUnit test race where
concurrent test-class hosts stomped a shared static connection-string field,
fixed by disabling cross-class test parallelization rather than adding DI the
stores don't otherwise need.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- StoredDocument gains ContentType + byte[] Content; POST /uploads now captures
the file bytes (in-memory, reset on restart — POC).
- GET /uploads/{documentId}/content streams the bytes: inline for pdf/image
(browser preview), attachment otherwise (download). 404 for unknown ids
(covers the demo-* simulation sentinels, which have no bytes).
- Bytes are never serialized into a JSON response; only this endpoint streams them.
- Tests: content served back with type inline for pdf, 404 for unknown. 56/56 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>