Files
slp-modular-cms/aidlc-docs/features/gitea-deployment-workflow/inception/plans/application-design-plan.md
T
SluijsensandClaude Opus 5 8568ca43c6 Plans the Gitea deployment feature and refreshes the codebase analysis
Adds the AI-DLC inception record for deploying the CMS as a single .NET
application on hosting where no server configuration is possible.

The reverse-engineering artifacts were regenerated: the previous set
predated the Master module, the Slave host, the solution reorganisation
and single-host serving, all of which matter for deployment. Findings
were verified by running the build, both test suites and the linter
rather than inferred, which surfaced two facts the plan depends on:
the frontend lint gate currently fails (5 errors), and two transitive
packages carry high-severity advisories.

Records 24 functional requirements, 32 traced decisions and a
seven-unit decomposition whose ordering is load-bearing: durability
work must land before the first automated deploy, or the very first
deploy is the one that silently breaks master/slave trust.

Two conflicts found while designing and carried into the units:
- Both modules call AddDataProtection(), which runs after the host and
  would override a persistent key store while still passing any
  registration test.
- The availability gate runs before authentication, so its admin
  bypass cannot read HttpContext.User.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-27 23:59:30 +02:00

16 KiB

Application Design Plan — Gitea Deployment Workflow

Stage: INCEPTION — Application Design Scope: high-level component identification, responsibilities, interfaces and service-layer orchestration. Detailed business logic follows per unit in Functional Design.


Part 1 — Design Steps

Step 1: Context analysis

  • Read requirements.md (23 FRs, 10 NFRs, 32 decisions, ASM-01, OPEN-01…04)
  • Read execution-plan.md (7 units, risk level High)
  • Read the shared reverse-engineering artifacts
  • Inspect Program.cs of both host projects to establish the current composition baseline

Step 2: Component identification

  • Identify new components introduced by this feature
  • Decide the home project for each (Core versus each host) — see Question 1
  • Establish which components the Slave host inherits and which it must not — see Question 2
  • Define component boundaries and responsibilities

Step 3: Static-file serving redesign

  • Design the two-mount model (wwwroot/web/ at /, wwwroot/admin/ at /admin) — see Question 3
  • Define fallback precedence and the nonfile constraint behaviour
  • Establish middleware ordering relative to security headers and the availability gate — see Question 4

Step 4: Cross-cutting component interfaces

  • Define the security-headers component and its configuration surface — see Question 5
  • Define the CSP path-scoping mechanism — see Question 6
  • Define health-check registration and its endpoint
  • Define Data Protection key-ring placement — see Question 7
  • Define migration-at-startup placement and failure behaviour — see Question 8

Step 5: Service layer and orchestration

  • Define registration extension methods and their composition order
  • Decide whether shared host composition is extracted — see Question 9
  • Define the observability registration surface (Sentry, logging) — see Question 10

Step 6: Deployment transport abstraction

  • Design the transport seam that admits FTPS later without restructuring (NFR-09, D-02) — see Question 11

Step 7: Scope confirmation

  • Resolve OPEN-02 ownership — see Question 12

Step 8: Mandatory design artifacts

  • Generate components.md — component definitions and high-level responsibilities
  • Generate component-methods.md — method signatures and input/output types
  • Generate services.md — service definitions and orchestration patterns
  • Generate component-dependency.md — dependency matrix, communication patterns, data flow
  • Generate application-design.md — consolidated design document
  • Validate design completeness and consistency against all 23 FRs
  • Verify Security Baseline compliance for the design

Part 2 — Design Questions

Vul je keuze in achter elke [Answer]:-tag. Kies de laatste optie (Anders) als niets past.


Question 1 — Waar horen de nieuwe cross-cutting componenten?

Context: SlpModularCms.Core wordt door beide hosts gebruikt (Api en Api.Slave). Alles wat je in Core registreert, krijgt de Slave er automatisch bij. Core heeft al FrameworkReference: Microsoft.AspNetCore.App, dus middleware in Core kan technisch prima.

Het gaat om vier nieuwe zaken: health checks, securityheaders-middleware, Data Protection key ring, en Sentry/logging.

Waar komen die te staan?

A) Alles in Core, aangeboden als extension methods (AddCmsHealthChecks(), AddCmsSecurityHeaders(), …) — beide hosts krijgen identiek gedrag, geen duplicatie B) Alles in het Api-host-project — de Slave is puur een lokaal ontwikkelhulpmiddel en heeft dit niet nodig C) Gesplitst: infrastructuur die beide hosts nodig hebben (Data Protection, health checks, logging) in Core; wat alleen met het publieke serveren te maken heeft (securityheaders) in Api X) Anders (beschrijf hieronder na de Answer:-tag)


Question 2 — Wat krijgt de Slave-host wél en niet?

Context: SlpModularCms.Api.Slave draait alleen lokaal, heeft geen testproject, en wordt níet gedeployed. Maar hij deelt wel de master↔slave-protocolcode, en juist daar speelt de Data Protection key ring een rol.

Welke van de nieuwe voorzieningen moet de Slave krijgen?

A) Alles behalve de statics — dus wél health check, securityheaders, key ring, Sentry; geen wwwroot/web of /admin. Maximale gelijkenis met productiegedrag B) Alleen wat functioneel nodig is voor het master/slave-protocol: de Data Protection key ring. Geen health check, securityheaders of Sentry — die voegen lokaal niets toe C) Alles wat Core biedt (volgt automatisch uit Question 1 = A), en verder niets host-specifieks X) Anders (beschrijf hieronder na de Answer:-tag)

Answer:A, Want de Slave is wel een API die laat zien hoe een klant-API eruit kan komen te zien.


Question 3 — Hoe worden de twee statics-mappen bediend?

Context: nu doet Program.cs UseDefaultFiles() + UseStaticFiles() op wwwroot/, met twee MapFallbackToFile-regels. Met de nieuwe indeling moet / uit wwwroot/web/ komen en /admin uit wwwroot/admin/.

A) Twee expliciete UseStaticFiles-registraties met elk een eigen PhysicalFileProvider en RequestPath — expliciet en goed leesbaar, elk pad heeft zijn eigen configuratie (en kan later eigen headers krijgen) B) WebRootPath verleggen naar wwwroot/web en /admin als losse extra mount toevoegen — kleinste wijziging, maar wwwroot betekent dan iets anders dan de mapnaam suggereert C) Eén statics-registratie op wwwroot/ houden en het onderscheid puur via fallback-routes regelen — minste code, maar dan is wwwroot/web/index.html ook direct op /web/index.html bereikbaar X) Anders (beschrijf hieronder na de Answer:-tag)


Question 4 — Waar in de pipeline komen de securityheaders?

Context: de huidige volgorde is exception handler → rate limiter → (dev: OpenAPI/Scalar) → HTTPS redirect → statics → CORS → availability-gate → auth → endpoints.

Statics short-circuiten: een bestaand bestand wordt direct geserveerd en alles daarna draait niet meer. Securityheaders die ná de statics staan, komen dus nooit op de publieke website terecht.

A) Direct vóór de statics — dan krijgen álle responses de headers, inclusief statische bestanden en de publieke website B) Direct ná de exception handler, helemaal vooraan — dan krijgen ook foutresponses de headers C) Alleen op de SPA/HTML-responses, niet op assets — minder overhead op afbeeldingen en scripts X) Anders (beschrijf hieronder na de Answer:-tag)


Question 5 — Hoe configureerbaar moeten de securityheaders zijn?

Context: de Umami-script-origin en de Sentry-ingest-origin moeten in de CSP toegelaten worden, en die verschillen per omgeving. Configuratie hoort volgens de bestaande stijl in appsettings via het Options-patroon.

A) Volledig via een nieuwe SecurityHeaders-sectie in appsettings.json, met een typed options-class — consistent met JwtSettings, MasterModule en de rest B) Vaste, in code ingebakken headers met alleen de CSP-uitzonderingen (Umami/Sentry-origins) configureerbaar — minder knoppen om verkeerd te zetten C) Volledig in code, met de origins afgeleid uit de bestaande Sentry- en Umami-configuratie — geen aparte sectie nodig X) Anders (beschrijf hieronder na de Answer:-tag)


Question 6 — Hoe wordt de CSP per pad gescopet?

Context: je koos strikt voor /admin en /api/v1, ruimer voor de publieke website (D-31/CQ5 = B). Dat vraagt een mechanisme dat per request beslist welke CSP geldt.

A) Padprefix-vergelijking in de middleware: begint het pad met /admin of /api/v1 → strikte policy, anders de ruime — eenvoudig en direct leesbaar B) Een configureerbare lijst van pad-naar-policy-regels in appsettings, zodat je later paden kunt toevoegen zonder code te wijzigen C) Twee losse middleware-registraties met UseWhen() op padprefix — elk met zijn eigen policy, geen if-logica binnen één component X) Anders (beschrijf hieronder na de Answer:-tag)


Question 7 — Welke DbContext huisvest de Data Protection keys?

Context: PersistKeysToDbContext<T> vereist een DbContext die IDataProtectionKeyContext implementeert. Er zijn er drie: ApplicationDbContext (Core/Identity, migreert straks automatisch), AvailabilityDbContext en MasterDbContext (beide migreren al automatisch). Ze delen één connection string.

A) ApplicationDbContext — de sleutels zijn infrastructuur van de hele applicatie, niet van één module. Vereist een nieuwe Core-migratie B) Een eigen, nieuwe DataProtectionDbContext — maximale scheiding, maar een vierde context en een vierde migratieset C) AvailabilityDbContext — die zit het dichtst bij de master/slave-functionaliteit waarvoor de sleutels gebruikt worden X) Anders (beschrijf hieronder na de Answer:-tag)


Question 8 — Wat gebeurt er als de migratie bij het opstarten faalt?

Context: ApplicationDbContext gaat automatisch migreren bij startup (FR-11). De twee modulecontexts doen dat al. De vraag is wat er moet gebeuren als dat misgaat — bijvoorbeeld doordat de database niet bereikbaar is of een migratie stukloopt.

Dit raakt de health check direct: bij "fail fast" start het proces niet, waardoor /health niets teruggeeft en UptimeRobot dus rood wordt — precies wat je wilt weten.

A) Fail fast — gooi de fout door, het proces start niet. Een half-werkende applicatie is erger dan een zichtbaar dode B) Loggen en toch doorstarten — de applicatie draait, en fouten worden zichtbaar zodra iemand de database aanraakt C) Fail fast in productie, loggen-en-doorstarten in Development — lokaal niet geblokkeerd worden door een migratieprobleem X) Anders (beschrijf hieronder na de Answer:-tag)


Question 9 — De twee Program.cs-bestanden zijn bijna identiek

Context: SlpModularCms.Api/Program.cs en SlpModularCms.Api.Slave/Program.cs verschillen alleen in de statics/SPA-fallbacks. Deze feature voegt aan beide dezelfde nieuwe registraties toe, waardoor de duplicatie groeit en het risico ontstaat dat ze uit elkaar gaan lopen.

A) Laat de duplicatie staan — twee losse hosts die expliciet zijn, is duidelijker dan een gedeelde abstractie. Deze feature blijft klein B) Extraheer de gedeelde compositie naar één extension method in Core (bijv. AddCmsHost() / UseCmsPipeline()); elke host voegt alleen zijn eigen specifieke stukken toe C) Extraheer alleen de nieuwe registraties uit deze feature naar gedeelde extension methods, en laat de bestaande duplicatie ongemoeid — kleinste risico, geen regressie in bestaand gedrag X) Anders (beschrijf hieronder na de Answer:-tag)


Question 10 — Hoe wordt Sentry geregistreerd?

Context: Sentry.AspNetCore haakt normaal in op de host-builder. Sentry moet optioneel blijven: zonder DSN geen initialisatie, alleen console-logging (FR-14).

A) Eén extension method die alles doet (Sentry + logging-configuratie), die zichzelf overslaat als er geen DSN is — één plek om naar te kijken B) Sentry en de logging-configuratie apart registreren, zodat je structured logging ook zonder Sentry kunt aanzetten C) Sentry alleen in het Api-host-project, logging-configuratie in Core X) Anders (beschrijf hieronder na de Answer:-tag)


Question 11 — Hoe ziet de transport-abstractie eruit?

Context: nu SSH/SCP, later mogelijk FTPS erbij, zonder de workflow te herstructureren (D-02, NFR-09). Dit is een ontwerpkeuze in de workflow-laag, niet in C#.

A) deploy.yaml krijgt een transport-input (scp nu, ftps later) en kiest intern de juiste stap met een if-conditie — één bestand, één interface, transport is een parameter B) Eén reusable workflow per transport (deploy-scp.yaml, later deploy-ftps.yaml), met een identieke input-interface; de aanroeper kiest welke — schoner gescheiden, iets meer bestanden C) Eén deploy.yaml met alleen SCP nu, en de FTPS-variant later toevoegen wanneer die daadwerkelijk nodig is — geen abstractie bouwen voor iets wat er nog niet is X) Anders (beschrijf hieronder na de Answer:-tag)


Question 12 — Wat doen we met OPEN-02?

Context: AvailabilityMiddleware.IsAdminBypass leest de JWT met ReadJwtToken zonder de handtekening te valideren. Iemand kan dus een zelfgemaakt, ongetekend token met een Owner-rol meesturen en daarmee de availability-gate omzeilen. Beveiligde endpoints weigeren daarna alsnog (echte authenticatie gebeurt wel goed), dus dit is geen rechten-escalatie — maar de gate is er wel mee te omzeilen door een niet-geauthenticeerde beller.

Dit is pre-existent en niet door deze feature veroorzaakt. Onder SECURITY-08 is het wel een echte bevinding.

A) Meenemen in deze feature — het is een kleine fix (valideer het token, of gebruik het al geauthenticeerde HttpContext.User) en het raakt precies de middleware die we toch aanpassen voor de /health-bypass B) Doorschuiven naar tech-debt-backlog als aparte feature — deze feature blijft over deployment gaan C) Alleen documenteren als geaccepteerd risico met onderbouwing, en verder niets X) Anders (beschrijf hieronder na de Answer:-tag)


Part 3 — Follow-up Questions (Ronde 2)

Twaalf antwoorden waren eenduidig. Eén punt moet ik terugleggen omdat het letterlijk uitvoeren van je antwoord de beveiliging zou verzwakken, en één punt wil ik bevestigen omdat ik twee antwoorden heb samengevoegd.


Follow-up Question 1 — nosniff hoort juist wél op assets

Context: bij Question 4 koos je C: securityheaders alleen op SPA/HTML-responses, niet op assets. Voor de meeste headers klopt dat — een CSP of X-Frame-Options op een PNG doet niets.

Maar voor twee headers werkt dat averechts:

  • X-Content-Type-Options: nosniff bestaat er juist om te voorkomen dat de browser het type van een niet-HTML-bestand verkeerd raadt. Een geüpload .txt- of .svg-bestand dat de browser als HTML of JavaScript interpreteert, is precies het aanvalsscenario. Alleen op HTML zetten haalt de maatregel dus weg waar hij nodig is.
  • Strict-Transport-Security is een transportregel voor het hele domein, geen paginaregel. Browsers onthouden hem per host; hem alleen op HTML sturen werkt in de praktijk wel, maar is onnodig fragiel — een bezoeker die als eerste een asset ophaalt, krijgt hem dan niet.

Content-Security-Policy, X-Frame-Options en Referrer-Policy zijn wél zinvol HTML-only.

Hoe wil je het?

A) Per header het passende bereik: X-Content-Type-Options en Strict-Transport-Security op alle responses; Content-Security-Policy, X-Frame-Options en Referrer-Policy alleen op HTML-responses (aanbevolen — dit is wat je met C bedoelde, maar zonder het gat) B) Toch strikt alle vijf de headers alleen op HTML-responses, zoals letterlijk geantwoord C) Alle vijf op alle responses — eenvoudigst, iets meer bytes per asset X) Anders (beschrijf hieronder na de Answer:-tag)


Follow-up Question 2 — Bevestiging van de configuratie-indeling

Context: Question 5 = B ("vaste headers in code, alleen de CSP-uitzonderingen configureerbaar — minder knoppen om verkeerd te zetten") en Question 6 = B ("een configureerbare lijst van pad-naar-policy-regels, zodat je later paden kunt toevoegen zonder code te wijzigen") lijken elkaar tegen te spreken: de een wil weinig configuratie, de ander voegt configuratie toe.

Ik lees ze als samen consistent, op één manier:

  • De policy-definities zelf (wat "strikt" en wat "ruim" betekent) staan in code — dat is Question 5 = B
  • De toewijzing van pad aan policy staat in configuratie, zodat je later een pad kunt toevoegen zonder code te wijzigen — dat is Question 6 = B
  • De uitzonderingsorigins (Umami-script, Sentry-ingest) staan in configuratie, want ze verschillen per omgeving

Concreet zou appsettings er dan ongeveer zo uitzien:

"SecurityHeaders": {
  "PathPolicies": [
    { "PathPrefix": "/admin", "Policy": "Strict" },
    { "PathPrefix": "/api/v1", "Policy": "Strict" }
  ],
  "DefaultPolicy": "Relaxed",
  "AllowedScriptOrigins": [ "https://analytics.slpsoftware.nl" ],
  "AllowedConnectOrigins": [ "https://<sentry-ingest-host>" ]
}

Klopt die lezing?

A) Ja, precies zo — policies in code, padtoewijzing en origins in configuratie B) Nee, ik wil de policy-inhoud zelf ook configureerbaar (volledige CSP-strings in appsettings) C) Nee, ik wil juist minder: ook de padtoewijzing in code, alleen de origins configureerbaar X) Anders (beschrijf hieronder na de Answer:-tag)