Designs the security headers and observability units
Records the functional design for the two remaining application units, before any of their code exists. Security headers have to come from the application, because relying on nginx or IIS configuration is exactly what this deployment model rules out. Strict applies to /admin, /api/v1 and /health; a relaxed policy applies to the public website, which this repository does not author. The strict policy needs style-src 'unsafe-inline'. That is not a shortcut: Radix positions dropdowns and dialogs with inline style attributes recalculated per click and scroll position, and CSP nonces apply only to style elements, never to style attributes. No nonce- or hash-based variant leaves the admin UI working. The exception is bounded to styles — script-src stays closed, which is where XSS actually lives. The website's policy is enforcing rather than absent, so every HTML-serving path carries a CSP and no exception has to be recorded. It still blocks external script origins, so it remains a real boundary. HSTS is skipped in development: browsers remember it per host and localhost is shared with unrelated projects. Every other header applies locally, so a CSP violation surfaces while developing. For observability, browser error reports tunnel through the API rather than going to Sentry directly. Ad blockers block Sentry domains, which loses errors precisely for the users most likely to have browser oddities. The tunnel forwards only to the host derived from the configured DSN — a caller-supplied destination would turn an anonymous endpoint into a request-forgery primitive. Two consequences of the chosen options are recorded rather than left implicit: Enabling SendDefaultPii attaches request headers, and this application carries two standing credentials in them. Besides the refreshToken cookie, X-Master-Api-Key would have been sent to a third party on every error raised during a master/slave call. The scrub list removes the whole Cookie header, Authorization, X-Master-Api-Key and the request body. Console logging at Information plus structured logging to Sentry would, taken literally, mean one Sentry event per request — exhausting the free plan within hours and burying real errors in request noise. The thresholds are split: console keeps Information, Sentry takes warnings and above as events with Information as breadcrumbs, so every event arrives carrying the trail that led to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# AI-DLC State Tracking
|
||||
|
||||
## Project Information
|
||||
- **Feature Name**: Gitea Deployment Workflow
|
||||
- **Feature Slug**: gitea-deployment-workflow
|
||||
- **Project Type**: Brownfield
|
||||
- **Start Date**: 2026-07-27T00:00:00Z
|
||||
- **Current Stage**: CONSTRUCTION - Code Generation, Round 1 (U1 + U2 complete, awaiting approval)
|
||||
- **Branch**: feature/gitea-deployment-workflow
|
||||
|
||||
## Workspace State
|
||||
- **Existing Code**: Yes
|
||||
- **Reverse Engineering Needed**: Completed — full rerun on 2026-07-27 (user chose Q3 = B)
|
||||
- **Workspace Root**: K:\Development\Projects\SlpModularCms
|
||||
|
||||
## Reverse Engineering Status
|
||||
- [x] Reverse Engineering — Completed on 2026-07-27
|
||||
- **Artifacts Location**: aidlc-docs/_shared/reverse-engineering/ (all 8 artifacts regenerated + timestamp)
|
||||
- **Verified by execution**: Release build 0 errors / 50 warnings; 219 backend tests pass; 213 frontend tests pass; `pnpm run lint` **fails** (5 errors, 1 warning); 2 high-severity transitive package advisories
|
||||
|
||||
## Code Location Rules
|
||||
- **Application Code**: Workspace root (NEVER in aidlc-docs/)
|
||||
- **Feature Documentation**: aidlc-docs/features/gitea-deployment-workflow/ only
|
||||
- **Shared Artifacts**: aidlc-docs/_shared/
|
||||
- **Structure patterns**: See code-generation.md Critical Rules
|
||||
|
||||
## Language Configuration
|
||||
- **Documentation Language**: English
|
||||
- **Conversation Language**: User Language (Dutch)
|
||||
|
||||
## Extension Configuration
|
||||
| Extension | Enabled | Decided At |
|
||||
|---|---|---|
|
||||
| Security Baseline | Yes (blocking) | Requirements Analysis |
|
||||
| Property-Based Testing | No | Requirements Analysis |
|
||||
|
||||
## Operations Configuration
|
||||
- **Include Operations Phase**: Yes
|
||||
- **Decided At**: Requirements Analysis
|
||||
|
||||
## Scope Decisions (from feature-selection.md)
|
||||
- **Public website**: documentation/instructions only — where the website build lands in `wwwroot/`, how it coexists with `wwwroot/admin/`, and what a per-website workspace must deliver. The website's own build/deploy workflow stays out of scope (Q4 = A).
|
||||
- **Environments**: local, test, production only.
|
||||
- **Observability stack**: UptimeRobot (uptime), Umami (analytics), console logging + Sentry (logging/errors).
|
||||
- **Deployment constraint**: upload as a published .NET application; no server configuration may be required.
|
||||
- **Reference**: existing working Gitea Actions setup at `K:\Development\SlpSoftware\Projects\SlpSoftware` (React/Vite) is the starting point.
|
||||
- **Health check endpoint**: IN SCOPE (decided 2026-07-27). **Liveness only** — `AddHealthChecks()` + `MapHealthChecks("/health")`, no package needed and **no database check** (Q17 = A / D-21, superseding the earlier note that `AddDbContextCheck` might be included). `/health` must be added to `AvailabilityMiddleware._bypassPrefixes` so the availability gate cannot return 503 for it. Health = infrastructure liveness; Availability/capabilities = CMS domain state — these stay strictly separate.
|
||||
|
||||
> **Note**: this section records the earliest scope decisions. The authoritative and complete decision set is `inception/requirements/requirements.md` § 3 (D-01…D-32) — in particular, Q4 = C changed the public website from living directly in `wwwroot/` to `wwwroot/web/`.
|
||||
|
||||
## Stage Progress
|
||||
|
||||
### INCEPTION
|
||||
- [x] Workspace Detection — Complete
|
||||
- [x] Reverse Engineering — Complete, approved 2026-07-27 (full rerun of all 8 `_shared/` artifacts)
|
||||
- [x] Requirements Analysis — Complete, approved 2026-07-27. 24 FRs (FR-24 added at Application Design), 10 NFRs, 32 decisions, 7 assumptions, 4 open items, 4 documented security deviations. Two question rounds: `requirement-verification-questions.md` (25 Q) and `requirement-clarification-questions.md` (5 Q).
|
||||
- [x] User Stories — **SKIP** (infrastructure/operations work; no new end-user functionality or persona. Offered at Requirements Analysis approval, not requested.)
|
||||
- [x] Workflow Planning — Complete, approved 2026-07-27. Artifact: `inception/plans/execution-plan.md`
|
||||
- [x] Application Design — Complete, approved 2026-07-27. 14 code components (9 new, 5 modified) + 2 workflow components. Artifacts in `inception/application-design/`. Two composition conflicts found and carried to Unit 2. Added FR-24, closed OPEN-02.
|
||||
- [x] Units Generation — Complete (awaiting approval). 7 units in 4 execution rounds. Artifacts: `unit-of-work.md`, `unit-of-work-dependency.md`, `unit-of-work-story-map.md`
|
||||
|
||||
### CONSTRUCTION
|
||||
Units finalised at Units Generation (see `inception/application-design/unit-of-work.md`):
|
||||
U1 Hosting & Serving · U2 Data Durability · U3 Security Headers & CSP · U4 Observability · U5 CI Workflow & Gates · U6 Deploy Workflow · U7 Documentation
|
||||
|
||||
Execution rounds (Q4 = B): **R1** = U1 + U2 · **R2** = U3 + U4 · **R3** = U5 + U6 · **R4** = U7. One commit per unit; single PR at the end (Q6 = A).
|
||||
|
||||
- [~] Functional Design — **EXECUTE for U1, U2, U3, U4**; SKIP for U5, U6, U7. **U1 ✅ U2 ✅** approved 2026-07-27
|
||||
- [ ] NFR Requirements — **SKIP (all units)** — already comprehensively captured in `requirements.md` § 5 and § 6
|
||||
- [ ] NFR Design — **EXECUTE for U3, U4**; SKIP for the rest. *Deliberate deviation from the default NFR-Requirements/NFR-Design coupling — rationale in the execution plan.*
|
||||
- [ ] Infrastructure Design — **EXECUTE for U6, U7**; SKIP for the rest
|
||||
- [~] Code Generation — **EXECUTE** (all 7 units, each built and tested before its completion message). **U1 ✅ U2 ✅** generated and verified 2026-07-27 — build 0 errors, 253 backend tests pass (was 219)
|
||||
- [ ] Build and Test — **EXECUTE**
|
||||
|
||||
### OPERATIONS
|
||||
- [ ] Deployment Setup — **EXECUTE**
|
||||
- [ ] Monitoring Setup — **EXECUTE**
|
||||
- [ ] Production Readiness Validation — **EXECUTE** (includes the `dotnet-appsettings` compliance gate)
|
||||
|
||||
## Execution Plan Summary
|
||||
- **Risk Level**: High — three destructive-and-silent failure modes (customer website loss, Data Protection key-ring loss, automatic migration against production)
|
||||
- **Stages to Execute**: Functional Design (×4: U1–U4), NFR Design (×2: U3, U4), Infrastructure Design (×2: U6, U7), Code Generation (×7), Build and Test, Deployment Setup, Monitoring Setup, Production Readiness Validation
|
||||
- **Stages to Skip**: User Stories (no end-user functionality), NFR Requirements (already captured), plus per-unit skips as listed above
|
||||
|
||||
## Current Status
|
||||
- **Lifecycle Phase**: CONSTRUCTION
|
||||
- **Current Stage**: Code Generation complete for Round 1 (U1 Hosting & Serving, U2 Data Durability)
|
||||
- **Next Stage**: Round 2 — U3 Security Headers & CSP + U4 Observability, starting with Functional Design
|
||||
- **Status**: Awaiting Round 1 code approval
|
||||
|
||||
## Round 1 Verification Record (2026-07-27)
|
||||
- `dotnet build SlpModularCms.sln -c Release` — 0 errors
|
||||
- Backend tests — **253 passed, 0 failed** (Core 83, Availability 82, Identity 37, Master 51); baseline was 219
|
||||
- New EF migration `20260727203036_AddDataProtectionKeys` — verified purely additive
|
||||
- Embedded placeholder resource name verified against the compiled assembly manifest
|
||||
- **Carried to phase-level Build and Test**: composed-startup behaviour that needs a running host and a real database — `/admin` trailing-slash redirect, 404-vs-HTML for missing assets, SPA fallback and placeholder resolution, `/health` while availability-disabled, `MigrateCoreDatabase` against SQL Server, and both hosts starting
|
||||
- **Deviation**: U1 plan Step 11 (`StaticContentTests`) not implemented — the code lives in `SlpModularCms.Api`, which has no test project by convention; behaviour carried to Build and Test instead. Recorded in the unit's `generation-summary.md`
|
||||
@@ -0,0 +1,802 @@
|
||||
# Audit Log — Gitea Deployment Workflow
|
||||
|
||||
## Initial Request
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Ik wil een Gitea workflow gaan opzetten om de CMS te kunnen deployen. In de instructies moet ook behandeld worden waar de frontend van de website moet gaan komen, maar de workflow voor de website zelf zal hierin niet worden gebouwd, datwordt per werbsite in hun eigen workspace gedaan. Gebruik de aidlc workflow om alles op te zetten. Voor Uptime gebruik ik UptimeRobot, voor analytics gebruik ik Umami en voor logging console logs samen met Sentry. Ik heb in de workspace `K:\Development\SlpSoftware\Projects\SlpSoftware` al een werkende workflow. Dit is voor een react vite front-end, maar is een goed startpunt voor deze workspace. Op dit moment zijn er alleen een lokale omgeving, test en productie. Het doel van deze workflow en eventuele deployment-opzet is dat we boel moeten kunnen uploaden als een .NET-applicatie en dat we geen server-configuratie hoeven te doen omdat dat voor andere websites waarschijnlijk niet kan. Vandaar dat ik een opzet wilde met de api en de 2 frontends als 1 website."
|
||||
|
||||
**AI Response**: "Started AI-DLC workflow. Loaded common rules and extension opt-in files. Performed workspace scan and reference-workspace analysis. Presented feature resolution, language preference, reverse-engineering staleness handling, and public-website scope questions in `aidlc-docs/feature-selection.md`."
|
||||
|
||||
**Context**: INCEPTION — Workspace Detection. Multi-feature workspace detected (`active-features.md` exists, 5 existing features). Request does not match any existing feature; proposed new feature `gitea-deployment-workflow`.
|
||||
|
||||
---
|
||||
|
||||
## Workspace Detection — Findings
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated analysis step)"
|
||||
|
||||
**AI Response**: "Recorded workspace state and reference-workflow analysis."
|
||||
|
||||
**Context**: INCEPTION — Workspace Detection
|
||||
|
||||
### Workspace State
|
||||
- **Existing Code**: Yes
|
||||
- **Programming Languages**: C# (.NET 10), TypeScript/React
|
||||
- **Build System**: MSBuild / `dotnet` (solution `SlpModularCms.sln`); pnpm + Vite for `frontend/`
|
||||
- **Project Structure**: Modular monolith — `src/SlpModularCms.Api` (host), `src/SlpModularCms.Api.Slave`, `src/SlpModularCms.Core`, `src/SlpModularCms.Modules.{Master,Identity,Availability}` plus mirrored test projects; `frontend/` admin SPA outside `src/`
|
||||
- **Workspace Root**: `K:\Development\Projects\SlpModularCms`
|
||||
- **Project Type**: Brownfield
|
||||
- **Git Branch**: `master`
|
||||
- **CI/CD present**: No `.gitea/` directory exists in this repo yet
|
||||
|
||||
### Reverse Engineering Artifact Status
|
||||
- Artifacts exist in `aidlc-docs/_shared/reverse-engineering/` (8 files), dated **2026-06-16T20:30:00Z**
|
||||
- Assessed as **stale**: commits after that date changed deployment-relevant structure — `master-cms-module`, `local-dev-master-slave-setup`, solution reorganization (`754bd97`), and single-host serving (`3885703`)
|
||||
- Staleness handling deferred to the user via Question 3 in `feature-selection.md`
|
||||
|
||||
### Reference Workflow Analysis (`K:\Development\SlpSoftware\Projects\SlpSoftware`)
|
||||
Existing, working Gitea Actions setup for a React/Vite frontend — to be used as the starting point for this feature:
|
||||
|
||||
- `.gitea/workflows/continuous_integration.yaml` — triggers on `pull_request`, `push` to `master`, and `workflow_dispatch` (with a `deploy_production` boolean input). Jobs: `config` (re-exports `env:` values as job outputs so they are usable in a reusable workflow's `with:` block), `prepare`, `build` (test env), `build-production` (separate build because `VITE_APP_ENV` is build-time), `test` (lint + unit tests), `deploy-test`, `deploy-production`.
|
||||
- `.gitea/workflows/deploy.yaml` — reusable `workflow_call` workflow taking `artifact_name`, `environment`, `deploy_path`; downloads the artifact and uploads it over SCP using `sshpass` in a plain shell step (the `appleboy/scp-action` container action fails on the Podman-backed runner with a 409 attach error).
|
||||
- Configuration split: non-secret values (Umami script URL / website IDs per environment, Sentry DSN) as Gitea Actions **variables**; SSH credentials as **secrets**.
|
||||
- Operations artifacts under `aidlc-docs/features/react-frontend/operations/` include `deployment-instructions.md`, `deployment-plan.md`, `rollback-plan.md`, nginx example configs, Umami podman-compose setup, `monitoring-plan.md`, `monitoring-setup.md`, `umami-setup.md`, and a `production-readiness-checklist.md`.
|
||||
|
||||
### Key Difference From The Reference
|
||||
The reference deploys a **static `dist/` folder** to an nginx webroot on self-hosted Raspberry Pis. This feature must instead deploy a **published .NET application** to shared hosting with **no server configuration possible**, which is why the API serves the public website (`/`), the admin SPA (`/admin`), and the API (`/api/v1`) from a single host process.
|
||||
|
||||
---
|
||||
|
||||
## Workspace Detection — Feature Resolution Answers
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "klaar, antwoorden staan in het bestand" — answers recorded in `aidlc-docs/feature-selection.md`: Q1 = "A, maak ook een nieuwe feature-branch aan voor deze feature.", Q2 = "A", Q3 = "B", Q4 = "A"
|
||||
|
||||
**AI Response**: "Validated answers — no contradictions or ambiguities detected. Created git branch `feature/gitea-deployment-workflow`, created `aidlc-state.md` for the feature, and registered the feature in `aidlc-docs/active-features.md` with status 🔵 Inception. Proceeding to Reverse Engineering (full rerun)."
|
||||
|
||||
**Context**: INCEPTION — Workspace Detection complete. Decisions: (1) new feature `gitea-deployment-workflow` on its own feature branch; (2) Documentation Language = English, Conversation Language = Dutch; (3) full reverse engineering rerun of all 8 `_shared/` artifacts; (4) public website handled as documentation/instructions only — its build/deploy workflow stays out of scope.
|
||||
|
||||
---
|
||||
|
||||
## Conflict Detection
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated analysis step)"
|
||||
|
||||
**AI Response**: "Compared affected components against other non-complete features."
|
||||
|
||||
**Context**: INCEPTION — Workspace Detection. One other active feature: `tech-debt-backlog` (🔵 Inception), affecting `Modules.Master` and `Frontend`. Overlap with this feature is limited to `Frontend` (this feature only touches its build/deploy configuration, not its source). Informational only — does not block progress; developer handles at merge time.
|
||||
|
||||
---
|
||||
|
||||
## Reverse Engineering — Start
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — stage entered automatically after Workspace Detection)"
|
||||
|
||||
**AI Response**: "Loaded `inception/reverse-engineering.md`. Started full rerun of all 8 shared artifacts per the user's Q3 = B decision."
|
||||
|
||||
**Context**: INCEPTION — Reverse Engineering. Full rerun, not a targeted refresh.
|
||||
|
||||
---
|
||||
|
||||
## Reverse Engineering — Complete
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated analysis and generation)"
|
||||
|
||||
**AI Response**: "Regenerated all 8 artifacts plus the timestamp file in `aidlc-docs/_shared/reverse-engineering/`. Updated the feature's `aidlc-state.md` with Reverse Engineering Status. Presented the completion message and awaiting explicit approval."
|
||||
|
||||
**Context**: INCEPTION — Reverse Engineering complete.
|
||||
|
||||
### Scope of analysis
|
||||
122 C# files (excluding `bin`/`obj`) and 111 TypeScript/TSX files under `frontend/src`, plus solution, project, configuration and lock files.
|
||||
|
||||
### Verification performed by execution (not inferred)
|
||||
- `dotnet build SlpModularCms.sln -c Release` — 0 errors, 50 warnings
|
||||
- `dotnet test SlpModularCms.sln -c Release` — 219 tests: Core 54, Identity 37, Availability 78, Master 50; all passed
|
||||
- `cd frontend && pnpm test` — 34 files, 213 tests, all passed
|
||||
- `cd frontend && pnpm run lint` — **FAILS**: 5 errors, 1 warning
|
||||
- `dotnet list package --vulnerable --include-transitive` — `Microsoft.OpenApi` 2.0.0 (High), `System.Security.Cryptography.Xml` 10.0.9 (High)
|
||||
|
||||
### Key deployment-relevant findings
|
||||
1. No CI/CD exists — the repository has no `.gitea/workflows/`.
|
||||
2. `dotnet publish` on `SlpModularCms.Api` requires Node and pnpm: the `BuildAndCopyAdminFrontend` target runs `pnpm install --frozen-lockfile` and `pnpm build` before publish.
|
||||
3. The admin SPA bundle is environment-specific — `frontend/src/lib/config.ts` requires `VITE_API_BASE_URL` as an absolute URL, so test and production cannot share one artifact unless a same-origin default is introduced.
|
||||
4. No health-check endpoint exists for uptime monitoring; the three anonymous candidates (`/api/v1/Availability/status`, `/api/v1/System/capabilities`, `/`) each prove something different, and only `capabilities` goes red when an instance is deliberately disabled.
|
||||
5. The public website at `/` is not behind the availability gate — static files short-circuit before `AvailabilityMiddleware` is installed.
|
||||
6. `ModuleOrchestrator` discovers modules by globbing `SlpModularCms.Modules.*.dll` in the app base directory, so deployment content determines capability; load failures are logged, not fatal.
|
||||
7. Data Protection uses the default file-system key ring with no persistent store — a redeploy that discards it makes stored slave API keys unreadable.
|
||||
8. No forwarded-headers middleware while `UseHttpsRedirection()` runs early — relevant behind a hosting provider's TLS-terminating proxy.
|
||||
9. `ApplicationDbContext` migrations are never applied automatically, while both module contexts self-migrate at startup.
|
||||
10. No `Test` environment configuration exists (`appsettings.Test.json` absent, no `ASPNETCORE_ENVIRONMENT` value defined for test).
|
||||
11. Observability is entirely greenfield — no Sentry, Umami, structured logging, metrics or tracing anywhere. Production log level is `Warning`.
|
||||
12. No `packages.lock.json` for any .NET project, so NuGet restore is not reproducible (the frontend is locked via `pnpm-lock.yaml`).
|
||||
13. `AvailabilityMiddleware.IsAdminBypass` reads the JWT without validating its signature, so the availability gate is bypassable by an unauthenticated caller (not a privilege escalation — protected endpoints still authenticate).
|
||||
|
||||
---
|
||||
|
||||
## Reverse Engineering — Change Requested
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "De availability is geen health check. Die zou dan nog apart moeten worden ingeregeld. De availabillity en capabilities is functionaliteit van de CMS zelf en/of tussen slave en master."
|
||||
|
||||
**AI Response**: "Correction accepted and applied. The earlier framing of `/api/v1/Availability/status` and `/api/v1/System/capabilities` as candidate monitoring endpoints was wrong. Both are CMS domain functionality — availability is the product's own on/off state (local switch plus master gate) and capabilities reports loaded modules; both also serve the master↔slave protocol. Neither reflects application health and neither may be repurposed for monitoring. Updated `api-documentation.md` (§ Observability endpoints rewritten), `architecture.md` (deployment-relevant observation 4) and `code-quality-assessment.md` (technical-debt item 4) to state that a dedicated health check is a separate concern still to be designed: its own endpoint, outside `/api/v1` domain routing and outside the availability gate, reporting infrastructure liveness (process up, database reachable, migrations applied) rather than product state."
|
||||
|
||||
**Context**: INCEPTION — Reverse Engineering. Supersedes deployment-relevant finding 4 as originally logged in the "Reverse Engineering — Complete" entry above. Carried forward as a requirement for the Operations phase: a health-check endpoint must be built for UptimeRobot rather than reusing domain endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Reverse Engineering — Scope Addition: Health Check Endpoint
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Is er in .NET niet iets simpels om health checks toe te toevegen? Iets van AddHealthChecks()? Als het zo simpel is mag het erbij omdat het wel kan helpen bij deze feature. Anders wordt dat los opgepakt"
|
||||
|
||||
**AI Response**: "Confirmed it is that simple, and verified package availability. `builder.Services.AddHealthChecks()` and `app.MapHealthChecks(\"/health\")` require NO package — both are in the ASP.NET Core shared framework; default response is plain text `Healthy`/200 or `Unhealthy`/503, which is directly consumable by UptimeRobot. A database probe costs one package, `Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore` 10.0.9 (verified available on nuget.org and in line with the existing 10.0.x dependencies), plus `.AddDbContextCheck<ApplicationDbContext>()`, which does `CanConnectAsync` by default and can optionally report pending migrations — extra valuable here because `ApplicationDbContext` never migrates automatically. Flagged one codebase-specific requirement: `MapHealthChecks` registers an endpoint and middleware runs first, so `AvailabilityMiddleware` would return 503 for `/health` on a switched-off instance — reproducing exactly the conflation the user corrected. `/health` must therefore be added to `AvailabilityMiddleware._bypassPrefixes`. Recorded as in scope in `aidlc-state.md` and updated `api-documentation.md` and `code-quality-assessment.md` accordingly."
|
||||
|
||||
**Context**: INCEPTION — Reverse Engineering. Scope decision: a dedicated health-check endpoint is IN SCOPE for this feature (rather than being picked up separately). Separation of concerns is explicit: `/health` reports infrastructure liveness; Availability and capabilities remain CMS domain functionality and are never used for monitoring.
|
||||
|
||||
---
|
||||
|
||||
## Reverse Engineering — Approved
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Continue to Next Stage"
|
||||
|
||||
**AI Response**: "Reverse Engineering approved. Marked complete in `aidlc-state.md` and proceeded to Requirements Analysis."
|
||||
|
||||
**Context**: INCEPTION — Reverse Engineering approved, including the health-check scope addition and the availability/capabilities correction.
|
||||
|
||||
---
|
||||
|
||||
## Requirements Analysis — Intent Analysis
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated analysis step)"
|
||||
|
||||
**AI Response**: "Loaded architecture.md, component-inventory.md and technology-stack.md from `_shared/reverse-engineering/`, plus the reference project's operations artifacts (deployment-plan.md, monitoring-plan.md) at `K:\Development\SlpSoftware\Projects\SlpSoftware\aidlc-docs\features\react-frontend\operations\`. Performed intent analysis and generated 25 clarifying questions."
|
||||
|
||||
**Context**: INCEPTION — Requirements Analysis, Steps 1–6.
|
||||
|
||||
### Intent Analysis
|
||||
- **Request Clarity**: Clear on goals and tooling choices, incomplete on hosting/transport specifics — the user named the target stack (Gitea Actions, UptimeRobot, Umami, Sentry + console logging), the environments (local/test/production), the reference implementation, and the core constraint (upload as a .NET application, no server configuration possible). What is not yet specified is how the shared host is reached, how migrations and configuration are delivered, and how the public website's `wwwroot` content is protected from the CMS deploy.
|
||||
- **Request Type**: New Feature (CI/CD and operational tooling), with supporting Enhancement work in the application itself (health-check endpoint, and candidate changes for frontend config, Data Protection key ring, and migration strategy).
|
||||
- **Initial Scope Estimate**: Multiple Components — new `.gitea/workflows/` definitions, changes to `SlpModularCms.Api` (health checks, bypass list, possibly key ring and migrations), possibly `frontend/src/lib/config.ts`, new `appsettings.Test.json`, plus documentation and operations artifacts.
|
||||
- **Initial Complexity Estimate**: Moderate-to-Complex. Not algorithmically hard, but the constraint set is unusually tight: one host process serving three surfaces, a customer-owned `wwwroot` that must survive deploys, shared hosting with no server configuration, self-migrating module contexts alongside a manually-migrated Core context, and an environment-specific frontend bundle.
|
||||
- **Requirements Depth**: **Comprehensive** — justified by the number of interacting constraints, the deployment risk (a wrong `wwwroot` sync destroys a customer's website; a lost Data Protection key ring silently breaks master↔slave), and the fact that several findings require explicit product decisions rather than defaults.
|
||||
|
||||
### Notable reference-project context loaded
|
||||
- Deploy transport in the reference is `scp` + `sshpass` in a plain shell step, because container-based actions fail on the Podman-backed self-hosted runner with a 409 attach error. Shared hosting is unlikely to offer SSH, so transport is an open question here.
|
||||
- The reference deliberately splits test (automatic on `master`) from production (manual `workflow_dispatch` with a `deploy_production` checkbox), and builds twice because `VITE_APP_ENV` is build-time.
|
||||
- The reference uses one Sentry project with environment tags, a self-hosted Umami on `analytics.slpsoftware.nl` with per-environment website IDs, and UptimeRobot for uptime.
|
||||
|
||||
### Questions generated
|
||||
25 questions in `inception/requirements/requirement-verification-questions.md`, grouped: A hosting/transport (3), B public website in `wwwroot` (2), C workflow setup and gates (4), D database and configuration (4), E monitoring and observability (7), F rollback and reliability (2), G extensions and phasing (3 — security opt-in, PBT opt-in, Operations phase inclusion).
|
||||
|
||||
---
|
||||
|
||||
## Requirements Analysis — Answers Received, Clarification Round Opened
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "klaar, antwoorden staan in het bestand" — answers recorded in `inception/requirements/requirement-verification-questions.md`: Q1 = "X, Op de pi kan ik met FileZilla ook verbinden met SFTP. Is dat voldoende om FTP als uitgangspunt te nemen? Anders beginnen met opzetten zoals de referentie met SSH/SCP en later aanpassen naar FTPS als dat nodig is.", Q2 = B, Q3 = B, Q4 = "C, noem de map \"web\" in plaats van \"site\", Dus dan krijgen we uiteindelijk 3 mappen in wwwroot: admin, web en eventueel een map voor de API (indien nodig).", Q5 = B, Q6 = A, Q7 = A, Q8 = A, Q9 = "A, B, C, D, E", Q10 = C, Q11 = "X, Het mag naar same-origin, maar de frontend kan ook een aparte test-build hebben net als de referentie website. Ook is het zo dat nu lokaal wel een url moet worden opgegeven. Als dat verder ook blijft werken hoeft de url variabele niet blokkerend te zijn voor een enkele bundel", Q12 = A, Q13 = A, Q14 = A, Q15 = A, Q16 = C, Q17 = A, Q18 = C, Q19 = A, Q20 = B, Q21 = C, Q22 = A, Security extension = A, PBT extension = C, Operations phase = A.
|
||||
|
||||
**AI Response**: "Loaded the security-baseline extension rules (opted in). Validated all answers and detected one direct question from the user plus three genuine conflicts; opened a clarification round in `inception/requirements/requirement-clarification-questions.md` with 5 questions."
|
||||
|
||||
**Context**: INCEPTION — Requirements Analysis, Step 6 contradiction/ambiguity analysis.
|
||||
|
||||
### Extension configuration decided
|
||||
- Security Baseline: **Enabled** (blocking) — full rules file loaded.
|
||||
- Property-Based Testing: **Disabled** — rules file deliberately not loaded.
|
||||
- Operations Phase: **Yes**, decided at Requirements Analysis.
|
||||
|
||||
### Contradictions and ambiguities detected
|
||||
1. **Q1 answered about the wrong environment, plus a protocol misconception.** The user asked whether FileZilla connecting over SFTP means FTP can be the baseline. It does not: SFTP is file transfer inside an SSH session (proving SSH/SCP works, exactly the reference approach), whereas FTPS is a separate protocol needing its own server. More importantly, Q1 asked about **production** but the answer describes the Pi, which Q2 assigns to **test**. Commit `3885703` cites shared hosting (mijnhostingpartner.nl) as the reason for single-host serving, so where production actually runs is unresolved.
|
||||
2. **Q11 (single bundle) contradicts Q14/Q15/Q20 (build-time environment tagging).** Same-origin API URL is implementable, but Sentry's `environment` tag and Umami's per-environment website ID are Vite build-time values, so one `dist/` cannot serve both test and production — the very reason the reference project has a separate `build-production` job. Either two builds are accepted or the environment config must become runtime-delivered.
|
||||
3. **Q22 (`app_offline.htm`) is inapplicable to Q2's chosen test environment.** `app_offline.htm` is an IIS ASP.NET Core Module feature; on the Pi the app runs as a Kestrel process (nginx + systemd), where the file is ignored and an upload would overwrite DLLs of a live process. Also noted: any clean stop takes the public website in `wwwroot/web/` offline too, even though it is unrelated to the CMS deploy.
|
||||
4. **Blocking security rules expand scope beyond the workflow** (raised for an explicit decision rather than silently expanding or skipping): SECURITY-04 requires HTTP security headers, of which the app currently sets none — and because server configuration is explicitly unavailable, they must be app middleware; a CSP additionally constrains the externally-owned public website and needs explicit exceptions for Umami and Sentry. SECURITY-14 requires alerting plus 90-day log retention, which conflicts with Sentry's free-plan 30-day event retention chosen in Q16.
|
||||
|
||||
### Answers accepted without conflict (noted for the requirements document)
|
||||
- Q4 = C with the folder named **`web`**, not `site` — final `wwwroot` layout: `admin/`, `web/`, and optionally an API folder if needed. This resolves the `wwwroot` destruction risk structurally rather than by exclusion lists.
|
||||
- Q10 = C (auto-migrate `ApplicationDbContext` at startup) sits consistently with Q17 = A (liveness-only health check): if the database is unreachable, startup migration fails, so liveness implicitly covers database availability at boot.
|
||||
- Q3 = B means the toolchain is installed explicitly in the workflow (`actions/setup-dotnet`, `pnpm/action-setup`) rather than assumed present on the runner — which also satisfies SECURITY-10's pinned-tool-version requirement.
|
||||
|
||||
---
|
||||
|
||||
## Requirements Analysis — Clarification Answers Received
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "klaar, antwoorden staan in het bestand" — answers recorded in `inception/requirements/requirement-clarification-questions.md`: CQ1 = C, CQ2 = C, CQ3 = C, CQ4 = A, CQ5 = B.
|
||||
|
||||
**AI Response**: "All clarifications unambiguous; no further contradictions detected. Generated `inception/requirements/requirements.md` at comprehensive depth. Updated `aidlc-state.md` with Extension Configuration and Operations Configuration."
|
||||
|
||||
**Context**: INCEPTION — Requirements Analysis, Steps 7–8.
|
||||
|
||||
### Clarifications resolved
|
||||
- **CQ1 = C**: Production runs on the Pi for now, with shared hosting as the eventual target. Transport must be structured so FTPS can be added alongside SSH/SCP without restructuring the workflow. Resolves the Q1 environment mix-up; SSH/SCP is correct for both environments today.
|
||||
- **CQ2 = C**: Two builds now (reference pattern), with runtime-delivered environment configuration recorded as a possible later improvement. Same-origin API base URL is still adopted for its own sake.
|
||||
- **CQ3 = C**: Atomic release-directory switch instead of `app_offline.htm`. Supersedes Q22 = A, which was inapplicable to the Pi.
|
||||
- **CQ4 = A**: Both SECURITY-04 (security headers middleware) and SECURITY-14 (Sentry alert rules) are addressed within this feature, with log retention documented as a deliberate deviation.
|
||||
- **CQ5 = B**: CSP strict for `/admin` and `/api/v1`, more permissive for the public website.
|
||||
|
||||
### Derived requirement stated rather than asked (ASM-01)
|
||||
Combining Q4 = C (`wwwroot/web/`) with CQ3 = C (atomic release switch) creates a failure mode neither answer covers on its own: switching the release directory would discard the customer's website along with the old release. `wwwroot/web/` must therefore live outside the swapped release directory and be linked into it. This follows necessarily from the two decisions, so it is recorded as assumption ASM-01 with its rationale and consequence rather than opening a third question round — the user can reject it at approval.
|
||||
|
||||
### Artifact produced
|
||||
`inception/requirements/requirements.md` — 23 functional requirements, 10 non-functional requirements, 32 traced decisions (D-01…D-32), 7 assumptions, 4 open items, 4 documented security deviations, and a full SECURITY-01…15 compliance assessment.
|
||||
|
||||
### Security Compliance summary (Security Baseline extension, enabled and blocking)
|
||||
Addressed: SECURITY-01, 03, 04, 06, 09, 10, 11, 13, 14 (with deviation DEV-01). Pre-existing and unchanged: SECURITY-05, 08, 12, 15. N/A: SECURITY-02 (no load balancer, API gateway or CDN in this architecture). Partially N/A: SECURITY-07 (no cloud networking; applicable parts documented as host setup).
|
||||
|
||||
**No blocking security findings** — every rule is either addressed by a requirement, pre-existing and unchanged, marked N/A with rationale, or recorded as an explicitly accepted deviation (DEV-01…DEV-04, all pre-existing or cost-driven, none introduced by this feature).
|
||||
|
||||
Two security-relevant findings are logged as open items rather than silently dropped: OPEN-01 (correlation/request ID required by SECURITY-03 does not exist yet) and OPEN-02 (`AvailabilityMiddleware.IsAdminBypass` reads the JWT without validating its signature — pre-existing, needs an explicit owner).
|
||||
|
||||
---
|
||||
|
||||
## Requirements Analysis — Approved
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Continue to Next Stage"
|
||||
|
||||
**AI Response**: "Requirements approved (User Stories option offered and not taken). Marked complete in `aidlc-state.md` and proceeded to Workflow Planning."
|
||||
|
||||
**Context**: INCEPTION — Requirements Analysis approved, including assumption ASM-01 and the four documented security deviations.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Planning — Complete
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated planning step)"
|
||||
|
||||
**AI Response**: "Loaded all prior context (4 reverse-engineering artifacts, requirements.md, both question files with answers). Performed transformation-scope, change-impact, component-relationship and risk analysis. Created `inception/plans/execution-plan.md` with a validated Mermaid workflow diagram. Updated `aidlc-state.md` with the full stage matrix, execution plan summary and current status."
|
||||
|
||||
**Context**: INCEPTION — Workflow Planning, Steps 1–8.
|
||||
|
||||
### Risk assessment: HIGH
|
||||
Rated High rather than Medium because three failure modes are destructive **and** silent:
|
||||
1. An atomic release switch carrying `wwwroot/web/` inside the swapped directory would destroy the customer's public website on every deploy (ASM-01) — data loss, not a bug.
|
||||
2. Losing the Data Protection key ring makes stored slave API keys undecryptable; the symptom resembles a network fault and would be misdiagnosed.
|
||||
3. Automatic Core migrations at startup (FR-11) make deployment self-contained and simultaneously remove the human gate before a migration runs against production — which is why FR-20 (pre-deploy backup) and forward-compatible migrations are load-bearing rather than optional.
|
||||
Also: the deploy path cannot be fully tested in CI (needs the real Pi, SSH credentials and a database), and CSP failures only manifest in a real browser on pages this repository does not own.
|
||||
|
||||
Rollback complexity: Moderate. Testing complexity: Complex.
|
||||
|
||||
### Stage decisions
|
||||
- **User Stories — SKIP**: infrastructure/operations work with no new end-user functionality or persona; the website-builder "persona" is properly served by FR-09's documented contract. Offered at Requirements Analysis approval and not requested.
|
||||
- **Application Design — EXECUTE**: genuine component-boundary decisions — whether cross-cutting registrations (health checks, security headers, Data Protection) belong in `Core` (inherited by both hosts including the Slave) or in `Api`; redesigning static-file serving for two mounts and two fallbacks; a CSP path-scoping mechanism that does not exist yet; and a deploy-transport abstraction admitting FTPS later (NFR-09).
|
||||
- **Units Generation — EXECUTE**: 7 units with a load-bearing ordering constraint — durability changes must precede the first automated deploy, and quality-gate fixes must precede switching blocking gates on.
|
||||
- **Functional Design — EXECUTE units 2, 3, 4 only**: those contain real behavioural logic; units 1, 5, 6, 7 are lint/package changes, declarative YAML and documentation.
|
||||
- **NFR Requirements — SKIP all units**: already captured comprehensively with traceability in `requirements.md` § 5 (NFR-01…10) and § 6 (full SECURITY-01…15 assessment); the tech stack is fixed.
|
||||
- **NFR Design — EXECUTE units 3, 4 only**, recorded as a **deliberate deviation** from the workflow's default coupling (NFR Design skipped when NFR Requirements is skipped). Rationale: for these two units the NFR *is* the deliverable — CSP composition and path-scoping plus HSTS-behind-proxy behaviour (SECURITY-04), and structured-logging shape, correlation ID (OPEN-01), alertable-event definition and PII exclusion (SECURITY-03, SECURITY-14). Those are pattern decisions, not requirement decisions, so skipping requirements while designing patterns is the correct split rather than an oversight.
|
||||
- **Infrastructure Design — EXECUTE units 6, 7 only**: the host layout (release-directory scheme, where `wwwroot/web/` lives so ASM-01 holds, symlink/mount strategy, process restart, backup placement, transport abstraction) and the website contract that depends on it. Produces documented procedure rather than IaC, because NFR-01 forbids server configuration.
|
||||
- **Operations — all three stages EXECUTE**: Operations Configuration = Yes, and for this feature it is the centre of gravity (FR-17, FR-19, FR-20, FR-23), including the `dotnet-appsettings` compliance gate at Production Readiness Validation.
|
||||
|
||||
### Proposed unit sequence (finalised at Units Generation)
|
||||
1. Quality Gate Prerequisites (FR-21, FR-22) — first, so blocking gates are not red on arrival
|
||||
2. Hosting Layout & Data Durability (FR-07, FR-08, FR-10, FR-11, FR-12) — before any deploy exists
|
||||
3. HTTP Security Headers & CSP (FR-18)
|
||||
4. Observability Integration (FR-13, FR-14, FR-15, FR-16)
|
||||
5. CI Workflow (FR-01, FR-05)
|
||||
6. Deploy Workflow (FR-02, FR-03, FR-04, FR-06, FR-20)
|
||||
7. Repository Documentation (FR-09 + README/`.env.example` updates)
|
||||
|
||||
### Coordination note
|
||||
`SlpModularCms.Api.Slave` inherits every `Core` change and has no test project by design, so it is verified at Build and Test by actually starting it. The `/health` bypass in `Modules.Availability` must land in the same unit as the health endpoint, or `/health` returns 503 on a disabled instance — the exact conflation the user corrected during Reverse Engineering.
|
||||
|
||||
### Security Compliance (Security Baseline extension — enabled, blocking)
|
||||
No new blocking findings at this stage. Workflow Planning produces a plan, not code or configuration; the SECURITY-01…15 assessment recorded in `requirements.md` § 6 remains current and is carried forward unchanged. SECURITY-10 (pinned tool versions, no `latest` tags) and SECURITY-13 (access-controlled, auditable pipeline definitions) are explicitly reflected in the plan's Unit 5 and Unit 6 scope.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Planning — Approved
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Continue to Next Stage"
|
||||
|
||||
**AI Response**: "Execution plan approved, including the User Stories and NFR Requirements skips and the deliberate NFR Design coupling deviation. Proceeded to Application Design."
|
||||
|
||||
**Context**: INCEPTION — Workflow Planning approved. Next stage: Application Design.
|
||||
|
||||
---
|
||||
|
||||
## Application Design — Plan Created
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated planning step)"
|
||||
|
||||
**AI Response**: "Analyzed context and inspected both host `Program.cs` files to establish the composition baseline. Created `inception/plans/application-design-plan.md` with 8 design steps and 12 embedded design questions."
|
||||
|
||||
**Context**: INCEPTION — Application Design, Steps 1–6.
|
||||
|
||||
### Baseline established by inspection
|
||||
`SlpModularCms.Api/Program.cs` and `SlpModularCms.Api.Slave/Program.cs` are near-identical, differing only in the static-file and SPA-fallback section. This feature adds the same new registrations to both, which would grow that duplication — raised as Question 9 rather than silently choosing.
|
||||
|
||||
### Question areas
|
||||
1. Home project for the four new cross-cutting concerns (`Core` versus host) — consequential because `Core` is inherited by the Slave
|
||||
2. What the Slave host should and should not receive
|
||||
3. Static-file serving model for the two mounts
|
||||
4. Security-headers placement in the pipeline — noting that static files short-circuit, so headers registered after them never reach the public website
|
||||
5. Configurability surface for the security headers
|
||||
6. CSP path-scoping mechanism
|
||||
7. Which `DbContext` hosts the Data Protection keys
|
||||
8. Migration-at-startup failure behaviour, and its interaction with the liveness health check
|
||||
9. Whether to extract the duplicated host composition
|
||||
10. Sentry registration shape
|
||||
11. Deploy transport abstraction (workflow layer, not C#)
|
||||
12. OPEN-02 ownership — fold into this feature, defer to `tech-debt-backlog`, or accept as documented risk
|
||||
|
||||
---
|
||||
|
||||
## Application Design — Answers Received, Follow-up Round Opened
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "klaar, antwoorden staan in het bestand" — answers in `inception/plans/application-design-plan.md`: Q1 = A, Q2 = "A, Want de Slave is wel een API die laat zien hoe een klant-API eruit kan komen te zien.", Q3 = A, Q4 = C, Q5 = B, Q6 = B, Q7 = A, Q8 = A, Q9 = A, Q10 = B, Q11 = B, Q12 = A.
|
||||
|
||||
**AI Response**: "Analyzed all 12 answers per Step 8. Ten are unambiguous and mutually consistent. Two required follow-up, added to the plan as Part 3."
|
||||
|
||||
**Context**: INCEPTION — Application Design, Steps 7–9.
|
||||
|
||||
### Decisions accepted
|
||||
- **Q1 = A**: all four new cross-cutting concerns live in `Core` as extension methods — both hosts get identical behaviour, no duplication.
|
||||
- **Q2 = A**: the Slave receives everything except the static-file mounts. User rationale recorded: the Slave demonstrates what a customer-facing API instance looks like, so it should behave like production rather than like a stripped-down test harness. This elevates the Slave from "local dev tool" to "reference instance", which is a meaningful reframing.
|
||||
- **Q3 = A**: two explicit `UseStaticFiles` registrations, each with its own `PhysicalFileProvider` and `RequestPath`.
|
||||
- **Q7 = A**: Data Protection keys live in `ApplicationDbContext`, which will implement `IDataProtectionKeyContext`. Requires a new Core migration — acceptable because FR-11 now migrates that context automatically.
|
||||
- **Q8 = A**: fail fast on migration failure. Consistent with the liveness-only health check (D-21): a process that cannot migrate does not start, so `/health` stops answering and UptimeRobot goes red — the monitoring signal is meaningful precisely because of this choice.
|
||||
- **Q9 = A**: the two `Program.cs` files stay separate. With Q1 = A the shared logic is in `Core` extension methods, so what remains duplicated is the explicit list of calls — which is intentional readability, not accidental duplication.
|
||||
- **Q10 = B**: Sentry and logging configuration registered separately, so structured logging works without Sentry.
|
||||
- **Q11 = B**: one reusable workflow per transport (`deploy-scp.yaml`, later `deploy-ftps.yaml`) with an identical input interface.
|
||||
- **Q12 = A**: **OPEN-02 is folded into this feature.** `AvailabilityMiddleware.IsAdminBypass` will validate the token properly instead of reading it unvalidated. Lands in the same unit as the `/health` bypass, since both touch the same middleware. This closes a SECURITY-08 finding and requires a new functional requirement — to be added as FR-24 and OPEN-02 marked resolved.
|
||||
|
||||
### Follow-ups raised
|
||||
1. **Q4 = C would weaken security if implemented literally.** The user chose "headers only on SPA/HTML responses, not assets". That is correct for `Content-Security-Policy`, `X-Frame-Options` and `Referrer-Policy`, but wrong for two headers: `X-Content-Type-Options: nosniff` exists specifically to stop MIME-sniffing of **non-HTML** resources, so restricting it to HTML removes it exactly where it does its job; and `Strict-Transport-Security` is a host-level transport directive that a visitor whose first request is an asset would never receive. Follow-up offers per-header scoping as the recommended reading of the same intent.
|
||||
2. **Q5 = B and Q6 = B appear to conflict** (minimal configuration versus a configurable path-to-policy rule list). One reading makes them consistent: policy *definitions* in code (Q5), path-to-policy *assignment* and environment-specific origins in configuration (Q6). Presented with a concrete `appsettings` shape for confirmation rather than assumed.
|
||||
|
||||
---
|
||||
|
||||
## Application Design — Follow-up Answers and Artifacts Generated
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "klaar, antwoorden staan in het bestand" — follow-up answers in `inception/plans/application-design-plan.md` Part 3: FU1 = A, FU2 = A.
|
||||
|
||||
**AI Response**: "Both follow-ups resolved. Updated `requirements.md` (added FR-24, closed OPEN-02, upgraded SECURITY-08 to Improved, count now 24 FRs). Generated all five Application Design artifacts and marked every plan step complete."
|
||||
|
||||
**Context**: INCEPTION — Application Design, Steps 10–12.
|
||||
|
||||
### Follow-up resolutions
|
||||
- **FU1 = A**: per-header scoping. `X-Content-Type-Options` and `Strict-Transport-Security` apply to **all** responses; `Content-Security-Policy`, `X-Frame-Options` and `Referrer-Policy` to HTML responses only. This preserves the user's intent from Q4 = C while closing the gap that a literal reading would have created — `nosniff` exists precisely to protect non-HTML resources, so restricting it to HTML would have removed it where it does its job.
|
||||
- **FU2 = A**: confirmed the synthesis of Q5 = B and Q6 = B — policy *definitions* in code, path *assignment* and environment-specific *origins* in configuration. A misconfiguration can misroute a path but cannot invent a broken policy.
|
||||
|
||||
### Requirements updated
|
||||
- **FR-24 added**: validate the token in the availability gate's admin bypass (Q12 = A), replacing the unvalidated `ReadJwtToken` call.
|
||||
- **OPEN-02 closed**: folded into this feature, landing in the same unit as the `/health` bypass.
|
||||
- **SECURITY-08 upgraded** from "Pre-existing, unchanged" to "**Improved**".
|
||||
- Totals now: 24 FRs, 10 NFRs, 32 decisions, 7 assumptions, 3 remaining open items, 4 documented deviations.
|
||||
|
||||
### Artifacts generated
|
||||
`inception/application-design/`: `components.md`, `component-methods.md`, `services.md`, `component-dependency.md`, `application-design.md`.
|
||||
|
||||
**14 code components** (9 new, 5 modified) plus 2 workflow components. All in `Core` per Q1 = A except the static mounts, which are `Api`-only per Q2 = A.
|
||||
|
||||
### TWO CONFLICTS DISCOVERED DURING DESIGN
|
||||
Both found by tracing composition order; both would have produced code that looks correct while achieving nothing.
|
||||
|
||||
1. **Duplicate `AddDataProtection()` would silently defeat FR-12.** `AvailabilityModule.RegisterServices` and `MasterModule.RegisterServices` each call `services.AddDataProtection()`. Module registration runs *after* the host's, so those bare calls would override the persistent key store from `AddCmsDataProtection()`. FR-12 would appear implemented, registration tests would pass, and the key ring would still be ephemeral — so the first atomic release switch would silently break master↔slave trust in a way presenting as a network fault. Resolution: remove both module calls; host configures once. Assigned to Unit 2 with a test asserting the persistent store survives module registration.
|
||||
**Second-order finding**: the Data Protection **application discriminator must be set explicitly** — it defaults to deriving from the content root path, which changes on every atomic release-directory switch (FR-06), defeating FR-12 by a different route.
|
||||
2. **`AvailabilityMiddleware` runs before `UseAuthentication()`**, so `HttpContext.User` is unpopulated when the admin bypass is evaluated. FR-24 therefore has two viable implementations — validate the token in the middleware with shared `TokenValidationParameters`, or move authentication earlier (smaller change, wider blast radius across all modules). Recorded rather than decided; resolved in Functional Design for Unit 2.
|
||||
|
||||
### Additional design obligations recorded
|
||||
- `C-10` must tolerate a **missing `wwwroot/web/` at startup** — a fresh deployment has none until a website workspace deploys into it, and the CMS must still start and serve `/admin` and `/api/v1`.
|
||||
- An unknown CSP policy name must fail at **startup**, not degrade per request (SECURITY-15, fail closed).
|
||||
|
||||
### Security Compliance (Security Baseline extension — enabled, blocking)
|
||||
Addressed: SECURITY-01, 03, 04, 06, 09, 10, 11, 13, 14 (with DEV-01). **Improved: SECURITY-08** (FR-24 fixes a pre-existing finding). Unchanged: SECURITY-05, 12, 15. N/A: SECURITY-02. Partially N/A: SECURITY-07.
|
||||
|
||||
**No blocking security findings.**
|
||||
|
||||
---
|
||||
|
||||
## Application Design — Approved
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Continue to Next Stage"
|
||||
|
||||
**AI Response**: "Application Design approved, including FR-24, the two composition conflicts carried to Unit 2, and the additional design obligations. Proceeded to Units Generation Part 1 (Planning)."
|
||||
|
||||
**Context**: INCEPTION — Application Design approved. Next stage: Units Generation.
|
||||
|
||||
---
|
||||
|
||||
## Units Generation — Plan Created (Part 1)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated planning step)"
|
||||
|
||||
**AI Response**: "Created `inception/plans/unit-of-work-plan.md` with 6 decomposition steps and 8 questions covering unit boundaries, sequencing, completion criteria, version control and artifact mapping."
|
||||
|
||||
**Context**: INCEPTION — Units Generation, Part 1 Steps 1–5.
|
||||
|
||||
### Question areas
|
||||
1. Whether the proposed 7-unit split holds
|
||||
2. Whether Unit 1 (lint fixes, package pins) warrants its own unit
|
||||
3. Whether Unit 2 is too large — it bundles serving, health/gate and data-durability concerns whose only commonality is that all must land before the first deploy
|
||||
4. Sequencing: strictly serial versus grouping independent units into one approval round
|
||||
5. Per-unit definition of done, noting that Q9 of the requirements deliberately excluded a coverage gate
|
||||
6. Commit and review granularity on `feature/gitea-deployment-workflow` — nothing has been committed yet; all aidlc documentation is currently uncommitted working-tree changes
|
||||
7. What the mandatory `unit-of-work-story-map.md` maps to, given User Stories was skipped — proposed mapping the 24 FRs as the equivalent
|
||||
8. Whether documentation belongs in Construction Unit 7 or the Operations phase, given Operations already produces deployment, rollback, monitoring and readiness documents
|
||||
|
||||
---
|
||||
|
||||
## Units Generation — Answers Received and Artifacts Generated (Part 2)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "ik heb de vragen beantwoord" — answers in `inception/plans/unit-of-work-plan.md`: Q1 = C, Q2 = B, Q3 = B, Q4 = B, Q5 = B, Q6 = A, Q7 = A, Q8 = A.
|
||||
|
||||
**AI Response**: "Analyzed all answers per Step 7 — no contradictions or ambiguities found, so no follow-up round was needed. Executed the plan and generated all three unit artifacts. Marked every plan step complete and updated `aidlc-state.md`."
|
||||
|
||||
**Context**: INCEPTION — Units Generation, Part 1 Steps 6–11 and Part 2 Steps 12–15.
|
||||
|
||||
### Decomposition outcome
|
||||
Two changes to the proposed split that cancel out numerically — **still 7 units**, but with boundaries drawn along the work rather than along the deadline:
|
||||
- **Q3 = B**: the oversized Unit 2 split into **U1 Hosting & Serving** and **U2 Data Durability**. It had bundled serving, middleware and persistence work whose only commonality was "must land before the first deploy".
|
||||
- **Q2 = B**: the quality-gate prerequisites (5 lint fixes, 2 package pins) merged into the CI unit, so gates and their prerequisites land in one commit and the pipeline is never red on arrival.
|
||||
|
||||
Final units: U1 Hosting & Serving · U2 Data Durability · U3 Security Headers & CSP · U4 Observability · U5 CI Workflow & Gates · U6 Deploy Workflow · U7 Documentation.
|
||||
|
||||
### Execution rounds (Q4 = B)
|
||||
R1 = U1 + U2 (mutually independent) · R2 = U3 + U4 (tightly coupled — U3's CSP needs U4's origins) · R3 = U5 + U6 (one shared input interface) · R4 = U7. One commit per unit; single PR at the end (Q6 = A). A round is an approval boundary, not a commit boundary.
|
||||
|
||||
### Consequence of Q2 = B, recorded and mitigated
|
||||
Merging the lint fixes into U5 means `pnpm run lint` stays failing through U3 and U4, and U4 changes frontend files — so new violations would hide among the five pre-existing ones. Mitigation recorded in `unit-of-work-dependency.md`: run lint on **changed files** during U4. The file sets are disjoint (U5 fixes four component files; U4 touches `main.tsx`, `config.ts` and a new Umami component), so there is no merge risk — only a detection gap.
|
||||
|
||||
### Change to the execution plan's per-unit stage assignment
|
||||
The plan assigned Functional Design to "units 2, 3, 4" under the pre-split numbering. After the split, **both halves need it**: U1 for the FR-24 pipeline-ordering decision and the missing-`wwwroot/web/` startup behaviour, U2 for the registration-order conflict and migration failure semantics. Functional Design is therefore **×4 (U1–U4)** rather than ×3. Recorded in `unit-of-work-story-map.md`, which is authoritative over the execution plan's pre-split table, and reflected in `aidlc-state.md`.
|
||||
|
||||
### Coverage verification
|
||||
All 24 functional requirements assigned, none orphaned or duplicated. All 16 design components assigned. Two requirements deliberately split across a boundary: FR-08 (serving precondition in U1, deployment behaviour in U6) and FR-19 (event emission in U4, alert rules in Operations). Every carried-forward item — the two Application Design conflicts, OPEN-01, OPEN-03, ASM-01, and the two additional design obligations — has an owning unit and a resolving stage.
|
||||
|
||||
### Artifacts generated
|
||||
`inception/application-design/`: `unit-of-work.md`, `unit-of-work-dependency.md`, `unit-of-work-story-map.md`.
|
||||
|
||||
### Security Compliance (Security Baseline extension — enabled, blocking)
|
||||
No new blocking findings. This stage produces a decomposition, not code. Relevant to the assessment: U2's ordering constraint directly protects SECURITY-01 and SECURITY-13 (key-ring integrity across deploys), U5 carries SECURITY-10 (vulnerability gate, pinned tool versions), and U1 carries the SECURITY-08 improvement (FR-24). The `requirements.md` § 6 assessment remains current.
|
||||
|
||||
---
|
||||
|
||||
## Units Generation — Approved
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Continue to Next Stage"
|
||||
|
||||
**AI Response**: "Units approved. Entered the CONSTRUCTION PHASE at Round 1 (U1 Hosting & Serving + U2 Data Durability), starting with Functional Design for both units."
|
||||
|
||||
**Context**: INCEPTION complete. Transition to CONSTRUCTION PHASE.
|
||||
|
||||
---
|
||||
|
||||
## Functional Design — Plans Created for U1 and U2 (Round 1)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated planning step)"
|
||||
|
||||
**AI Response**: "Created functional design plans and dedicated question files for both Round 1 units, per the rule that plan files contain only checklists and questions live in separate files."
|
||||
|
||||
**Context**: CONSTRUCTION — Functional Design, Steps 1–5, for U1 and U2 together (Round 1 grouping per Q4 = B).
|
||||
|
||||
### Artifacts created
|
||||
- `construction/plans/u1-hosting-serving-functional-design-plan.md` — 6 steps
|
||||
- `construction/plans/u1-hosting-serving-fd-questions.md` — 5 questions
|
||||
- `construction/plans/u2-data-durability-functional-design-plan.md` — 6 steps
|
||||
- `construction/plans/u2-data-durability-fd-questions.md` — 5 questions
|
||||
|
||||
### U1 question areas
|
||||
1. **FR-24 implementation approach** — the § 5.2 conflict: validate the token in the middleware with shared `TokenValidationParameters`, versus moving `UseAuthentication()` before the module middleware
|
||||
2. Behaviour when `wwwroot/web/` is absent — 404, built-in placeholder, or redirect to `/admin`
|
||||
3. Whether a missing `wwwroot/web/` should be logged at startup, noting it is normal on a fresh install but would mean the customer's website has vanished on a running production environment
|
||||
4. `/health` response contract — framework default plain text, or JSON with more detail
|
||||
5. Whether `/admin` should redirect to `/admin/`, since the SPA is built with `base: '/admin/'` and relative references resolve one level too high without the trailing slash
|
||||
|
||||
### U2 question areas
|
||||
1. Application-discriminator source — the value that must stay stable across atomic release switches, since the default derives from the content root path
|
||||
2. **Key encryption at rest** — `PersistKeysToDbContext` stores keys unencrypted by default; DPAPI is unavailable on Linux, and X.509 encryption relocates the loss problem to the certificate. SECURITY-01 relevance flagged explicitly
|
||||
3. Startup behaviour when the database is temporarily unreachable, distinguishing "not up yet" from "migration is broken" — relevant because the app and SQL Server may start together on the Pi
|
||||
4. Concurrent migration by two starting instances — currently impossible (one instance per database) but worth an explicit assumption
|
||||
5. Key lifetime and rotation, including whether to document that the key table must never be pruned
|
||||
|
||||
---
|
||||
|
||||
## Functional Design — Answers Received and Artifacts Generated (U1 and U2, Round 1)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "klaar, antwoorden staan in het bestand" — U1: Q1 = A, Q2 = B, Q3 = A, Q4 = C, Q5 = A. U2: Q1 = A, Q2 = C, Q3 = C, Q4 = C, Q5 = C.
|
||||
|
||||
**AI Response**: "Analyzed all ten answers per Step 6. No contradictions or vague responses; no clarification round needed. Generated six functional design artifacts (three per unit). Recorded DEV-05 in `requirements.md` and documented one accepted disclosure trade-off. Marked all plan steps complete."
|
||||
|
||||
**Context**: CONSTRUCTION — Functional Design, Steps 6–8, for U1 and U2.
|
||||
|
||||
### U1 decisions
|
||||
- **Q1 = A — FR-24 approach resolved (§ 5.2 conflict closed)**: validate the token inside `AvailabilityMiddleware` using the same `TokenValidationParameters` as the JWT bearer scheme, resolved from a **single shared source** rather than copied. Chosen over moving `UseAuthentication()` earlier, which would have changed the pipeline for every module including future ones. A drift in which the gate is more permissive than the scheme would silently re-open the hole FR-24 closes, which is why the single-source constraint is a rule (BR-U1-11) rather than a note.
|
||||
- **Q2 = B**: a built-in placeholder page is served when `wwwroot/web/index.html` is absent. Stored as an **embedded resource**, not a file in `wwwroot/web/` — a file there would sit inside the directory a website workspace owns and overwrites, so it would either be deleted by the first real deployment or mistaken for part of the customer's site.
|
||||
- **Q3 = A**: a missing `wwwroot/web/` logs a warning at startup with the expected path. Normal on a fresh install, but on a running production instance it means the customer's website has vanished — a warning is visible in Sentry without blocking startup.
|
||||
- **Q4 = C**: `/health` returns JSON with status, timestamp, version and loaded module names. Directly serves NFR-06: `ModuleOrchestrator` logs rather than throws on module load failure, so an instance can start with reduced capability, and this is the only way to detect that after a deploy without host access.
|
||||
- **Q5 = A**: `/admin` redirects to `/admin/`, since the SPA is built with `base: '/admin/'` and relative references otherwise resolve one level too high.
|
||||
|
||||
### U2 decisions
|
||||
- **Q1 = A**: the application discriminator is a **fixed constant in code**, not configuration. The default derives from the content root path, which changes on every atomic release switch — persisting keys in the database while letting the discriminator move would produce keys that are stored but underivable. A constant cannot be forgotten during a host migration or accidentally differ between instances.
|
||||
- **Q2 = C**: keys stored unencrypted at rest for now, with certificate encryption as a separate follow-up. **Recorded as DEV-05** in `requirements.md` — the one deviation this feature introduces, accepted because it removes a far larger risk than it adds.
|
||||
- **Q3 = C**: failures are classified — connection failures retry with backoff, migration failures fail immediately. On the Pi the app and SQL Server may start together after a reboot, so a brief unavailability window is normal operation rather than a fault; a broken migration is a fault, and retrying it only delays the inevitable.
|
||||
- **Q4 = C**: concurrent migration handled by documentation rather than a distributed lock. One instance per database holds by design today; a lock would add failure modes without removing any. Recorded with the condition that automatic startup migration must be revisited **before** any move to multiple instances sharing a database.
|
||||
- **Q5 = C**: framework default 90-day rotation, plus an explicit documented rule that the keys table must **never** be pruned — the single most destructive maintenance action available against this system, because it looks like harmless housekeeping and permanently breaks every Master↔slave relationship.
|
||||
|
||||
### Accepted trade-off recorded rather than escalated
|
||||
U1 Q4 = C places the application **version** on an anonymous endpoint, which touches SECURITY-09 hardening. Judged acceptable and documented in `business-rules.md` under BR-U1-17 rather than opened as another question round, on two grounds: module names are **already** publicly exposed by the anonymous `/api/v1/System/capabilities`, so that field adds no new disclosure; and the version field is the primary reason the endpoint exists — confirming which build is live without host access — while an authenticated health endpoint would not work with UptimeRobot. Flagged to the user in the completion message so it can be reversed if unwanted.
|
||||
|
||||
### Artifacts generated
|
||||
- `construction/u1-hosting-serving/functional-design/`: `business-logic-model.md`, `business-rules.md`, `domain-entities.md` — 22 business rules (BR-U1-01…22)
|
||||
- `construction/u2-data-durability/functional-design/`: `business-logic-model.md`, `business-rules.md`, `domain-entities.md` — 18 business rules (BR-U2-01…18)
|
||||
|
||||
U1 adds no persisted entity, no migration and no configuration section. U2 adds one entity (the Data Protection keys table), one Core migration, and no configuration section.
|
||||
|
||||
### Security Compliance (Security Baseline extension — enabled, blocking)
|
||||
- **U1** — SECURITY-05 compliant, **SECURITY-08 improved** (forged-token bypass closed), SECURITY-09 compliant with the documented version disclosure, SECURITY-15 compliant (gate fails closed).
|
||||
- **U2** — SECURITY-01 **partially compliant, DEV-05** (in-transit enforced, at-rest deferred with compensating controls), SECURITY-03 compliant, SECURITY-09 compliant, **SECURITY-13 improved** (key-ring survival across redeploys is a software-integrity property), SECURITY-15 compliant.
|
||||
|
||||
**No blocking security findings.** DEV-05 is a newly introduced deviation, explicitly accepted with rationale and compensating controls.
|
||||
|
||||
---
|
||||
|
||||
## Functional Design — Approved (U1 and U2)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Continue to next stage"
|
||||
|
||||
**AI Response**: "Functional Design approved for both Round 1 units, including DEV-05 and the accepted version-disclosure trade-off on `/health`. Proceeded to Code Generation Part 1 (Planning). NFR Requirements, NFR Design and Infrastructure Design are skipped for U1 and U2 per the execution plan."
|
||||
|
||||
**Context**: CONSTRUCTION — Functional Design approved for U1 and U2.
|
||||
|
||||
---
|
||||
|
||||
## Code Generation — Plans Created for U1 and U2 (Part 1)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated planning step)"
|
||||
|
||||
**AI Response**: "Inspected the actual files to be modified (`ApplicationDbContext`, existing test conventions, both `Program.cs` files) and verified the required package version on nuget.org before writing the plans. Created `u1-hosting-serving-code-generation-plan.md` (13 steps) and `u2-data-durability-code-generation-plan.md` (11 steps)."
|
||||
|
||||
**Context**: CONSTRUCTION — Code Generation, Part 1 Steps 1–6, for U1 and U2.
|
||||
|
||||
### Approval prompt logged before asking
|
||||
"Code generation plans ready for U1 Hosting & Serving and U2 Data Durability. Review the plans at `construction/plans/u1-hosting-serving-code-generation-plan.md` and `construction/plans/u2-data-durability-code-generation-plan.md`. Approve the plans and proceed to Code Generation (Part 2)?"
|
||||
|
||||
### U1 plan — 13 steps
|
||||
9 files created, 7 modified. Key design decisions recorded in the plan:
|
||||
- **Step 1 extracts the JWT validation parameters into a shared factory** consumed by both `AddJwtBearer` and the new admin token validator. This implements the BR-U1-11 single-source constraint — two copies could drift, and a gate more permissive than the bearer scheme would silently re-open the hole FR-24 closes.
|
||||
- **Step 2 deliberately exposes no options parameter** on `AddCmsHealthChecks()`, so adding a database check later is a visible code change rather than configuration drift — keeping D-21 (liveness only) enforced by shape.
|
||||
- **Step 5 embeds the placeholder page as an assembly resource** rather than placing it in `wwwroot/web/`, because that directory is owned and overwritten by a website workspace.
|
||||
- **Step 10 modifies two existing test files** for the middleware constructor change, and adds cases asserting that a forged Owner token grants no bypass while a valid one still does.
|
||||
|
||||
### U2 plan — 11 steps
|
||||
5 files created, 6 modified, 1 EF migration. Verified `Microsoft.AspNetCore.DataProtection.EntityFrameworkCore` **10.0.9** exists on nuget.org and matches the existing 10.0.x line.
|
||||
- **Step 5 removes `AddDataProtection()` from both modules** — the § 5.1 conflict. Recorded in the plan with its rationale, because the removal looks like a regression to a reviewer who does not know the ordering issue.
|
||||
- **Step 8's first assertion is that the persistent key store survives module registration**, explicitly flagged as the highest-value test in the unit: a test that merely asserts "Data Protection is registered" passes in both the broken and fixed cases.
|
||||
- A **Risk Notes for the Executor** section lists five ways this unit could be implemented in a way that looks correct while remaining ineffective, each paired with the step that prevents it.
|
||||
|
||||
---
|
||||
|
||||
## Code Generation — Plans Approved (U1 and U2)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Approve Plan"
|
||||
|
||||
**AI Response**: "Both code generation plans approved. Proceeded to Part 2 (Generation)."
|
||||
|
||||
**Context**: CONSTRUCTION — Code Generation Part 1 approved for U1 and U2.
|
||||
|
||||
---
|
||||
|
||||
## Code Generation — Complete (U1 and U2, Round 1)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — plan execution)"
|
||||
|
||||
**AI Response**: "Executed both plans in order. 16 files created, 13 modified, 1 EF migration generated. Build clean and all tests green on the first run; nothing needed fixing and retrying. Recorded one plan deviation."
|
||||
|
||||
**Context**: CONSTRUCTION — Code Generation, Part 2 Steps 10–14, for U1 and U2.
|
||||
|
||||
### Verification (Step 13.5)
|
||||
- `dotnet build SlpModularCms.sln -c Release` — **0 errors**, 70 warnings (all pre-existing NU1903/NU1510; the two vulnerable packages are U5's scope)
|
||||
- `dotnet test SlpModularCms.sln -c Release` — **253 passed, 0 failed, 0 skipped**. Baseline was 219, so 34 tests added. Per project: Core 83 (was 54), Availability 82 (was 78), Identity 37 (unchanged), Master 51 (was 50)
|
||||
- `dotnet ef migrations add AddDataProtectionKeys` — generated `20260727203036_AddDataProtectionKeys`; inspected and confirmed **purely additive** (creates one table, drops nothing), so rollback by redeploying an earlier release stays safe per BR-U2-16
|
||||
- **Embedded resource name verified against the compiled assembly manifest** — `SlpModularCms.Api.Extensions.WebsitePlaceholder.html`. A wrong name would have failed *silently*, falling back to a minimal inline HTML string, so this was checked rather than assumed
|
||||
|
||||
No build or test failure occurred during generation.
|
||||
|
||||
### Implementation decisions worth recording
|
||||
- **The forged-token fix is proven against the real validator, not only a substitute.** The middleware's own tests substitute `IAdminTokenValidator`, which is correct unit-testing practice — but a substitute keeps passing even if the middleware were later rewired back to unvalidated token parsing. A nested `WithRealValidator` test class therefore wires the middleware to the actual `AdminTokenValidator` and asserts both halves: a forged unsigned Owner token is rejected, **and** a genuine Owner token still bypasses (an administrator must always be able to reach a disabled instance).
|
||||
- **U2's tests assert resulting configuration, not registration.** A test asserting "Data Protection is registered" passes in both the broken and fixed cases because `IDataProtector` resolves either way. The tests instead inspect `KeyManagementOptions.XmlRepository` (must be the EF repository, not the filesystem default) and `DataProtectionOptions.ApplicationDiscriminator` (must be the fixed constant), plus a round-trip proving a value encrypted before a deploy is readable after one.
|
||||
- **The module-level "does not override" tests live in each module's own test project**, because `Core.Tests` does not reference the modules. Each registers the host's Data Protection first and the module second — the real ordering.
|
||||
- **`SqlException` is produced genuinely rather than faked.** It has no public constructor, so the test provokes a real one against an unreachable host with a one-second timeout, exercising the classifier against the exact type it will meet in production.
|
||||
- **Both `AddDataProtection()` removal sites carry an explanatory comment**, because deleting them looks like a regression to anyone unaware of the ordering issue.
|
||||
|
||||
### DEVIATION FROM PLAN
|
||||
**U1 plan Step 11 (`StaticContentTests`) was not implemented as written.** The plan placed it in `SlpModularCms.Core.Tests`, but `StaticContentExtensions` lives in `SlpModularCms.Api`, which `Core.Tests` does not reference and must not. `SlpModularCms.Api` has no test project, by the same deliberate convention that gives `SlpModularCms.Api.Slave` none — the Clients solution folder holds deployables, not tested libraries. Creating one would have been a structural change outside this unit's scope.
|
||||
|
||||
The behaviour the step was meant to cover is largely ASP.NET Core's own static-file behaviour rather than this project's logic. The genuinely project-specific parts are **carried to the phase-level Build and Test stage**, where both hosts are started: `/admin` trailing-slash redirect, 404-vs-HTML for missing assets under either mount, SPA fallback resolution for both mounts, placeholder substitution when the website index is absent, and `/health` answering while the instance is availability-disabled.
|
||||
|
||||
Recorded in the plan file (Step 11 marked `[~]` with the reason) and in `construction/u1-hosting-serving/code/generation-summary.md`.
|
||||
|
||||
### Also carried to phase-level Build and Test
|
||||
`MigrateCoreDatabase()` against a real SQL Server, and both hosts actually starting. Neither is verifiable without a database.
|
||||
|
||||
### Behaviour note recorded for Operations
|
||||
Static mounts are resolved **at startup**: a directory created after the process started is not served until the next restart. Correct for the intended deployment model — the atomic release switch links `wwwroot/web/` into place before the process starts — but it means dropping a website into a running instance requires a restart.
|
||||
|
||||
### Security Compliance (Security Baseline extension — enabled, blocking)
|
||||
- **SECURITY-08 — improved and now verified by test.** The forged-token bypass is closed; `AdminTokenValidatorTests` covers forged-unsigned, wrong-key, expired, wrong-issuer, non-admin, malformed and absent tokens.
|
||||
- **SECURITY-13 — improved.** The key ring now survives redeploys, asserted by a round-trip test.
|
||||
- **SECURITY-03 — compliant.** Migration failure logging carries diagnostic context but no connection string or credentials.
|
||||
- **SECURITY-15 — compliant.** The gate fails closed; startup fails closed.
|
||||
- **SECURITY-01 — partially compliant, DEV-05** (keys unencrypted at rest, compensating controls documented).
|
||||
|
||||
**No blocking security findings.**
|
||||
|
||||
---
|
||||
|
||||
## Code Generation — Approved (U1 and U2, Round 1)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "Continue to Next Stage"
|
||||
|
||||
**AI Response**: "Round 1 code approved, including the U1 Step 11 deviation and the items carried to the phase-level Build and Test stage. Performed the mandatory phase transition check: updated this feature's status in `aidlc-docs/active-features.md` from 🔵 Inception to 🟢 Construction, and broadened its Affected Components to include Core and Modules. Proceeded to Round 2 (U3 Security Headers & CSP + U4 Observability), starting with Functional Design for both units."
|
||||
|
||||
**Context**: CONSTRUCTION — Code Generation approved for U1 and U2. Phase transition applied.
|
||||
|
||||
---
|
||||
|
||||
## Functional Design — Plans Created for U3 and U4 (Round 2)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "(no user input — automated planning step)"
|
||||
|
||||
**AI Response**: "Created functional design plans and dedicated question files for both Round 2 units. Each plan states an explicit scope boundary against the NFR Design stage that follows for these two units, so behaviour questions and pattern questions are not duplicated."
|
||||
|
||||
**Context**: CONSTRUCTION — Functional Design, Steps 1–5, for U3 and U4 together (Round 2 grouping per Q4 = B).
|
||||
|
||||
### Artifacts created
|
||||
- `construction/plans/u3-security-headers-functional-design-plan.md` — 6 steps
|
||||
- `construction/plans/u3-security-headers-fd-questions.md` — 5 questions
|
||||
- `construction/plans/u4-observability-functional-design-plan.md` — 7 steps
|
||||
- `construction/plans/u4-observability-fd-questions.md` — 5 questions
|
||||
|
||||
### U3 question areas
|
||||
1. **A genuinely strict CSP would break the admin SPA.** Radix UI components position dialogs and dropdowns using inline `style` attributes, which `style-src 'self'` blocks without `'unsafe-inline'`. The failure is silent in the console sense — misplaced or invisible menus, visible only in a browser and only where the user clicks. Options offered: documented `'unsafe-inline'` for styles while keeping `script-src` strict, nonces/hashes (impractical because Radix generates styles per interaction), or report-only first.
|
||||
2. What "relaxed" concretely permits for the public website, given the same process also serves the admin UI and API.
|
||||
3. Whether headers apply in Development — flagged that HSTS on `localhost` is remembered per host by browsers and can interfere with unrelated local projects, while an active CSP locally is genuinely useful.
|
||||
4. `X-Frame-Options: DENY` as specified in FR-18 also applies to the public website, so a customer embedding their own page in an iframe would break. Offered per-path scoping.
|
||||
5. Behaviour when the Umami/Sentry origin lists are empty — including the case where a DSN or website ID is configured but the matching CSP origin is not, which would silently block monitoring traffic.
|
||||
|
||||
### U4 question areas
|
||||
1. **Sentry transport: tunnel versus direct.** The reference project tunnels through nginx because ad blockers return `ERR_BLOCKED_BY_CLIENT` for Sentry domains — meaning errors are lost precisely for ad-blocker users. Here nginx is unavailable (NFR-01), so a tunnel would have to be an app endpoint. Noted the side benefit: a tunnel keeps traffic same-origin, so U3's CSP needs no external Sentry origin.
|
||||
2. **`SendDefaultPii`** — flagged that enabling it sends request headers **including cookies**, and this application carries the `refreshToken` in a cookie. SECURITY-03 forbids logging secrets and PII.
|
||||
3. Production log level — noted that the current `Warning` baseline means a normal production run logs almost nothing, including startup, module discovery and migration outcome, which are the three most useful things after a deploy.
|
||||
4. Which security events to emit for alerting (multi-select), including the availability-gate rejection that only became meaningful after the FR-24 fix.
|
||||
5. Umami on the admin SPA — always, or respecting `Do Not Track`.
|
||||
|
||||
---
|
||||
|
||||
## Functional Design — U3 Answers Received, U4 Answers Missing (Round 2)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "klaar, antwoorden staan in het bestand" — U3 answers: Q1 = B, Q2 = B, Q3 = A, Q4 = A, Q5 = C. **U4: all five `[Answer]:` tags are empty.**
|
||||
|
||||
**AI Response**: "Cannot proceed to artifact generation. Two blockers reported: (1) `u4-observability-fd-questions.md` has no answers filled in at all; (2) U3 Q1 = B is technically unworkable and I described that option too mildly in the first place — correction and a follow-up question added to the U3 file, along with a confirmation question about the SECURITY-04 consequence of Q2 = B."
|
||||
|
||||
**Context**: CONSTRUCTION — Functional Design, Step 6 (answer analysis) for U3 and U4. Gate not passed.
|
||||
|
||||
### Blocker 1 — U4 unanswered
|
||||
All five questions in `construction/plans/u4-observability-fd-questions.md` still have empty `[Answer]:` tags. Per the workflow's answer-collection rule, generation cannot start until they are completed.
|
||||
|
||||
### Blocker 2 — U3 Q1 = B is not implementable, and my option description was at fault
|
||||
The user chose "nonces or hashes for styles". I had described this as "theoretically neater but impractical because Radix generates styles per interaction", which understated the problem: **CSP nonces apply only to `<style>` and `<script>` elements, never to inline `style` attributes.** That is specification behaviour, not an implementation limitation. Radix positions dropdowns and dialogs via `style="transform: translate(...)"` attributes whose values change per click, viewport and scroll position.
|
||||
|
||||
The only CSP mechanisms that can permit inline style attributes are `'unsafe-inline'` (permits all) or `'unsafe-hashes'` with a hash per exact attribute value (no finite set exists here). There is therefore no variant of option B that leaves the admin UI working.
|
||||
|
||||
Follow-up Question 1 added to the U3 file with the corrected explanation and the genuinely available options: `'unsafe-inline'` for styles while keeping `script-src` strict, report-only first, or replacing Radix (an admin-UI rebuild, far outside this feature's scope).
|
||||
|
||||
### Consequence of U3 Q2 = B recorded for confirmation rather than assumed
|
||||
Q2 = B means the public website receives the protective headers but **no CSP at all**. This is a defensible choice consistent with D-31, but SECURITY-04 requires a CSP on all HTML-serving endpoints, so it is a deviation — one that would need recording as DEV-06 with its rationale. Follow-up Question 2 offers three ways to settle it: accept the deviation with the website contract making output escaping the builder's responsibility, a report-only CSP on the website, or an enforcing relaxed CSP that satisfies SECURITY-04 outright.
|
||||
|
||||
Note this is a **blocking** consideration under the enabled Security Baseline extension, so it must be resolved explicitly before U3's design artifacts are generated.
|
||||
|
||||
### U3 answers accepted without issue
|
||||
- **Q3 = A**: CSP and the other headers apply in Development; HSTS only outside it — avoids polluting `localhost` while surfacing CSP violations early.
|
||||
- **Q4 = A**: `X-Frame-Options: DENY` for `/admin` and `/api/v1`, `SAMEORIGIN` for the public website. Refines FR-18, which specified `DENY` globally without considering that a customer might embed their own pages.
|
||||
- **Q5 = C**: warn at startup when a Sentry DSN or Umami website ID is configured but the matching CSP origin is not — catching precisely the case where monitoring appears configured but silently receives nothing.
|
||||
|
||||
---
|
||||
|
||||
## Functional Design — Complete (U3 and U4, Round 2)
|
||||
|
||||
**Timestamp**: 2026-07-27T00:00:00Z
|
||||
|
||||
**User Input**: "klaar, U4 en de vervolgvragen staan nu ook ingevuld" — U3: Q1 → FU1 = A, Q2 → FU2 = C, Q3 = A, Q4 = A, Q5 = C. U4: Q1 = A, Q2 = B, Q3 = C, Q4 = A+B+C+D+E+F, Q5 = A.
|
||||
|
||||
**AI Response**: "All answers received and analysed. Generated seven functional design artifacts (three for U3, four for U4 including `frontend-components.md`). Three technical consequences recorded as design decisions rather than opened as a further question round. No new deviation needed — SECURITY-04 is now satisfied outright."
|
||||
|
||||
**Context**: CONSTRUCTION — Functional Design, Steps 6–8, for U3 and U4.
|
||||
|
||||
### U3 decisions
|
||||
- **FU1 = A**: `style-src 'self' 'unsafe-inline'` under `Strict`, bounded to styles. `script-src 'self'` stays closed with no `'unsafe-inline'` and no `'unsafe-eval'` — recorded as BR-U3-13, a rule rather than a default, because relaxing it would collapse the value of the whole policy.
|
||||
- **FU2 = C — supersedes Q2 = B.** The public website receives an **enforcing** relaxed CSP rather than none, so SECURITY-04 is satisfied on every HTML-serving path and **DEV-06 is not needed**. "Relaxed" still blocks external script origins, so it remains a genuine boundary rather than a rubber stamp.
|
||||
- **Q3 = A**: every header applies in Development except HSTS, which browsers remember per host and would otherwise pollute `localhost`.
|
||||
- **Q4 = A**: `DENY` / `frame-ancestors 'none'` under `Strict`; `SAMEORIGIN` / `'self'` under `Relaxed`. Refines FR-18, which specified `DENY` globally without accounting for a customer embedding their own pages.
|
||||
- **Q5 = C**: startup warning when a Umami website ID is configured but its origin is absent from the CSP.
|
||||
|
||||
### U4 decisions
|
||||
- **Q1 = A**: Sentry tunnel through the API. Ad blockers block Sentry domains, losing errors precisely for ad-blocker users. Constrained by BR-U4-16: the tunnel forwards **only** to the host derived from the configured DSN — a caller-supplied destination would make an anonymous endpoint into a server-side request forgery primitive.
|
||||
- **Q2 = B**: `SendDefaultPii` enabled with scrubbing.
|
||||
- **Q3 = C**: `Information` for the console, framework included.
|
||||
- **Q4 = all six event types**, all emitted at `Warning` or above so they cross the Sentry event threshold by construction.
|
||||
- **Q5 = A**: Umami in test and production, never locally, `Do Not Track` deliberately not consulted.
|
||||
|
||||
### THREE CONSEQUENCES RECORDED AS DESIGN DECISIONS RATHER THAN A FURTHER QUESTION ROUND
|
||||
|
||||
1. **The scrub list is longer than Q2 = B implied.** Enabling `SendDefaultPii` attaches request headers, and this application carries **two** standing credentials in headers: the `refreshToken` cookie and `X-Master-Api-Key`. The API key was not mentioned when the option was chosen, but it is the same class of secret and would otherwise be sent to a third party on every error raised during a master/slave call. BR-U4-13 therefore removes the entire `Cookie` header, `Authorization`, `X-Master-Api-Key` and the request body. Removing the whole `Cookie` header rather than surgically deleting one cookie, because rewriting a header is error-prone.
|
||||
|
||||
2. **Console and Sentry thresholds had to be split.** Q3 = C (`Information` everywhere) taken literally together with Q16 = C (structured logging to Sentry beyond exceptions) would send an event per request, exhausting Sentry's free plan within hours and burying real errors in request noise. BR-U4-06 and BR-U4-07 split them: console `Information` (honouring Q3 = C), Sentry **events** at `Warning` and above (still more than exceptions, honouring Q16 = C), Sentry **breadcrumbs** at `Information` so every event arrives with the trail that led to it. This satisfies both answers rather than choosing between them.
|
||||
|
||||
3. **The Q5 = C startup warning covers Umami only, not Sentry.** The question anticipated needing a Sentry ingest origin in `connect-src`, but Q1 = A's tunnel keeps browser reporting same-origin, so `connect-src 'self'` already covers it and there is no Sentry origin to forget. Umami's script still loads from its own origin, so that check remains meaningful.
|
||||
|
||||
### Additional decisions taken and documented
|
||||
- The **placeholder page** (U1) receives the relaxed policy; its inline `<style>` block is permitted. Flagged because it is the one HTML document this repository serves at the website path, so tightening the relaxed policy would break it on a **fresh installation** — the worst moment for a confusing failure.
|
||||
- **Frontend `sendDefaultPii` is false** even though the backend enables it with scrubbing: the backend can scrub in-process before transmission, the browser cannot, and the frontend has nothing to add that the backend cannot already report.
|
||||
- **Handled `ProblemDetailsError` instances must not become Sentry events**, or every user validation failure becomes an alert. Only unhandled errors and `NetworkError` are reported.
|
||||
- **Invitation tokens travel in a query string** on one endpoint and query strings are retained. Single-use and time-limited rather than a standing credential; documented so the trade-off is visible rather than accidental.
|
||||
|
||||
### Artifacts generated
|
||||
- `construction/u3-security-headers/functional-design/`: `business-logic-model.md`, `business-rules.md`, `domain-entities.md` — 24 rules (BR-U3-01…24)
|
||||
- `construction/u4-observability/functional-design/`: `business-logic-model.md`, `business-rules.md`, `domain-entities.md`, `frontend-components.md` — 29 rules (BR-U4-01…29)
|
||||
|
||||
### Security Compliance (Security Baseline extension — enabled, blocking)
|
||||
- **SECURITY-04 — compliant, no deviation.** All five headers, HSTS one year with `includeSubDomains`, and a CSP on every HTML-serving path including the public website. `'unsafe-inline'` documented per policy.
|
||||
- **SECURITY-03 — compliant.** Correlation ID on every entry; credentials excluded by BR-U4-05 and BR-U4-13.
|
||||
- **SECURITY-11 — improved.** CSP is a genuine second layer; the tunnel's fixed destination prevents request forgery.
|
||||
- **SECURITY-14 — addressed, DEV-01 unchanged** (retention shortfall already accepted).
|
||||
- **SECURITY-15 — compliant.** Fails closed at startup on an unknown policy; observability failures never reach request handling.
|
||||
|
||||
**No blocking security findings. No new deviation** — FU2 = C removed the need for the DEV-06 that Q2 = B would have required.
|
||||
|
||||
---
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# Functional Design Questions — U3 HTTP Security Headers & CSP
|
||||
|
||||
Vul je keuze in achter elke `[Answer]:`-tag. Kies de laatste optie (`Anders`) als niets past.
|
||||
|
||||
---
|
||||
|
||||
## Question 1 — De admin-SPA gaat kapot van een echt strikte CSP
|
||||
|
||||
**Context**: de admin-SPA gebruikt Radix UI-componenten (dialogen, dropdowns, selects) via shadcn-stijl wrappers. Die plaatsen positionering als **inline `style`-attributen** op elementen — dat is hoe een dropdown weet waar hij moet staan.
|
||||
|
||||
Een CSP met `style-src 'self'` en zonder `'unsafe-inline'` blokkeert die inline styles. Het gevolg is niet een foutmelding maar verkeerd gepositioneerde of onzichtbare menu's en dialogen: kapot op een manier die je pas in de browser ziet, en pas op de plekken waar je klikt.
|
||||
|
||||
Er zijn drie manieren om hiermee om te gaan:
|
||||
|
||||
A) `style-src 'self' 'unsafe-inline'` in de strikte policy, met een gedocumenteerde onderbouwing waarom die uitzondering nodig is — pragmatisch en meteen werkend. `script-src` blijft wél strikt, en dat is waar XSS-risico echt zit
|
||||
B) Nonces of hashes gebruiken voor styles — theoretisch netter, maar Radix genereert styles op runtime per interactie, dus dit werkt in de praktijk niet zonder de componenten te herschrijven
|
||||
C) Begin met `Content-Security-Policy-Report-Only` voor `/admin`, kijk wat er daadwerkelijk overtreden wordt, en zet hem daarna afdwingend — niets breekt stil, maar de bescherming staat er in de tussentijd niet
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
## Question 2 — Wat mag de publieke website?
|
||||
|
||||
**Context**: je koos een ruimere CSP voor de publieke website (D-31), omdat een website-bouwer niet beperkt moet worden door een policy die hij nooit gezien heeft. Maar "ruimer" moet nog wel iets betekenen — de website wordt door hetzelfde proces geserveerd als de admin-UI en de API.
|
||||
|
||||
A) Ruim maar niet leeg: sta scripts, styles, afbeeldingen en fonts van eigen origin plus inline toe, en verbindingen naar eigen origin plus de geconfigureerde origins. Externe scripts (bijv. een YouTube-embed) worden dan geblokkeerd tenzij toegevoegd
|
||||
B) Alleen de echt beschermende headers voor de website (`X-Content-Type-Options`, HSTS, `Referrer-Policy`) en **geen** CSP — dan kan een website-bouwer nooit verrast worden
|
||||
C) Ruim, plus in het website-contract vastleggen dat een bouwer extra origins kan laten toevoegen aan de configuratie als hij externe bronnen nodig heeft
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:B
|
||||
|
||||
---
|
||||
|
||||
## Question 3 — Gelden de headers ook lokaal?
|
||||
|
||||
**Context**: `Strict-Transport-Security` vertelt de browser: gebruik voor dit domein voortaan altijd HTTPS. Browsers onthouden dat **per host, langdurig**. Stuur je die header op `localhost`, dan kan dat je lokale ontwikkeling van andere projecten op dezelfde `localhost` gaan dwarszitten, en het is niet triviaal om weer ongedaan te maken.
|
||||
|
||||
De CSP lokaal wél actief hebben is juist nuttig: dan merk je een overtreding tijdens ontwikkelen in plaats van in productie.
|
||||
|
||||
A) CSP en de overige headers ook in Development; **HSTS alleen buiten Development** — je ziet CSP-problemen vroeg, zonder je `localhost` te vervuilen
|
||||
B) Alle headers in alle omgevingen, inclusief HSTS lokaal
|
||||
C) Geen enkele header in Development — lokaal zo weinig ruis als mogelijk
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
---
|
||||
|
||||
## Question 4 — Mag `/admin` in een iframe?
|
||||
|
||||
**Context**: je koos `X-Frame-Options: DENY` in de requirements (FR-18). Dat betekent dat geen enkele pagina van dit domein in een iframe geplaatst mag worden — ook niet door de site zelf.
|
||||
|
||||
Dat is voor de admin-UI precies goed. Maar het geldt dan ook voor de publieke website, en een klant die op zijn eigen site een pagina in een iframe toont (bijvoorbeeld een formulier of een kaart in een eigen iframe), loopt daar tegenaan.
|
||||
|
||||
A) `DENY` voor `/admin` en `/api/v1`, `SAMEORIGIN` voor de publieke website — de admin-UI blijft maximaal beschermd, de website kan zijn eigen pagina's insluiten
|
||||
B) `DENY` overal, zoals in de requirements — en een klant die iframes nodig heeft, komt dan bij jou terecht
|
||||
C) `SAMEORIGIN` overal — eenvoudiger, iets minder strikt voor de admin-UI
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
|
||||
---
|
||||
|
||||
## Question 5 — Wat als er geen origins geconfigureerd zijn?
|
||||
|
||||
**Context**: de Umami- en Sentry-origins komen uit configuratie en verschillen per omgeving. In een omgeving zonder Sentry of Umami — bijvoorbeeld lokaal, of een instantie zonder analytics — zijn die lijsten leeg.
|
||||
|
||||
Een strikte CSP zonder die origins is correcter (minder toegestaan), maar als iemand later Sentry aanzet en de origin vergeet toe te voegen, worden de foutmeldingen stil geblokkeerd: je monitoring lijkt dan te werken maar ontvangt niets.
|
||||
|
||||
A) Lege lijsten zijn normaal — de policy wordt dan simpelweg strikter. Bij opstarten een informatieregel loggen welke origins actief zijn, zodat je in het log kunt zien wat is toegestaan
|
||||
B) Lege lijsten zijn normaal, en géén logging — het is een gewone toestand
|
||||
C) Bij opstarten een waarschuwing als er een Sentry-DSN of Umami-website-ID is geconfigureerd maar de bijbehorende origin niet in de CSP staat — vangt precies de fout waarbij monitoring stil niets ontvangt
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
---
|
||||
|
||||
# Vervolgvragen (ronde 2)
|
||||
|
||||
---
|
||||
|
||||
## Follow-up Question 1 — Optie B bij vraag 1 kan technisch niet
|
||||
|
||||
Je koos bij vraag 1 optie B: nonces of hashes voor styles. Ik moet dat terugleggen, want ik heb die optie te mild beschreven — hij is niet "theoretisch netter maar lastig", hij **werkt principieel niet** voor dit probleem.
|
||||
|
||||
**Waarom niet:** een CSP-nonce werkt alleen op `<style>`- en `<script>`-**elementen**. Inline `style="..."`-**attributen** vallen daar volledig buiten; een nonce kan er niet aan gehangen worden. Dat is geen implementatiedetail maar hoe de specificatie werkt.
|
||||
|
||||
En juist die attributen zijn wat Radix gebruikt: een dropdown die opengaat krijgt zijn positie als `style="transform: translate(...)"` op het element. Die waarde verandht per klik, per schermgrootte en per scrollpositie.
|
||||
|
||||
De enige CSP-mechanismen die inline `style`-attributen kunnen toestaan zijn:
|
||||
- `'unsafe-inline'` — staat ze allemaal toe
|
||||
- `'unsafe-hashes'` met een hash per exacte attribuutwaarde — en die waarden zijn hier dynamisch, dus er is geen eindige verzameling om te hashen
|
||||
|
||||
Er is dus geen variant van B die de admin-UI heel laat. Wat overblijft:
|
||||
|
||||
A) `style-src 'self' 'unsafe-inline'` in de strikte policy, met gedocumenteerde onderbouwing. `script-src` blijft strikt zonder `unsafe-inline` en zonder `unsafe-eval` — en daar zit het werkelijke XSS-risico. Dit is wat vrijwel elke applicatie met een component-bibliotheek doet
|
||||
C) Beginnen met `Content-Security-Policy-Report-Only` voor `/admin`, kijken wat er in de praktijk overtreden wordt, en daarna afdwingend zetten met de dan blijkbaar benodigde uitzonderingen. Niets breekt stil, maar de CSP beschermt `/admin` in de tussentijd niet
|
||||
D) Radix vervangen door componenten zonder inline styles — een herbouw van de hele admin-UI, ver buiten de scope van deze feature
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## Follow-up Question 2 — Bevestiging: geen CSP op de publieke website is een afwijking
|
||||
|
||||
Bij vraag 2 koos je B: alleen de beschermende headers voor de publieke website en **geen** CSP. Dat is een verdedigbare keuze en past bij je uitgangspunt dat een website-bouwer niet mag struikelen over een policy die hij nooit gezien heeft.
|
||||
|
||||
Wel moet ik het expliciet vastleggen, want SECURITY-04 vraagt een CSP op **alle** HTML-servende endpoints. Zonder CSP op de website betekent dat: als daar ooit een XSS-gat zit, is er geen tweede verdedigingslaag die het beperkt. `/admin` en `/api/v1` houden hun strikte policy, dus het beheerdeel blijft wel beschermd.
|
||||
|
||||
Hoe wil je dit vastleggen?
|
||||
|
||||
A) Als gedocumenteerde afwijking (DEV-06) met deze onderbouwing, en in het website-contract opnemen dat een bouwer zelf verantwoordelijk is voor uitvoer-escaping op zijn site
|
||||
B) Toch een CSP op de website, maar alleen rapporterend (`Report-Only`) — dan beperkt hij niets en breekt er niets, maar je ziet wel wat er zou zijn geblokkeerd
|
||||
C) Toch een afdwingende ruime CSP op de website (optie A van de oorspronkelijke vraag), zodat SECURITY-04 gewoon gehaald wordt
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: C
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# Functional Design Plan — U3 HTTP Security Headers & CSP
|
||||
|
||||
**Unit**: U3 HTTP Security Headers & CSP
|
||||
**Round**: R2 (with U4 Observability)
|
||||
**Requirements**: FR-18
|
||||
**Components**: C-01, C-02, C-03, U3 portion of C-16
|
||||
|
||||
**Scope boundary**: this stage designs *behaviour* — which headers apply to which response, what each policy permits, and what happens in edge cases. The *pattern* decisions (how the policy is composed, how the configuration surface is shaped) belong to this unit's NFR Design, which follows.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Analyze unit context
|
||||
- [x] Read the U3 definition from `unit-of-work.md`
|
||||
- [x] Read FR-18 and D-31 from `requirements.md`
|
||||
- [x] Read the per-header scoping decision (FU1 = A) and the configuration split (FU2 = A)
|
||||
- [x] Confirm the settled path layout from U1 as delivered
|
||||
|
||||
## Step 2: Design header applicability
|
||||
- [x] Define which headers apply to all responses and which to HTML only
|
||||
- [x] Define behaviour for redirects, `304 Not Modified` and error responses
|
||||
- [x] Define behaviour when a header is already present
|
||||
- [x] Define whether headers apply in Development — see Question 3
|
||||
|
||||
## Step 3: Design the two policies
|
||||
- [x] Define what the `Strict` policy permits for `/admin` and `/api/v1` — see Question 1
|
||||
- [x] Define what the `Relaxed` policy permits for the public website — see Question 2
|
||||
- [x] Define how the Umami and Sentry origins enter the policy
|
||||
- [x] Define behaviour for the placeholder page
|
||||
|
||||
## Step 4: Design failure behaviour
|
||||
- [x] Define startup behaviour on an unknown policy name
|
||||
- [x] Define behaviour when no origins are configured
|
||||
- [x] Confirm header application never throws into the response path
|
||||
|
||||
## Step 5: Define business rules
|
||||
- [x] Enumerate applicability rules
|
||||
- [x] Enumerate policy-content rules
|
||||
- [x] Identify error and edge-case scenarios
|
||||
|
||||
## Step 6: Generate artifacts
|
||||
- [x] Generate `business-logic-model.md`
|
||||
- [x] Generate `business-rules.md`
|
||||
- [x] Generate `domain-entities.md`
|
||||
- [x] Validate all diagrams against the Mermaid standards
|
||||
- [x] Verify Security Baseline compliance for this unit's design
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
# Functional Design Questions — U4 Observability Integration
|
||||
|
||||
Vul je keuze in achter elke `[Answer]:`-tag. Kies de laatste optie (`Anders`) als niets past.
|
||||
|
||||
---
|
||||
|
||||
## Question 1 — Gaan Sentry-meldingen direct of via een tunnel?
|
||||
|
||||
**Context**: in je SlpSoftware-workflow loopt Sentry via een **tunnel**: de browser stuurt fouten naar een pad op je eigen domein (`/sentry-tunnel`), dat ze doorstuurt naar Sentry. Dat is daar gedaan omdat adblockers verzoeken naar Sentry-domeinen blokkeren met `ERR_BLOCKED_BY_CLIENT` — waardoor je juist bij de gebruikers met een adblocker geen fouten meer ziet.
|
||||
|
||||
Dat probleem geldt hier net zo goed. Een tunnel heeft bovendien een neveneffect dat hier goed uitkomt: alle verkeer blijft same-origin, dus `connect-src 'self'` volstaat en de CSP van U3 heeft geen externe Sentry-origin nodig.
|
||||
|
||||
Nadeel: de tunnel moet ergens draaien. In jouw referentie doet nginx dat; hier kan het geen serverconfiguratie zijn, dus zou de .NET-app het zelf moeten doorsturen.
|
||||
|
||||
A) Tunnel via de API — een endpoint in de app stuurt de Sentry-envelope door. Werkt met adblockers, houdt de CSP eenvoudig, en vereist geen serverconfiguratie. Wel nieuwe code die uitgaand verkeer doet
|
||||
B) Direct naar Sentry, en de Sentry-ingest-origin in de CSP toestaan — eenvoudiger, geen extra endpoint, maar fouten van gebruikers met een adblocker komen niet aan
|
||||
C) Direct nu, tunnel als vervolgpunt als blijkt dat er te veel gemist wordt
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: A
|
||||
|
||||
---
|
||||
|
||||
## Question 2 — Mag Sentry gebruikersgegevens meesturen?
|
||||
|
||||
**Context**: `Sentry.AspNetCore` heeft een instelling `SendDefaultPii`. Staat die aan, dan stuurt Sentry bij elke fout ook het IP-adres, de gebruikersnaam en request-headers mee — inclusief cookies. In deze applicatie zit in die cookies de `refreshToken`.
|
||||
|
||||
Standaard staat de instelling uit. SECURITY-03 verbiedt expliciet het loggen van secrets en PII.
|
||||
|
||||
A) Uit laten — geen PII, geen cookies, geen IP. Fouten bevatten dan alleen de technische context, wat voor diagnose vrijwel altijd genoeg is
|
||||
B) Aan, maar met een filter dat de `refreshToken`-cookie en de `Authorization`-header verwijdert — meer context bij een fout, met het gevoelige eruit gehaald
|
||||
C) Aan zonder filter — maximale context
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]: B
|
||||
|
||||
---
|
||||
|
||||
## Question 3 — Welk logniveau in productie?
|
||||
|
||||
**Context**: `appsettings.json` staat nu op `Warning`. Dat betekent dat een normale productie-run vrijwel niets logt — geen opstartmeldingen, geen module-discovery, geen migratie-uitkomst. Juist die drie zijn na een deploy het interessantst, en `ModuleOrchestrator` logt op `Information` dat hij modules gevonden heeft (bij falen logt hij een waarschuwing, maar hij stopt niet).
|
||||
|
||||
Je koos structured logging naar Sentry op niveaus verder dan alleen exceptions (Q16 = C).
|
||||
|
||||
A) `Information` als standaard in productie, met `Microsoft.AspNetCore` op `Warning` — dan zie je opstart, modules en migraties, zonder request-ruis
|
||||
B) `Warning` behouden en de belangrijke opstartmeldingen expliciet naar `Warning` tillen — minimale logvolume, maar dan staat "alles ging goed" als waarschuwing in het log
|
||||
C) `Information` voor alles, inclusief het framework — meeste inzicht, meeste volume
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:C
|
||||
|
||||
---
|
||||
|
||||
## Question 4 — Welke gebeurtenissen zijn het waard om op te alarmeren?
|
||||
|
||||
**Context**: SECURITY-14 vraagt alerting op authenticatiefouten en autorisatieschendingen. De applicatie moet die dus eerst als gebeurtenis uitsturen voordat je er in Sentry een alertregel op kunt zetten.
|
||||
|
||||
Kandidaten die deze applicatie kan onderscheiden. Meerdere letters mogen (bijv. `A, B, D`).
|
||||
|
||||
A) Mislukte logins — meerdere achter elkaar duidt op een aanval of een vergeten wachtwoord
|
||||
B) Geweigerde autorisatie op een beveiligd endpoint — iemand probeert iets waarvoor hij geen rechten heeft
|
||||
C) Een geweigerde master-API-key op `/api/v1/master/*` of `/api/v1/SlaveStatus` — dat betekent óf een aanvaller, óf een echt probleem met de key ring
|
||||
D) Een geweigerde admin-bypass op de availability-gate — sinds de FR-24-fix betekent dit iemand die het met een ongeldig token probeerde
|
||||
E) Rate limiting die aanslaat op de login-endpoints
|
||||
F) Migratiefouten bij het opstarten — geen beveiligingsgebeurtenis, maar wel iets waarvan je direct wilt weten
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A, B, C, D, E, F
|
||||
|
||||
---
|
||||
|
||||
## Question 5 — Umami op de admin-SPA: altijd, of respecteren wat de browser vraagt?
|
||||
|
||||
**Context**: je koos Umami op zowel de publieke website als de admin-SPA (Q20 = B). De admin-SPA is een intern beheerscherm, en de gebruikers daarvan zijn jouw klanten en hun medewerkers — herkenbare, kleine groepen.
|
||||
|
||||
Umami is privacyvriendelijk (geen cookies, geen persoonsgegevens), dus dit is geen juridische vraag maar een keuze over verwachtingen.
|
||||
|
||||
A) Altijd meten in test en productie, nooit lokaal — eenvoudig en consistent
|
||||
B) Altijd meten, maar de `Do Not Track`-voorkeur van de browser respecteren en dan niets laden
|
||||
C) Alleen de publieke website meten en de admin-SPA overslaan, in afwijking van Q20 — beheerders worden dan niet gemeten
|
||||
X) Anders (beschrijf hieronder na de [Answer]:-tag)
|
||||
|
||||
[Answer]:A
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# Functional Design Plan — U4 Observability Integration
|
||||
|
||||
**Unit**: U4 Observability Integration
|
||||
**Round**: R2 (with U3 Security Headers & CSP)
|
||||
**Requirements**: FR-13, FR-14, FR-15, FR-16, FR-19
|
||||
**Components**: C-08, C-09, C-14, C-15, U4 portion of C-16
|
||||
|
||||
**Scope boundary**: this stage designs *behaviour* — what is reported, when, and what happens when a service is absent or unreachable. The *pattern* decisions (structured-log shape, correlation-ID mechanism per OPEN-01, alert-rule design) belong to this unit's NFR Design, which follows.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Analyze unit context
|
||||
- [x] Read the U4 definition from `unit-of-work.md`
|
||||
- [x] Read FR-13 through FR-16 and FR-19 from `requirements.md`
|
||||
- [x] Read the reference project's Sentry and Umami implementation for the frontend
|
||||
- [x] Note the coupling to U3: the origins this unit introduces must be permitted by U3's CSP
|
||||
|
||||
## Step 2: Design backend error and log reporting
|
||||
- [x] Define behaviour when no Sentry DSN is configured
|
||||
- [x] Define behaviour when Sentry is configured but unreachable
|
||||
- [x] Define what is sent and what is deliberately withheld — see Question 2
|
||||
- [x] Define the production log level — see Question 3
|
||||
- [x] Define environment and release tagging
|
||||
|
||||
## Step 3: Design security event emission
|
||||
- [x] Define which events are emitted for alerting — see Question 4
|
||||
- [x] Define what context each event carries, and what it must never carry
|
||||
- [x] Confirm no event carries a password, token or PII
|
||||
|
||||
## Step 4: Design frontend observability
|
||||
- [x] Define Sentry initialisation and the absent-DSN path
|
||||
- [x] Define the transport route to Sentry — see Question 1
|
||||
- [x] Define Umami inclusion behaviour and its absent-configuration path — see Question 5
|
||||
- [x] Define behaviour in local development
|
||||
|
||||
## Step 5: Design the same-origin API base URL
|
||||
- [x] Define resolution when `VITE_API_BASE_URL` is absent or empty
|
||||
- [x] Define resolution when an explicit absolute URL is supplied
|
||||
- [x] Define validation behaviour for a malformed value
|
||||
- [x] Confirm the local master and slave development setups keep working
|
||||
|
||||
## Step 6: Define business rules
|
||||
- [x] Enumerate reporting rules
|
||||
- [x] Enumerate degradation rules
|
||||
- [x] Enumerate configuration-resolution rules
|
||||
- [x] Identify error and edge-case scenarios
|
||||
|
||||
## Step 7: Generate artifacts
|
||||
- [x] Generate `business-logic-model.md`
|
||||
- [x] Generate `business-rules.md`
|
||||
- [x] Generate `domain-entities.md`
|
||||
- [x] Generate `frontend-components.md`
|
||||
- [x] Validate all diagrams against the Mermaid standards
|
||||
- [x] Verify Security Baseline compliance for this unit's design
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
# Business Logic Model — U3 HTTP Security Headers & CSP
|
||||
|
||||
**Unit**: U3 HTTP Security Headers & CSP
|
||||
**Requirements**: FR-18
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope of the Logic
|
||||
|
||||
Normally these headers come from nginx or IIS configuration. NFR-01 forbids relying on server configuration, so the application must supply them itself — which turns a configuration file into request-processing logic with three decisions per response:
|
||||
|
||||
1. **Which policy** applies to this request path
|
||||
2. **Which headers** apply to this response, based on its content type
|
||||
3. **Whether** headers apply at all in this environment
|
||||
|
||||
---
|
||||
|
||||
## 2. Header Application Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
req["Incoming request"]
|
||||
enabled{"Headers enabled ?"}
|
||||
skip["Continue without headers"]
|
||||
resolve["Resolve policy name<br/>from request path"]
|
||||
hook["Register response-start callback"]
|
||||
next["Continue pipeline"]
|
||||
start["Response starting"]
|
||||
always["Apply always-headers:<br/>X-Content-Type-Options<br/>plus HSTS outside Development"]
|
||||
ishtml{"Content type is HTML ?"}
|
||||
htmlonly["Apply HTML-only headers:<br/>Content-Security-Policy<br/>X-Frame-Options<br/>Referrer-Policy"]
|
||||
done["Response sent"]
|
||||
|
||||
req --> enabled
|
||||
enabled -->|no| skip
|
||||
enabled -->|yes| resolve
|
||||
resolve --> hook
|
||||
hook --> next
|
||||
next --> start
|
||||
start --> always
|
||||
always --> ishtml
|
||||
ishtml -->|yes| htmlonly
|
||||
ishtml -->|no| done
|
||||
htmlonly --> done
|
||||
|
||||
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef step fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef neutral fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000;
|
||||
class req,start entry;
|
||||
class enabled,ishtml decision;
|
||||
class resolve,hook,next,always,htmlonly step;
|
||||
class skip,done neutral;
|
||||
```
|
||||
|
||||
Text alternative: the policy for the path is resolved when the request arrives, but headers are written at response start — because the content type, which decides whether the HTML-only headers apply, is not known any earlier.
|
||||
|
||||
**Why the work is split across two moments**: path matching happens once per request, cheaply, before the pipeline continues. Content-type inspection can only happen at response start. Doing both at response start would repeat path matching on every static asset; doing both early would force an all-or-nothing choice on header scope.
|
||||
|
||||
**Why registration must precede static files**: static-file middleware short-circuits the pipeline. Anything registered after it never observes a static response — and static responses are exactly what the public website consists of.
|
||||
|
||||
---
|
||||
|
||||
## 3. Per-Header Scoping
|
||||
|
||||
Per FU1 = A, scope is decided per header rather than uniformly.
|
||||
|
||||
| Header | Applies to | Reason |
|
||||
|---|---|---|
|
||||
| `X-Content-Type-Options: nosniff` | **All** responses | Exists specifically to stop MIME-sniffing of non-HTML resources. Restricting it to HTML would remove it exactly where it does its job |
|
||||
| `Strict-Transport-Security` | **All** responses, **outside Development only** | A host-level transport directive, not a page directive. A visitor whose first request is an asset would otherwise never receive it |
|
||||
| `Content-Security-Policy` | HTML responses only | Meaningless on an image or a script file |
|
||||
| `X-Frame-Options` | HTML responses only | Governs framing of documents |
|
||||
| `Referrer-Policy` | HTML responses only | Governs navigation and resource referrers from a document |
|
||||
|
||||
**HSTS and Development** (Q3 = A): browsers remember HSTS per host, for a long time, and `localhost` is shared with every other local project. Sending it during development would affect unrelated work and is awkward to undo. Every other header **does** apply in Development, so a CSP violation surfaces while developing rather than in production.
|
||||
|
||||
---
|
||||
|
||||
## 4. Policy Selection
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
path["Request path"]
|
||||
admin{"Starts with /admin ?"}
|
||||
api{"Starts with /api/v1 ?"}
|
||||
health{"Is /health ?"}
|
||||
strict["Strict policy"]
|
||||
relaxed["Relaxed policy"]
|
||||
|
||||
path --> admin
|
||||
admin -->|yes| strict
|
||||
admin -->|no| api
|
||||
api -->|yes| strict
|
||||
api -->|no| health
|
||||
health -->|yes| strict
|
||||
health -->|no| relaxed
|
||||
|
||||
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef strictnode fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef relaxednode fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
class path entry;
|
||||
class admin,api,health decision;
|
||||
class strict strictnode;
|
||||
class relaxed relaxednode;
|
||||
```
|
||||
|
||||
Text alternative: paths under `/admin`, `/api/v1` and `/health` get the strict policy; everything else — the public website and the placeholder page — gets the relaxed policy.
|
||||
|
||||
The mapping itself is configuration (FU2 = A), so a path can be added without code changes. The two policies are defined in code, so a misconfiguration can misroute a path but cannot invent a broken policy.
|
||||
|
||||
---
|
||||
|
||||
## 5. What Each Policy Permits
|
||||
|
||||
### Strict — `/admin`, `/api/v1`, `/health`
|
||||
|
||||
| Directive | Value | Reason |
|
||||
|---|---|---|
|
||||
| `default-src` | `'self'` | Deny by default |
|
||||
| `script-src` | `'self'` | **No `'unsafe-inline'`, no `'unsafe-eval'`.** This is where XSS risk actually lives, and it stays closed |
|
||||
| `style-src` | `'self' 'unsafe-inline'` | Required — see below |
|
||||
| `img-src` | `'self' data:` | `data:` covers inlined icons in the built bundle |
|
||||
| `font-src` | `'self'` | Fonts ship with the bundle |
|
||||
| `connect-src` | `'self'` + configured origins | Same-origin API. The Sentry tunnel (U4) keeps error reporting same-origin too |
|
||||
| `frame-ancestors` | `'none'` | Matches `X-Frame-Options: DENY` for modern browsers |
|
||||
| `base-uri` | `'self'` | Prevents base-tag injection redirecting relative URLs |
|
||||
| `form-action` | `'self'` | Prevents form hijacking |
|
||||
| `object-src` | `'none'` | No plugins |
|
||||
|
||||
**`style-src 'unsafe-inline'` — why it is unavoidable** (FU1 = A):
|
||||
|
||||
Radix UI positions dropdowns, dialogs and selects by writing inline `style` attributes such as `style="transform: translate(...)"`, recalculated per click, viewport and scroll position.
|
||||
|
||||
CSP nonces apply only to `<style>` and `<script>` **elements** — inline `style` **attributes** are outside their reach entirely. The only mechanisms that can permit them are `'unsafe-inline'`, or `'unsafe-hashes'` with a hash per exact attribute value, and those values are dynamic so no finite set exists. There is therefore no nonce- or hash-based variant that leaves the admin UI functional.
|
||||
|
||||
The exception is contained: it applies to `style-src` only. Injected CSS can restyle a page, but `script-src 'self'` still prevents execution of injected script, which is the actual escalation path.
|
||||
|
||||
### Relaxed — the public website
|
||||
|
||||
Per FU2 = C, the website receives an **enforcing** CSP rather than none, so SECURITY-04 is satisfied on every HTML-serving path.
|
||||
|
||||
| Directive | Value | Reason |
|
||||
|---|---|---|
|
||||
| `default-src` | `'self'` | Deny by default |
|
||||
| `script-src` | `'self' 'unsafe-inline'` | A website author may use inline scripts and has never seen this policy |
|
||||
| `style-src` | `'self' 'unsafe-inline'` | Same |
|
||||
| `img-src` | `'self' data: https:` | Images from any HTTPS source — commonplace on a marketing site |
|
||||
| `font-src` | `'self' data: https:` | Web fonts from any HTTPS source |
|
||||
| `connect-src` | `'self'` + configured origins | Includes the Umami origin when configured |
|
||||
| `frame-src` | `'self' https:` | Embeds such as maps and video |
|
||||
| `frame-ancestors` | `'self'` | Matches `X-Frame-Options: SAMEORIGIN` |
|
||||
| `base-uri` | `'self'` | Retained — cheap and breaks nothing |
|
||||
| `object-src` | `'none'` | Retained |
|
||||
|
||||
**What "relaxed" deliberately still blocks**: an external `script-src`. A website author who needs a third-party script must have its origin added to configuration — which the website contract (FR-09) documents. That keeps the policy from being a rubber stamp while still not surprising anyone with a broken layout.
|
||||
|
||||
**Note on `'unsafe-inline'` and external scripts**: when a source list contains `'unsafe-inline'`, browsers honour it *and* the listed origins. Adding an origin therefore does not silently disable inline scripts.
|
||||
|
||||
---
|
||||
|
||||
## 6. Frame Options Per Policy
|
||||
|
||||
Per Q4 = A, this refines FR-18, which specified `DENY` globally without accounting for the public website:
|
||||
|
||||
| Path | `X-Frame-Options` | `frame-ancestors` |
|
||||
|---|---|---|
|
||||
| `/admin`, `/api/v1`, `/health` | `DENY` | `'none'` |
|
||||
| Public website | `SAMEORIGIN` | `'self'` |
|
||||
|
||||
The admin UI stays maximally protected against click-jacking. A customer embedding one of their own pages in an iframe on their own site is not broken by a policy they never chose.
|
||||
|
||||
Both headers are emitted because they overlap rather than replace: `X-Frame-Options` covers older browsers, `frame-ancestors` is the modern equivalent and takes precedence where supported.
|
||||
|
||||
---
|
||||
|
||||
## 7. Startup Validation
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
boot["Startup"]
|
||||
unknown{"Every configured policy name<br/>is a known policy ?"}
|
||||
fail["Throw: process does not start"]
|
||||
build["Build both policy strings once"]
|
||||
monitoring{"Umami website ID configured<br/>but its origin missing from CSP ?"}
|
||||
warn["Log a warning naming the missing origin"]
|
||||
log["Log which origins are permitted"]
|
||||
ready["Ready to serve"]
|
||||
|
||||
boot --> unknown
|
||||
unknown -->|no| fail
|
||||
unknown -->|yes| build
|
||||
build --> monitoring
|
||||
monitoring -->|yes| warn
|
||||
monitoring -->|no| log
|
||||
warn --> log
|
||||
log --> ready
|
||||
|
||||
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef step fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef warnnode fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef bad fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000;
|
||||
class boot entry;
|
||||
class unknown,monitoring decision;
|
||||
class build,log,ready step;
|
||||
class warn warnnode;
|
||||
class fail bad;
|
||||
```
|
||||
|
||||
Text alternative: an unknown policy name stops the process at startup rather than degrading per request, and a configured Umami website ID whose origin is missing from the CSP produces a warning — catching the case where analytics appears configured but is silently blocked.
|
||||
|
||||
**Why an unknown policy name is fatal** (fail closed, SECURITY-15): the alternative is falling back to something, and every fallback is either wrong or silently permissive. A typo in a path-policy mapping should stop a deployment, not quietly serve `/admin` under the relaxed policy.
|
||||
|
||||
**Why the monitoring warning covers Umami only** (Q5 = C): the question anticipated needing a Sentry ingest origin in `connect-src`. U4 Q1 = A chose a **tunnel through the API**, so browser error reports go to the application's own origin and `connect-src 'self'` already covers them. There is no Sentry origin to forget, so the warning would have nothing to check. Umami's script is still loaded from its own origin, so that check remains meaningful.
|
||||
|
||||
**Policies are built once at startup**, then reused. Composing a CSP per response would be wasteful on a workload that is mostly static files.
|
||||
|
||||
---
|
||||
|
||||
## 8. Interaction With the Placeholder Page
|
||||
|
||||
The built-in placeholder (U1) is served at `/`, so it receives the **relaxed** policy. It is a self-contained HTML document with a `<style>` block and no scripts, which the relaxed policy permits via `style-src 'unsafe-inline'`.
|
||||
|
||||
Worth stating because it is easy to overlook: the placeholder is the one HTML document this repository serves at the website path, so if the relaxed policy were ever tightened, it is the first thing that would break — and it would break on a fresh installation, which is the worst moment for a confusing failure.
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# Business Rules — U3 HTTP Security Headers & CSP
|
||||
|
||||
---
|
||||
|
||||
## Decision Logic
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
start["Response starting"]
|
||||
exists{"Header already set<br/>by something else ?"}
|
||||
leave["Leave it untouched"]
|
||||
dev{"Environment is Development<br/>and header is HSTS ?"}
|
||||
skiphsts["Skip HSTS"]
|
||||
always{"Header is nosniff or HSTS ?"}
|
||||
apply["Apply"]
|
||||
html{"Content type is HTML ?"}
|
||||
skiphtml["Skip: not an HTML response"]
|
||||
|
||||
start --> exists
|
||||
exists -->|yes| leave
|
||||
exists -->|no| dev
|
||||
dev -->|yes| skiphsts
|
||||
dev -->|no| always
|
||||
always -->|yes| apply
|
||||
always -->|no| html
|
||||
html -->|yes| apply
|
||||
html -->|no| skiphtml
|
||||
|
||||
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef good fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef neutral fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000;
|
||||
class start entry;
|
||||
class exists,dev,always,html decision;
|
||||
class apply good;
|
||||
class leave,skiphsts,skiphtml neutral;
|
||||
```
|
||||
|
||||
Text alternative: an already-present header is never overwritten; HSTS is skipped in Development; `nosniff` and HSTS apply to every response while the remaining three apply only to HTML responses.
|
||||
|
||||
---
|
||||
|
||||
## Applicability Rules
|
||||
|
||||
| ID | Rule |
|
||||
|---|---|
|
||||
| **BR-U3-01** | `X-Content-Type-Options: nosniff` is applied to **every** response. |
|
||||
| **BR-U3-02** | `Strict-Transport-Security` is applied to **every** response **except in Development**. |
|
||||
| **BR-U3-03** | `Content-Security-Policy`, `X-Frame-Options` and `Referrer-Policy` are applied **only** to responses whose content type is HTML. |
|
||||
| **BR-U3-04** | A header already present on the response is never overwritten. |
|
||||
| **BR-U3-05** | Headers are written at response start, not before the pipeline continues, because the content type is unknown earlier. |
|
||||
| **BR-U3-06** | The middleware is registered **before** static-file middleware, which short-circuits the pipeline. |
|
||||
| **BR-U3-07** | Header application never throws into the response path. A configuration error is a startup failure, not a per-request one. |
|
||||
| **BR-U3-08** | Every header except HSTS applies in Development, so a CSP violation surfaces during development. |
|
||||
| **BR-U3-09** | Headers apply to error responses too — the middleware sits inside the exception handler. |
|
||||
|
||||
**Rationale for BR-U3-01**: `nosniff` exists to stop a browser guessing the type of a **non-HTML** resource. An uploaded `.txt` or `.svg` interpreted as HTML or JavaScript is the attack it prevents, so restricting it to HTML would remove it precisely where it works.
|
||||
|
||||
**Rationale for BR-U3-04**: a component that deliberately set a header — a download endpoint setting its own `Content-Disposition`-adjacent policy, for example — knows something this middleware does not. Overwriting would be silently destructive.
|
||||
|
||||
---
|
||||
|
||||
## Policy Content Rules
|
||||
|
||||
| ID | Rule |
|
||||
|---|---|
|
||||
| **BR-U3-10** | Exactly two policies exist, defined **in code**: `Strict` and `Relaxed`. |
|
||||
| **BR-U3-11** | Path-to-policy assignment and allowed origins come from **configuration**, so a path or origin can be added without a code change. |
|
||||
| **BR-U3-12** | `Strict` applies to `/admin`, `/api/v1` and `/health`. `Relaxed` is the default for everything else. |
|
||||
| **BR-U3-13** | `Strict` sets `script-src 'self'` — **no `'unsafe-inline'` and no `'unsafe-eval'`**. This must not be relaxed. |
|
||||
| **BR-U3-14** | `Strict` sets `style-src 'self' 'unsafe-inline'`. Documented exception, unavoidable — see below. |
|
||||
| **BR-U3-15** | `Relaxed` is **enforcing**, not report-only, so SECURITY-04 is satisfied on every HTML-serving path. |
|
||||
| **BR-U3-16** | `Relaxed` permits inline scripts and styles, and images, fonts and frames from any HTTPS origin — but **not** external script origins. |
|
||||
| **BR-U3-17** | Both policies set `object-src 'none'` and `base-uri 'self'`. |
|
||||
| **BR-U3-18** | Policy strings are composed once at startup and reused. |
|
||||
| **BR-U3-19** | `X-Frame-Options` is `DENY` under `Strict` and `SAMEORIGIN` under `Relaxed`, with `frame-ancestors` set to match. |
|
||||
|
||||
**Rationale for BR-U3-14 — the one exception, and why it is not negotiable**: Radix UI positions dropdowns, dialogs and selects using inline `style` attributes whose values are recomputed per click, viewport and scroll position. CSP nonces apply only to `<style>` and `<script>` *elements*; inline `style` *attributes* are outside their scope entirely. The alternatives are `'unsafe-inline'` or `'unsafe-hashes'` with a hash per exact value — and the values are dynamic, so no finite set exists. No nonce- or hash-based variant leaves the admin UI functional.
|
||||
|
||||
The exception is bounded to `style-src`. Injected CSS can restyle a page; it cannot execute, because `script-src 'self'` still holds. The escalation path stays closed.
|
||||
|
||||
**Rationale for BR-U3-13**: this is the directive that matters. If it is ever relaxed, the value of the whole policy collapses — so it is stated as a rule rather than left as a default.
|
||||
|
||||
**Rationale for BR-U3-16**: "relaxed" must not mean "absent". Blocking external script origins keeps a genuine boundary while not surprising a website author with a broken layout. A third-party script requires adding its origin to configuration, which FR-09's contract documents.
|
||||
|
||||
---
|
||||
|
||||
## Startup Validation Rules
|
||||
|
||||
| ID | Rule |
|
||||
|---|---|
|
||||
| **BR-U3-20** | A configured policy name that is not a known policy causes startup to **fail**. No fallback. |
|
||||
| **BR-U3-21** | Empty origin lists are a normal state. The policy simply becomes stricter. |
|
||||
| **BR-U3-22** | When a Umami website ID is configured but its script origin is absent from the allowed origins, a **warning** is logged at startup naming the missing origin. |
|
||||
| **BR-U3-23** | The permitted origins are logged at startup at informational level, so the log records what was actually allowed. |
|
||||
| **BR-U3-24** | Headers can be disabled wholesale by configuration, for diagnosis. Disabling is logged as a warning. |
|
||||
|
||||
**Rationale for BR-U3-20** (fail closed, SECURITY-15): any fallback is either wrong or silently permissive. A typo in a path mapping should stop a deployment rather than quietly serve `/admin` under the relaxed policy.
|
||||
|
||||
**Rationale for BR-U3-22**: this catches a failure that is otherwise invisible — analytics configured, appearing to work, and silently blocked by the browser. Note it deliberately does **not** check for a Sentry origin: U4's tunnel keeps error reporting same-origin, so there is no Sentry origin to forget.
|
||||
|
||||
**Rationale for BR-U3-24**: a diagnostic escape hatch is worth having, but silently disabled security headers are worse than none, so switching them off announces itself.
|
||||
|
||||
---
|
||||
|
||||
## Error and Edge-Case Scenarios
|
||||
|
||||
| Scenario | Expected behaviour |
|
||||
|---|---|
|
||||
| Request for `/admin/dashboard`, HTML response | All five headers; `Strict` policy; `X-Frame-Options: DENY` |
|
||||
| Request for `/admin/assets/app.js` | `nosniff` and HSTS only — not an HTML response |
|
||||
| Request for `/`, website HTML | All five headers; `Relaxed` policy; `SAMEORIGIN` |
|
||||
| Request for `/` on a fresh install, placeholder page | `Relaxed` policy. The placeholder's `<style>` block is permitted by `style-src 'unsafe-inline'` |
|
||||
| Request for `/api/v1/Users`, JSON response | `nosniff` and HSTS only. `Strict` policy resolved but the CSP is not written to a JSON response |
|
||||
| `503` from the availability gate, JSON `ProblemDetails` | `nosniff` and HSTS. The middleware sits before the gate, so the response still carries them |
|
||||
| Unhandled exception, `ProblemDetails` response | Headers applied — the middleware is inside the exception handler |
|
||||
| `304 Not Modified` | `nosniff` and HSTS. No body, so the HTML-only headers do not apply |
|
||||
| Redirect from `/admin` to `/admin/` | `nosniff` and HSTS. A redirect has no HTML body |
|
||||
| Running in Development | Every header except HSTS |
|
||||
| No origins configured | Policies composed without them; stricter, and logged |
|
||||
| Umami ID configured, origin missing | Warning at startup naming the origin |
|
||||
| Configured policy name is `Stricct` | Startup fails with the unknown name |
|
||||
| Headers disabled by configuration | No headers, and a warning logged |
|
||||
| A downstream component already set `Referrer-Policy` | Its value is kept |
|
||||
|
||||
---
|
||||
|
||||
## Security Compliance for U3
|
||||
|
||||
| Rule | Status | Notes |
|
||||
|---|---|---|
|
||||
| SECURITY-04 | **Compliant** | All five headers present. HSTS `max-age` is one year with `includeSubDomains`. A CSP applies to **every** HTML-serving path, including the public website (BR-U3-15) — so no deviation is needed. `'unsafe-inline'` appears only in `style-src` under `Strict`, documented in BR-U3-14; `Relaxed` additionally permits inline scripts, documented in BR-U3-16 as a deliberate choice for content this repository does not author |
|
||||
| SECURITY-09 | Compliant | No internal detail is exposed by any header |
|
||||
| SECURITY-11 | **Improved** | Defence in depth: the CSP is a second layer behind output escaping, on both the admin UI and the website |
|
||||
| SECURITY-15 | Compliant | Fails closed at startup on an unknown policy; never throws per request |
|
||||
|
||||
**No deviation recorded.** The earlier answer of "no CSP on the public website" was superseded by FU2 = C, which keeps SECURITY-04 satisfied outright rather than accepting a documented exception.
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
# Domain Entities — U3 HTTP Security Headers & CSP
|
||||
|
||||
**No persisted entity.** U3 adds no table, no migration and no database column. It introduces one configuration section and two in-code policy definitions.
|
||||
|
||||
---
|
||||
|
||||
## Concept Relationships
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
config["SecurityHeaders configuration section"]
|
||||
rules["PathPolicyRule list<br/>path prefix to policy name"]
|
||||
origins["Allowed origin lists<br/>script and connect"]
|
||||
toggle["Enabled flag"]
|
||||
builder["Policy builder"]
|
||||
strict["Strict policy<br/>defined in code"]
|
||||
relaxed["Relaxed policy<br/>defined in code"]
|
||||
composed["Composed policy strings<br/>built once at startup"]
|
||||
middleware["Security headers middleware"]
|
||||
response["HTTP response"]
|
||||
|
||||
config --> rules
|
||||
config --> origins
|
||||
config --> toggle
|
||||
rules -->|"selects"| builder
|
||||
origins -->|"injected into"| builder
|
||||
builder --> strict
|
||||
builder --> relaxed
|
||||
strict --> composed
|
||||
relaxed --> composed
|
||||
composed -->|"read by"| middleware
|
||||
toggle -->|"gates"| middleware
|
||||
middleware -->|"writes headers to"| response
|
||||
|
||||
classDef cfg fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef code fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef runtime fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef output fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000;
|
||||
class config,rules,origins,toggle cfg;
|
||||
class builder,strict,relaxed code;
|
||||
class composed,middleware runtime;
|
||||
class response output;
|
||||
```
|
||||
|
||||
Text alternative: configuration supplies the path-to-policy mapping, the allowed origins and an enable flag; the two policy definitions live in code and are composed into strings once at startup, which the middleware then writes onto responses.
|
||||
|
||||
**The split is the design** (FU2 = A): configuration decides *where* a policy applies and *which external origins* are permitted. Code decides *what a policy means*. A misconfiguration can therefore misroute a path or omit an origin — both recoverable and both visible — but cannot produce a policy that is subtly wrong.
|
||||
|
||||
---
|
||||
|
||||
## SecurityHeaders configuration section
|
||||
|
||||
New section in `appsettings.json`, following the existing Options pattern used by `JwtSettings`, `MasterModule`, `MasterPolling` and `Availability`.
|
||||
|
||||
| Field | Type | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `Enabled` | bool | `true` | Diagnostic escape hatch. Disabling logs a warning (BR-U3-24) |
|
||||
| `PathPolicies` | list of rules | `/admin` → Strict, `/api/v1` → Strict, `/health` → Strict | Ordered path-prefix to policy-name mapping |
|
||||
| `DefaultPolicy` | string | `Relaxed` | Applied when no prefix matches — the public website |
|
||||
| `AllowedScriptOrigins` | string list | empty | Added to `script-src`. The Umami script host |
|
||||
| `AllowedConnectOrigins` | string list | empty | Added to `connect-src` |
|
||||
|
||||
### PathPolicyRule
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `PathPrefix` | string | Matched case-insensitively against the start of the request path |
|
||||
| `Policy` | string | Must name a known policy, or startup fails (BR-U3-20) |
|
||||
|
||||
### Validation
|
||||
|
||||
| Aspect | Rule |
|
||||
|---|---|
|
||||
| Unknown policy name | Startup **fails**. No fallback |
|
||||
| Empty origin lists | Valid — the policy is simply stricter (BR-U3-21) |
|
||||
| Empty `PathPolicies` | Valid — everything falls to `DefaultPolicy` |
|
||||
| Origin format | Must be a scheme-and-host origin, without a path |
|
||||
|
||||
**No Sentry ingest origin is expected**, because U4's tunnel keeps browser error reporting same-origin. `AllowedConnectOrigins` exists for Umami and any future external call, not for Sentry.
|
||||
|
||||
---
|
||||
|
||||
## Policy definitions (in code, not configuration)
|
||||
|
||||
### Strict
|
||||
|
||||
| Directive | Value |
|
||||
|---|---|
|
||||
| `default-src` | `'self'` |
|
||||
| `script-src` | `'self'` |
|
||||
| `style-src` | `'self' 'unsafe-inline'` |
|
||||
| `img-src` | `'self' data:` |
|
||||
| `font-src` | `'self'` |
|
||||
| `connect-src` | `'self'` + `AllowedConnectOrigins` |
|
||||
| `frame-ancestors` | `'none'` |
|
||||
| `base-uri` | `'self'` |
|
||||
| `form-action` | `'self'` |
|
||||
| `object-src` | `'none'` |
|
||||
|
||||
Companion headers: `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`.
|
||||
|
||||
### Relaxed
|
||||
|
||||
| Directive | Value |
|
||||
|---|---|
|
||||
| `default-src` | `'self'` |
|
||||
| `script-src` | `'self' 'unsafe-inline'` + `AllowedScriptOrigins` |
|
||||
| `style-src` | `'self' 'unsafe-inline'` |
|
||||
| `img-src` | `'self' data: https:` |
|
||||
| `font-src` | `'self' data: https:` |
|
||||
| `connect-src` | `'self'` + `AllowedConnectOrigins` |
|
||||
| `frame-src` | `'self' https:` |
|
||||
| `frame-ancestors` | `'self'` |
|
||||
| `base-uri` | `'self'` |
|
||||
| `object-src` | `'none'` |
|
||||
|
||||
Companion headers: `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin`.
|
||||
|
||||
**Both policies always carry** `X-Content-Type-Options: nosniff` and, outside Development, `Strict-Transport-Security: max-age=31536000; includeSubDomains`.
|
||||
|
||||
---
|
||||
|
||||
## Non-persisted runtime state
|
||||
|
||||
| Item | Lifetime | Notes |
|
||||
|---|---|---|
|
||||
| Composed policy strings | Singleton, built at startup | Two strings, keyed by policy name. Never rebuilt per request |
|
||||
| Resolved policy name per request | Request scope | Resolved on the way in, used at response start |
|
||||
|
||||
---
|
||||
|
||||
## Persistence Summary
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| New tables? | None |
|
||||
| New migrations? | None |
|
||||
| New configuration sections? | One — `SecurityHeaders` |
|
||||
| Anything written at runtime? | Only HTTP response headers |
|
||||
| Secrets in configuration? | None. Origins are public hostnames |
|
||||
|
||||
---
|
||||
|
||||
## Environment-Specific Values
|
||||
|
||||
| Environment | `AllowedScriptOrigins` | HSTS | Notes |
|
||||
|---|---|---|---|
|
||||
| Local | empty | **Not sent** | Umami is not loaded locally, so no origin is needed |
|
||||
| Test | Umami host | Sent | Umami website ID for test |
|
||||
| Production | Umami host | Sent | Umami website ID for production |
|
||||
|
||||
The Umami script origin is the same host across test and production — only the website ID differs, and that is a frontend build-time value rather than a CSP concern.
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
# Business Logic Model — U4 Observability Integration
|
||||
|
||||
**Unit**: U4 Observability Integration
|
||||
**Requirements**: FR-13, FR-14, FR-15, FR-16, FR-19
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope of the Logic
|
||||
|
||||
U4 answers three questions that cannot otherwise be answered without host access: is the application erroring, is it being used, and which build is running. Its logic is mostly about **graceful absence** — every observability service must be optional, because local development and any deployment without them must work unchanged.
|
||||
|
||||
---
|
||||
|
||||
## 2. Degradation Model
|
||||
|
||||
Three fully functional configurations rather than one required setup:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
boot["Startup"]
|
||||
logging["Structured console logging<br/>always active"]
|
||||
dsn{"Sentry DSN configured ?"}
|
||||
sentryon["Sentry initialised<br/>environment and release tagged"]
|
||||
sentryoff["Sentry skipped<br/>console only"]
|
||||
reachable{"Sentry reachable ?"}
|
||||
delivered["Events delivered"]
|
||||
buffered["Sentry buffers and drops<br/>application never blocked"]
|
||||
|
||||
boot --> logging
|
||||
logging --> dsn
|
||||
dsn -->|yes| sentryon
|
||||
dsn -->|no| sentryoff
|
||||
sentryon --> reachable
|
||||
reachable -->|yes| delivered
|
||||
reachable -->|no| buffered
|
||||
|
||||
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef always fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef degraded fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
class boot entry;
|
||||
class logging,sentryon,delivered always;
|
||||
class dsn,reachable decision;
|
||||
class sentryoff,buffered degraded;
|
||||
```
|
||||
|
||||
Text alternative: structured console logging is always active; Sentry initialises only when a DSN is present, and an unreachable Sentry never blocks the application.
|
||||
|
||||
**An absent DSN is a supported state, not an error.** Logging is registered before Sentry so that a problem initialising Sentry is itself logged.
|
||||
|
||||
---
|
||||
|
||||
## 3. Log Levels — Console Versus Sentry
|
||||
|
||||
Q3 = C asked for `Information` across the board, including the framework. That is right for the **console**, and would be wrong for **Sentry**.
|
||||
|
||||
Sending every framework `Information` entry to Sentry means an event per request. Sentry's free plan would be exhausted within hours, and the errors that matter would be lost among request noise — the opposite of what monitoring is for.
|
||||
|
||||
The two destinations therefore get different thresholds:
|
||||
|
||||
| Destination | Threshold | Rationale |
|
||||
|---|---|---|
|
||||
| **Console** | `Information` for everything, framework included (Q3 = C) | On the Pi the console is captured by the process manager, so volume is cheap and detail is useful |
|
||||
| **Sentry — events** | `Warning` and above | Still "more than exceptions" as Q16 = C requires, since warnings are included, without one event per request |
|
||||
| **Sentry — breadcrumbs** | `Information` | Informational entries travel *attached to* an event as context, so the detail is there when something goes wrong without being an event itself |
|
||||
|
||||
This satisfies both answers rather than choosing between them: the console gets everything, Sentry gets warnings and errors, and every Sentry event arrives carrying the informational trail that led to it.
|
||||
|
||||
---
|
||||
|
||||
## 4. Security Event Emission
|
||||
|
||||
Per Q4 = all of A–F, six event types are emitted for alerting.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph auth["Authentication and authorization"]
|
||||
e1["Failed login"]
|
||||
e2["Authorization denied<br/>on a protected endpoint"]
|
||||
e5["Rate limit triggered<br/>on login endpoints"]
|
||||
end
|
||||
subgraph proto["Master and slave protocol"]
|
||||
e3["Master API key rejected"]
|
||||
e4["Admin bypass rejected<br/>at the availability gate"]
|
||||
end
|
||||
subgraph infra["Infrastructure"]
|
||||
e6["Migration failure at startup"]
|
||||
end
|
||||
sink["Structured log entry<br/>plus Sentry event"]
|
||||
alert["Sentry alert rule<br/>configured in Operations"]
|
||||
|
||||
e1 --> sink
|
||||
e2 --> sink
|
||||
e3 --> sink
|
||||
e4 --> sink
|
||||
e5 --> sink
|
||||
e6 --> sink
|
||||
sink --> alert
|
||||
|
||||
classDef authgrp fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000;
|
||||
classDef protogrp fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef infragrp fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef out fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
class e1,e2,e5 authgrp;
|
||||
class e3,e4 protogrp;
|
||||
class e6 infragrp;
|
||||
class sink,alert out;
|
||||
```
|
||||
|
||||
Text alternative: six event types across authentication, the master/slave protocol and infrastructure all flow into structured log entries that also become Sentry events, on which alert rules are configured during the Operations phase.
|
||||
|
||||
### What each event means, and what it carries
|
||||
|
||||
| Event | What it indicates | Context carried | Never carried |
|
||||
|---|---|---|---|
|
||||
| Failed login | Repeated occurrences suggest an attack or a forgotten password | Timestamp, endpoint, correlation ID, whether the account exists | Password, the attempted password, full email |
|
||||
| Authorization denied | Someone reached an endpoint they lack rights for | Endpoint, required policy, role held, correlation ID | Token contents |
|
||||
| Master API key rejected | An attacker, **or** a genuine key-ring problem | Endpoint, calling host, correlation ID | The key, or any part of it |
|
||||
| Admin bypass rejected | Since FR-24, someone attempted the availability gate with an invalid token | Path, reason class (invalid signature, expired, wrong role), correlation ID | The token |
|
||||
| Rate limit triggered | Brute-force pressure on login | Limiter name, endpoint, correlation ID | Client identity beyond what the limiter partitions on |
|
||||
| Migration failure | Not a security event, but you want to know immediately | Exception, attempt count, correlation ID | Connection string, credentials |
|
||||
|
||||
**The two most diagnostically valuable are also the least obvious.** A rejected master API key is ambiguous by nature — it means either an intruder or that the key ring has become unreadable. Distinguishing them is exactly what the U2 durability work exists to make unnecessary, but if it ever happens, this event is the first sign. And a rejected admin bypass only became a meaningful signal *because* FR-24 started validating properly; before that, a forged token succeeded silently.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sentry Transport — Tunnel Through the API
|
||||
|
||||
Per Q1 = A, browser error reports do not go directly to Sentry.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
box rgba(246,224,94,0.4) Browser
|
||||
participant SPA as Admin SPA
|
||||
end
|
||||
box rgba(144,205,244,0.4) Application
|
||||
participant T as Tunnel endpoint
|
||||
end
|
||||
box rgba(251,182,206,0.4) External
|
||||
participant S as Sentry ingest
|
||||
end
|
||||
SPA->>T: POST envelope to same-origin tunnel path
|
||||
T->>T: validate size and content type
|
||||
T->>S: forward envelope to the configured DSN host
|
||||
S-->>T: accepted
|
||||
T-->>SPA: 200
|
||||
```
|
||||
|
||||
Text alternative: the admin SPA posts its Sentry envelope to a same-origin tunnel endpoint, which forwards it to Sentry's ingest host and returns success to the browser.
|
||||
|
||||
**Why a tunnel** — two reasons, one of them the actual motivation:
|
||||
1. **Ad blockers block requests to Sentry domains** with `ERR_BLOCKED_BY_CLIENT`. Without a tunnel, errors are lost precisely for the users who have an ad blocker — a silently biased sample of exactly the group most likely to have browser oddities.
|
||||
2. It keeps browser traffic same-origin, so U3's CSP needs `connect-src 'self'` and no external Sentry origin. Simpler policy, and one fewer thing to forget.
|
||||
|
||||
**The reference project tunnels through nginx.** NFR-01 forbids relying on server configuration, so here the application forwards it.
|
||||
|
||||
**Constraints on the tunnel**, because it is an anonymous endpoint that makes outbound requests on request:
|
||||
- Only forwards to the host derived from the configured DSN — never to a caller-supplied destination
|
||||
- Rejects payloads above a fixed size
|
||||
- Does nothing at all when no DSN is configured
|
||||
- Not on the availability bypass list: if the instance is switched off, error reporting from the admin SPA stopping is acceptable
|
||||
|
||||
**The backend's own Sentry reporting does not use the tunnel** — server-side code has no ad blocker and no CSP, and reports directly.
|
||||
|
||||
---
|
||||
|
||||
## 6. Sentry Request Context and Scrubbing
|
||||
|
||||
Per Q2 = B, `SendDefaultPii` is enabled with a filter. This needs care, because "PII" understates what is actually attached.
|
||||
|
||||
With `SendDefaultPii` on, Sentry includes request headers — and this application carries a `refreshToken` in a cookie and a master API key in a header. Both are **credentials**, not merely personal data. Sending them to a third party would be worse than the problem the setting solves.
|
||||
|
||||
The filter therefore removes, before any event leaves the process:
|
||||
|
||||
| Removed | Why |
|
||||
|---|---|
|
||||
| The entire `Cookie` header | Contains the `refreshToken`. Removing one cookie by rewriting the header is error-prone; removing the header is not |
|
||||
| `Authorization` header | Bearer token |
|
||||
| `X-Master-Api-Key` header | The master/slave shared secret. **Not mentioned when this was chosen, but the same class of secret** |
|
||||
| Request body | Login and password-change bodies contain passwords |
|
||||
|
||||
What remains and is genuinely useful: method, path, query string, user agent, IP address, authenticated username, and the correlation ID.
|
||||
|
||||
**Query strings are retained** — but note that invitation tokens travel as `?token=…` on `/api/v1/Invitation/validate`. That is a single-use, time-limited token rather than a standing credential, and the diagnostic value of seeing which endpoint was called outweighs it. Recorded so the decision is visible rather than accidental.
|
||||
|
||||
---
|
||||
|
||||
## 7. Frontend Configuration Resolution
|
||||
|
||||
Per FR-13, the API base URL becomes same-origin by default.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
read["Read VITE_API_BASE_URL"]
|
||||
empty{"Absent or empty ?"}
|
||||
same["Same-origin: use relative paths"]
|
||||
valid{"Valid absolute URL ?"}
|
||||
explicit["Use the explicit origin"]
|
||||
invalid["Development: warn loudly<br/>Production: use as given"]
|
||||
|
||||
read --> empty
|
||||
empty -->|yes| same
|
||||
empty -->|no| valid
|
||||
valid -->|yes| explicit
|
||||
valid -->|no| invalid
|
||||
|
||||
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef good fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef warn fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
class read entry;
|
||||
class empty,valid decision;
|
||||
class same,explicit good;
|
||||
class invalid warn;
|
||||
```
|
||||
|
||||
Text alternative: an absent or empty API base URL means same-origin relative requests; an explicit absolute URL is used as given; a malformed value warns loudly in development rather than being silently accepted.
|
||||
|
||||
**Why same-origin is the right default here**: in the single-host model the API is served by the same process as `/admin`, so a relative path always works and no CORS configuration is needed. The explicit form remains fully supported because local development runs the SPA on port 5173 against the API on 7221 (or 7222 for the slave) — that setup must keep working exactly as before.
|
||||
|
||||
**Malformed values are not silently accepted.** Validation is relaxed to permit an empty string, not to permit anything.
|
||||
|
||||
---
|
||||
|
||||
## 8. Umami Analytics
|
||||
|
||||
Per Q5 = A: measured in test and production, never locally.
|
||||
|
||||
| Condition | Behaviour |
|
||||
|---|---|
|
||||
| Local development | Script never loaded, regardless of configuration |
|
||||
| No website ID configured | Nothing rendered |
|
||||
| Website ID configured, test or production | Script loaded with the environment's own website ID |
|
||||
|
||||
Per-environment website IDs are why two separate frontend builds exist (D-15): the ID is a build-time value, so one bundle cannot carry both.
|
||||
|
||||
`Do Not Track` is deliberately **not** consulted (Q5 = A rather than B). Umami sets no cookies and collects no personal data, and the admin SPA's audience is a known set of operators — so honouring DNT would reduce data without protecting anyone. Recorded as a conscious choice.
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
# Business Rules — U4 Observability Integration
|
||||
|
||||
---
|
||||
|
||||
## Reporting Decision Logic
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
entry["Log entry or exception"]
|
||||
console["Write to console<br/>Information and above"]
|
||||
dsn{"Sentry DSN configured ?"}
|
||||
stop["Done: console only"]
|
||||
level{"Level is Warning or above ?"}
|
||||
crumb["Attach as breadcrumb<br/>context for a future event"]
|
||||
scrub["Scrub credentials from request context"]
|
||||
event["Send as Sentry event<br/>environment and release tagged"]
|
||||
|
||||
entry --> console
|
||||
console --> dsn
|
||||
dsn -->|no| stop
|
||||
dsn -->|yes| level
|
||||
level -->|no| crumb
|
||||
level -->|yes| scrub
|
||||
scrub --> event
|
||||
|
||||
classDef entrynode fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef step fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
classDef neutral fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000;
|
||||
class entry entrynode;
|
||||
class dsn,level decision;
|
||||
class console,crumb,scrub,event step;
|
||||
class stop neutral;
|
||||
```
|
||||
|
||||
Text alternative: everything goes to the console; with a DSN configured, informational entries become breadcrumbs while warnings and above become Sentry events, always after credentials are scrubbed from the request context.
|
||||
|
||||
---
|
||||
|
||||
## Logging Rules
|
||||
|
||||
| ID | Rule |
|
||||
|---|---|
|
||||
| **BR-U4-01** | Structured console logging is always active, independent of Sentry. |
|
||||
| **BR-U4-02** | Console threshold is `Information`, including framework categories. |
|
||||
| **BR-U4-03** | Every log entry carries a correlation identifier. |
|
||||
| **BR-U4-04** | Logging is registered **before** Sentry, so a Sentry initialisation problem is itself logged. |
|
||||
| **BR-U4-05** | No log entry may contain a password, token, API key, connection string or cookie value. |
|
||||
| **BR-U4-06** | Sentry **event** threshold is `Warning` and above. |
|
||||
| **BR-U4-07** | Sentry **breadcrumb** threshold is `Information`, so events arrive with the trail that led to them. |
|
||||
|
||||
**Rationale for BR-U4-06 and BR-U4-07 — reconciling two answers rather than choosing between them**: Q3 = C asked for `Information` everywhere, and Q16 = C asked for structured logging to Sentry beyond exceptions. Taken literally together, every framework `Information` entry would become a Sentry event — an event per request, exhausting the free plan within hours and burying real errors in request noise.
|
||||
|
||||
Splitting the thresholds honours both: the console gets everything (Q3 = C), and Sentry gets warnings and errors — still more than exceptions, as Q16 = C requires — each carrying its informational breadcrumbs.
|
||||
|
||||
---
|
||||
|
||||
## Sentry Rules
|
||||
|
||||
| ID | Rule |
|
||||
|---|---|
|
||||
| **BR-U4-08** | An absent DSN is a normal, supported state. Sentry initialisation is skipped and console logging continues. |
|
||||
| **BR-U4-09** | An unreachable Sentry never blocks or fails a request. |
|
||||
| **BR-U4-10** | Every event is tagged with the environment and the release. |
|
||||
| **BR-U4-11** | One Sentry project serves both backend and frontend, and both environments, distinguished by tags. |
|
||||
| **BR-U4-12** | Request context is included, with credentials scrubbed per BR-U4-13. |
|
||||
| **BR-U4-13** | Before any event leaves the process, these are removed: the entire `Cookie` header, the `Authorization` header, the `X-Master-Api-Key` header, and the request body. |
|
||||
| **BR-U4-14** | Scrubbing happens in-process, before transmission — never relying on a server-side setting in Sentry. |
|
||||
|
||||
**Rationale for BR-U4-13 — this list is longer than the question implied.** Enabling `SendDefaultPii` attaches request headers, and this application carries two standing credentials in headers: the `refreshToken` cookie and the `X-Master-Api-Key` used by the master/slave protocol. The API key was not mentioned when the setting was chosen, but it is the same class of secret and would otherwise be sent to a third party on every error raised during a master/slave call.
|
||||
|
||||
The request body is removed because the login and password-change endpoints carry passwords in it.
|
||||
|
||||
**Rationale for BR-U4-14**: server-side scrubbing means the secret already left the building. Doing it in-process is the only version that actually protects anything.
|
||||
|
||||
**What is deliberately retained**: method, path, query string, user agent, IP address, authenticated username and correlation ID. Note that invitation tokens travel as `?token=…` on one endpoint — a single-use, time-limited token rather than a standing credential, and the diagnostic value of the path and query outweighs it. Recorded so the trade-off is visible rather than accidental.
|
||||
|
||||
---
|
||||
|
||||
## Sentry Tunnel Rules
|
||||
|
||||
| ID | Rule |
|
||||
|---|---|
|
||||
| **BR-U4-15** | The browser sends Sentry envelopes to a same-origin tunnel endpoint, not directly to Sentry. |
|
||||
| **BR-U4-16** | The tunnel forwards **only** to the host derived from the configured DSN. A caller-supplied destination is never honoured. |
|
||||
| **BR-U4-17** | The tunnel rejects payloads above a fixed maximum size. |
|
||||
| **BR-U4-18** | With no DSN configured, the tunnel accepts nothing and does nothing. |
|
||||
| **BR-U4-19** | The tunnel is anonymous — error reports must work for a user whose session just expired. |
|
||||
| **BR-U4-20** | The tunnel is **not** on the availability bypass list. |
|
||||
| **BR-U4-21** | The backend's own Sentry reporting bypasses the tunnel and reports directly. |
|
||||
|
||||
**Rationale for BR-U4-16 — the rule that keeps this endpoint from being a liability**: an anonymous endpoint that makes an outbound request on demand is a server-side request forgery primitive if the destination comes from the caller. Deriving the destination solely from configuration removes that entirely.
|
||||
|
||||
**Rationale for BR-U4-19 and BR-U4-20 together**: the tunnel must be anonymous, because the errors most worth capturing include authentication failures. But it need not survive the instance being switched off — if the CMS is deliberately disabled, losing admin-SPA error reports is acceptable, and keeping it off the bypass list means one less anonymous, outbound-capable endpoint reachable on a disabled instance.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Configuration Rules
|
||||
|
||||
| ID | Rule |
|
||||
|---|---|
|
||||
| **BR-U4-22** | An absent or empty `VITE_API_BASE_URL` resolves to same-origin: requests use relative paths. |
|
||||
| **BR-U4-23** | An explicit absolute URL is used as supplied. |
|
||||
| **BR-U4-24** | Validation accepts an empty string **or** a valid absolute URL — nothing else. A malformed value is not silently accepted. |
|
||||
| **BR-U4-25** | Local development against `https://localhost:7221` (master) and `:7222` (slave) must keep working unchanged. |
|
||||
| **BR-U4-26** | Frontend Sentry initialisation is skipped when no DSN is configured. |
|
||||
| **BR-U4-27** | The Umami script is never loaded in local development, regardless of configuration. |
|
||||
| **BR-U4-28** | With no Umami website ID configured, nothing is rendered. |
|
||||
| **BR-U4-29** | `Do Not Track` is not consulted. |
|
||||
|
||||
**Rationale for BR-U4-24**: relaxing validation to allow an empty value is not the same as removing validation. A typo such as `htp://localhost:7221` must still be caught, or the SPA silently issues requests to a nonexistent origin.
|
||||
|
||||
**Rationale for BR-U4-29** (Q5 = A): Umami sets no cookies and collects no personal data, and the admin SPA's audience is a known set of operators. Honouring DNT would reduce data without protecting anyone. A conscious choice rather than an omission.
|
||||
|
||||
---
|
||||
|
||||
## Error and Edge-Case Scenarios
|
||||
|
||||
| Scenario | Expected behaviour |
|
||||
|---|---|
|
||||
| No DSN, application runs normally | Console logging only. No error, no warning about the absence |
|
||||
| DSN configured, Sentry unreachable | Sentry buffers and eventually drops. No request fails |
|
||||
| Exception during a master/slave call | Event sent; `X-Master-Api-Key` scrubbed |
|
||||
| Failed login | Event at warning level; no password, no attempted password |
|
||||
| Startup migration fails | Event sent, then the process does not start. The event must be delivered before exit |
|
||||
| Sentry initialisation itself throws | Logged by the already-registered console logger; the application continues without Sentry |
|
||||
| Browser posts to the tunnel with no DSN configured | Rejected; nothing forwarded |
|
||||
| Browser posts an oversized payload to the tunnel | Rejected |
|
||||
| Instance availability-disabled, browser posts to the tunnel | `503` from the gate. Accepted loss |
|
||||
| Ad blocker active | The tunnel is same-origin, so reports arrive — the reason it exists |
|
||||
| `VITE_API_BASE_URL` unset in a production build | Same-origin. The intended production configuration |
|
||||
| `VITE_API_BASE_URL` set to `https://localhost:7221` locally | Used as given; local development unchanged |
|
||||
| `VITE_API_BASE_URL` set to `htp://typo` | Development: loud warning. Production: used as given, and the requests visibly fail |
|
||||
| Umami configured but its origin missing from the CSP | Script blocked by the browser; U3's startup warning (BR-U3-22) flags the misconfiguration |
|
||||
| Local development with a Umami ID configured | Script not loaded |
|
||||
|
||||
---
|
||||
|
||||
## Security Compliance for U4
|
||||
|
||||
| Rule | Status | Notes |
|
||||
|---|---|---|
|
||||
| SECURITY-03 | **Compliant** | Structured logging with a correlation ID on every entry (BR-U4-03); credentials and PII excluded by BR-U4-05 and BR-U4-13; centralised destination via Sentry |
|
||||
| SECURITY-11 | Compliant | The tunnel's fixed destination (BR-U4-16) prevents it becoming a request-forgery primitive |
|
||||
| SECURITY-13 | Compliant | The Umami script is external and constrained by U3's CSP; SRI applied where the provider supports it |
|
||||
| SECURITY-14 | **Addressed, with DEV-01** | Six alertable event types emitted (BR-U4-01 group); alert rules configured in Operations. Retention remains the accepted deviation — Sentry's plan retains roughly 30 days against the 90 the rule asks for |
|
||||
| SECURITY-15 | Compliant | Observability failures never propagate into request handling (BR-U4-09) |
|
||||
|
||||
**No new deviation.** DEV-01 already covers the retention shortfall; nothing here introduces another.
|
||||
|
||||
**One risk closed that was not in the original scope**: BR-U4-13 adds `X-Master-Api-Key` to the scrub list. Without it, enabling `SendDefaultPii` would have sent the master/slave shared secret to a third-party service on every error raised during a master/slave call.
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
# Domain Entities — U4 Observability Integration
|
||||
|
||||
**No persisted entity.** U4 adds no table, no migration and no database column. It adds two configuration sections, one anonymous endpoint, and build-time frontend values.
|
||||
|
||||
---
|
||||
|
||||
## Concept Relationships
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
obsconfig["Observability configuration<br/>backend"]
|
||||
dsn["Sentry DSN"]
|
||||
env["Environment name"]
|
||||
release["Release identifier"]
|
||||
logging["Structured logger"]
|
||||
corr["Correlation identifier"]
|
||||
scrubber["Credential scrubber"]
|
||||
sentrybe["Sentry client<br/>backend"]
|
||||
tunnel["Tunnel endpoint"]
|
||||
vite["Vite build-time values<br/>frontend"]
|
||||
sentryfe["Sentry client<br/>frontend"]
|
||||
umami["Umami script component"]
|
||||
apicfg["API base URL resolution"]
|
||||
|
||||
obsconfig --> dsn
|
||||
obsconfig --> env
|
||||
obsconfig --> release
|
||||
dsn --> sentrybe
|
||||
dsn --> tunnel
|
||||
env --> sentrybe
|
||||
release --> sentrybe
|
||||
logging --> corr
|
||||
logging --> sentrybe
|
||||
scrubber -->|"filters before send"| sentrybe
|
||||
vite --> sentryfe
|
||||
vite --> umami
|
||||
vite --> apicfg
|
||||
sentryfe -->|"posts envelopes to"| tunnel
|
||||
tunnel -->|"forwards to DSN host only"| sentrybe
|
||||
|
||||
classDef cfg fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef backend fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef frontend fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000;
|
||||
classDef guard fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
class obsconfig,dsn,env,release,vite cfg;
|
||||
class logging,corr,sentrybe,tunnel backend;
|
||||
class sentryfe,umami,apicfg frontend;
|
||||
class scrubber guard;
|
||||
```
|
||||
|
||||
Text alternative: backend configuration supplies the Sentry DSN, environment and release; the structured logger attaches a correlation identifier and feeds Sentry through a credential scrubber; the frontend reads build-time values and routes its error envelopes through the same-origin tunnel, which forwards only to the configured DSN host.
|
||||
|
||||
---
|
||||
|
||||
## Backend configuration — `Observability` section
|
||||
|
||||
| Field | Type | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `SentryDsn` | string | empty | Absent means Sentry is skipped entirely (BR-U4-08) |
|
||||
| `Environment` | string | falls back to `ASPNETCORE_ENVIRONMENT` | Tag distinguishing test from production |
|
||||
| `TunnelMaxPayloadBytes` | int | fixed default | Upper bound enforced by the tunnel (BR-U4-17) |
|
||||
| `TracesSampleRate` | double | conservative default | Performance sampling; kept low so the free plan is not exhausted |
|
||||
|
||||
**Not configurable, deliberately**: the scrub list (BR-U4-13) and the tunnel's destination host (BR-U4-16). Both are security-critical, and making either configurable would create a way to switch the protection off — the scrub list by omission, the destination by turning the endpoint into a request-forgery primitive.
|
||||
|
||||
**Secret handling**: a Sentry DSN is not a secret in the usual sense — it identifies a project and permits event submission, and the frontend's copy is visible in the page source. It still comes from environment variables in production, consistent with D-16, rather than being committed.
|
||||
|
||||
---
|
||||
|
||||
## Correlation identifier
|
||||
|
||||
| Aspect | Detail |
|
||||
|---|---|
|
||||
| Purpose | Ties every log entry and Sentry event from one request together (SECURITY-03) |
|
||||
| Scope | One request |
|
||||
| Presence | On **every** log entry, not only on errors |
|
||||
| Mechanism | **Not fixed here** — `HttpContext.TraceIdentifier` versus W3C `traceparent` is OPEN-01, decided in this unit's NFR Design |
|
||||
|
||||
Recorded as an entity because the choice affects the shape of every log entry, and NFR Design will settle it rather than leaving it to code generation.
|
||||
|
||||
---
|
||||
|
||||
## Credential scrubber
|
||||
|
||||
Not persisted; a filter applied to every outbound Sentry event.
|
||||
|
||||
| Removed | Reason |
|
||||
|---|---|
|
||||
| `Cookie` header, entire | Carries the `refreshToken`. Removing one cookie by rewriting the header is error-prone |
|
||||
| `Authorization` header | Bearer token |
|
||||
| `X-Master-Api-Key` header | Master/slave shared secret |
|
||||
| Request body | Login and password-change bodies carry passwords |
|
||||
|
||||
| Retained | Reason |
|
||||
|---|---|
|
||||
| Method, path, query string | Diagnostic value. Note the invitation-token caveat below |
|
||||
| User agent | Browser-specific failures |
|
||||
| IP address | Attack-pattern recognition |
|
||||
| Authenticated username | Whose session hit the problem |
|
||||
| Correlation identifier | Ties the event to its log entries |
|
||||
|
||||
**Invitation-token caveat**: `/api/v1/Invitation/validate?token=…` puts a token in the query string, which is retained. It is single-use and time-limited rather than a standing credential, and the value of knowing which endpoint was called outweighs it. Documented so the trade-off is visible.
|
||||
|
||||
---
|
||||
|
||||
## Security event types
|
||||
|
||||
Six event shapes, not persisted — emitted as structured log entries that also become Sentry events.
|
||||
|
||||
| Event | Level | Context |
|
||||
|---|---|---|
|
||||
| Failed login | Warning | Endpoint, whether the account exists, correlation ID |
|
||||
| Authorization denied | Warning | Endpoint, required policy, role held, correlation ID |
|
||||
| Master API key rejected | Warning | Endpoint, calling host, correlation ID |
|
||||
| Admin bypass rejected | Warning | Path, rejection reason class, correlation ID |
|
||||
| Rate limit triggered | Warning | Limiter name, endpoint, correlation ID |
|
||||
| Migration failure | Critical | Exception, attempt count, correlation ID |
|
||||
|
||||
All six sit at `Warning` or above, so they cross the Sentry event threshold (BR-U4-06) by construction rather than by coincidence.
|
||||
|
||||
**Rejection reason classes** for the admin bypass — `InvalidSignature`, `Expired`, `WrongIssuer`, `NotAdmin`, `Malformed` — never the token itself. The distinction matters: `InvalidSignature` suggests forgery, while `Expired` is usually an administrator with a stale tab.
|
||||
|
||||
---
|
||||
|
||||
## Frontend build-time values
|
||||
|
||||
| Variable | Purpose | Absent behaviour |
|
||||
|---|---|---|
|
||||
| `VITE_API_BASE_URL` | API origin | Same-origin (BR-U4-22) |
|
||||
| `VITE_SENTRY_DSN` | Frontend Sentry project | Sentry skipped (BR-U4-26) |
|
||||
| `VITE_APP_ENV` | Environment tag | Untagged |
|
||||
| `VITE_UMAMI_SCRIPT_URL` | Umami script origin | Umami not loaded |
|
||||
| `VITE_UMAMI_WEBSITE_ID` | Per-environment website ID | Nothing rendered (BR-U4-28) |
|
||||
| `VITE_APP_TITLE` | Existing; unchanged | Defaults to `SlpModularCms` |
|
||||
|
||||
**Why these force two build artifacts** (D-15): Vite bakes them into the bundle at build time, so one `dist/` cannot carry both the test and production website IDs or environment tags. The CI workflow therefore produces one artifact per environment.
|
||||
|
||||
### Frontend config model change
|
||||
|
||||
`AppConfig.apiBaseUrl` gains one new legal value: the empty string, meaning same-origin. Its Zod schema becomes "empty string **or** valid absolute URL" — relaxed by exactly one case, not loosened to accept anything (BR-U4-24).
|
||||
|
||||
---
|
||||
|
||||
## Persistence Summary
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| New tables? | None |
|
||||
| New migrations? | None |
|
||||
| New configuration sections? | One backend section (`Observability`); frontend variables are build-time |
|
||||
| New endpoints? | One — the anonymous Sentry tunnel |
|
||||
| Secrets stored? | None. The DSN is not a standing credential and comes from environment variables |
|
||||
| Data sent to third parties? | Error events to Sentry, page views to self-hosted Umami. Credentials scrubbed per BR-U4-13 |
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
# Frontend Components — U4 Observability Integration
|
||||
|
||||
Changes to the admin SPA in `frontend/`. Two new components, two modified files.
|
||||
|
||||
---
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
main["main.tsx<br/>entry point"]
|
||||
sentryinit["initSentry<br/>called before render"]
|
||||
config["lib/config.ts<br/>getAppConfig"]
|
||||
apiclient["lib/api-client.ts<br/>ApiClient"]
|
||||
query["QueryClientProvider"]
|
||||
authprov["AuthProvider"]
|
||||
errbound["SentryErrorBoundary<br/>NEW"]
|
||||
inner["InnerApp"]
|
||||
umami["UmamiAnalytics<br/>NEW"]
|
||||
router["RouterProvider"]
|
||||
toaster["Toaster"]
|
||||
|
||||
main --> sentryinit
|
||||
main --> config
|
||||
sentryinit --> config
|
||||
config --> apiclient
|
||||
main --> query
|
||||
query --> authprov
|
||||
authprov --> errbound
|
||||
errbound --> inner
|
||||
errbound --> umami
|
||||
inner --> router
|
||||
authprov --> toaster
|
||||
|
||||
classDef root fill:#4CAF50,stroke:#2e7d32,color:#000;
|
||||
classDef guard fill:#FF9800,stroke:#e65100,color:#000;
|
||||
classDef page fill:#2196F3,stroke:#0d47a1,color:#000;
|
||||
classDef hook fill:#9C27B0,stroke:#4a148c,color:#000;
|
||||
classDef newcomp fill:#9ae6b4,stroke:#2f855a,color:#000;
|
||||
class main root;
|
||||
class errbound,authprov guard;
|
||||
class inner,router,toaster page;
|
||||
class config,apiclient,sentryinit hook;
|
||||
class umami newcomp;
|
||||
```
|
||||
|
||||
Text alternative: Sentry initialises before rendering, a new error boundary wraps the application inside the auth provider, and a new Umami component sits alongside the app tree; the existing config module now also feeds Sentry initialisation.
|
||||
|
||||
---
|
||||
|
||||
## New Component — `SentryErrorBoundary`
|
||||
|
||||
**Location**: `frontend/src/components/SentryErrorBoundary.tsx`
|
||||
|
||||
| Aspect | Detail |
|
||||
|---|---|
|
||||
| Purpose | Catch render-time React errors that would otherwise blank the screen, report them, and show a recoverable fallback |
|
||||
| Props | `children: ReactNode` |
|
||||
| State | Held by Sentry's own boundary implementation |
|
||||
| Placement | **Inside** `AuthProvider`, **outside** `InnerApp` |
|
||||
| Behaviour without a DSN | Still catches and still shows the fallback; simply reports nothing |
|
||||
| `data-testid` | `error-boundary-fallback`, `error-boundary-retry-button` |
|
||||
|
||||
**Why inside `AuthProvider` rather than outermost**: the fallback needs to be reachable for a logged-in user, and an error inside a page should not tear down the session context — otherwise recovering from a render error would also log the user out.
|
||||
|
||||
### Fallback content rules
|
||||
|
||||
| Must | Must not |
|
||||
|---|---|
|
||||
| State that something went wrong | Show the exception message |
|
||||
| Offer a retry that remounts the subtree | Show a stack trace |
|
||||
| Offer a link to the dashboard | Show a Sentry event ID as the primary content |
|
||||
|
||||
Exception text frequently contains internal detail; showing it to an operator is both unhelpful and a small information leak (SECURITY-09).
|
||||
|
||||
---
|
||||
|
||||
## New Component — `UmamiAnalytics`
|
||||
|
||||
**Location**: `frontend/src/components/UmamiAnalytics.tsx`
|
||||
|
||||
| Aspect | Detail |
|
||||
|---|---|
|
||||
| Purpose | Inject the Umami tracking script when configured |
|
||||
| Props | None — reads configuration directly |
|
||||
| Renders | Nothing visible |
|
||||
| `data-testid` | Not applicable — no interactive element |
|
||||
|
||||
### Behaviour
|
||||
|
||||
| Condition | Result |
|
||||
|---|---|
|
||||
| `import.meta.env.DEV` | Script **never** injected (BR-U4-27) |
|
||||
| Script URL or website ID absent | Nothing injected (BR-U4-28) |
|
||||
| Both present, not local | Script injected once with the website ID |
|
||||
| Component re-renders | Script injected **once** — guarded against duplicates |
|
||||
| `Do Not Track` set | Ignored; script still injected (BR-U4-29) |
|
||||
|
||||
**Why a component rather than a tag in `index.html`**: the website ID is a build-time variable, and `index.html` cannot read `import.meta.env`. A component also makes the "never in development" and "once only" rules testable.
|
||||
|
||||
---
|
||||
|
||||
## Modified — `frontend/src/lib/config.ts`
|
||||
|
||||
| Change | Detail |
|
||||
|---|---|
|
||||
| `apiBaseUrl` | An absent or empty `VITE_API_BASE_URL` now resolves to `''`, meaning same-origin |
|
||||
| Zod schema | Accepts an empty string **or** a valid absolute URL — nothing else (BR-U4-24) |
|
||||
| New fields | `sentryDsn`, `appEnv`, `umamiScriptUrl`, `umamiWebsiteId` |
|
||||
| Existing behaviour | An explicit absolute URL still works unchanged, so local development against `:7221` and `:7222` is unaffected |
|
||||
|
||||
**The validation is relaxed by exactly one case, not removed.** A value like `htp://localhost:7221` must still be caught, or the SPA silently issues requests to a nonexistent origin — a failure that looks like the API being down.
|
||||
|
||||
---
|
||||
|
||||
## Modified — `frontend/src/main.tsx`
|
||||
|
||||
| Change | Order |
|
||||
|---|---|
|
||||
| Call `initSentry()` | **First**, before the query client and before render — so an error during startup is still captured |
|
||||
| Wrap the tree in `SentryErrorBoundary` | Inside `AuthProvider` |
|
||||
| Render `UmamiAnalytics` | Alongside `InnerApp` |
|
||||
| Existing `document.title` and MSW logic | Unchanged |
|
||||
|
||||
---
|
||||
|
||||
## Sentry Initialisation
|
||||
|
||||
**Location**: `frontend/src/lib/sentry.ts` (new)
|
||||
|
||||
| Aspect | Detail |
|
||||
|---|---|
|
||||
| Skipped when | No DSN configured (BR-U4-26) |
|
||||
| `environment` | From `VITE_APP_ENV` |
|
||||
| `release` | From the package version, matching the existing `__APP_VERSION__` pattern in the reference project |
|
||||
| `tunnel` | Same-origin tunnel path — **not** Sentry's ingest URL |
|
||||
| `sendDefaultPii` | `false` on the frontend |
|
||||
|
||||
**Why `sendDefaultPii` is false here even though the backend enables it with scrubbing**: the backend can scrub in-process before transmission because it controls the send. In the browser there is no equivalent guarantee, and the frontend has nothing to add that the backend cannot already report. There is no reason to accept the risk.
|
||||
|
||||
**Why the tunnel matters most on the frontend**: ad blockers block requests to Sentry domains, so without the tunnel the admin SPA loses errors precisely for users who have one.
|
||||
|
||||
---
|
||||
|
||||
## User Interaction Flows
|
||||
|
||||
### Render error recovery
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
render["Page renders"]
|
||||
err["Component throws"]
|
||||
catch["SentryErrorBoundary catches"]
|
||||
report{"DSN configured ?"}
|
||||
send["Report via the tunnel"]
|
||||
skip["No report"]
|
||||
fallback["Show fallback:<br/>message, retry, dashboard link"]
|
||||
retry["User clicks retry"]
|
||||
remount["Subtree remounts;<br/>session preserved"]
|
||||
|
||||
render --> err
|
||||
err --> catch
|
||||
catch --> report
|
||||
report -->|yes| send
|
||||
report -->|no| skip
|
||||
send --> fallback
|
||||
skip --> fallback
|
||||
fallback --> retry
|
||||
retry --> remount
|
||||
|
||||
classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
|
||||
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
|
||||
classDef step fill:#2196F3,stroke:#0d47a1,color:#000;
|
||||
classDef good fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
|
||||
class render,err entry;
|
||||
class report decision;
|
||||
class catch,send,skip,fallback step;
|
||||
class retry,remount good;
|
||||
```
|
||||
|
||||
Text alternative: a component error is caught by the boundary, reported through the tunnel when a DSN is configured, and shown as a recoverable fallback whose retry remounts the subtree while preserving the session.
|
||||
|
||||
---
|
||||
|
||||
## Form Validation Rules
|
||||
|
||||
U4 adds no form. Existing validation via `react-hook-form` and Zod is unchanged.
|
||||
|
||||
One indirect effect worth noting: existing form submission errors surface as `ProblemDetailsError` from `ApiClient`. Those are **handled** errors, already shown to the user, and must **not** become Sentry events — otherwise every validation failure a user makes becomes an alert. Only unhandled errors and `NetworkError` are reported.
|
||||
|
||||
---
|
||||
|
||||
## API Integration Points
|
||||
|
||||
| Component | Endpoint | Notes |
|
||||
|---|---|---|
|
||||
| `ApiClient` | `/api/v1/**` | Now same-origin by default (BR-U4-22) |
|
||||
| Sentry client | Same-origin tunnel path | New. Not an `/api/v1` route, so no version prefix |
|
||||
| `UmamiAnalytics` | Umami script origin | External; must be permitted by U3's CSP `script-src` |
|
||||
|
||||
---
|
||||
|
||||
## Testing Approach
|
||||
|
||||
| Component | Assertions |
|
||||
|---|---|
|
||||
| `config.ts` | Empty value resolves to same-origin; explicit URL preserved; malformed value rejected |
|
||||
| `UmamiAnalytics` | Nothing injected in development; nothing without a website ID; injected once when configured; not injected twice on re-render |
|
||||
| `SentryErrorBoundary` | Fallback shown on a child throw; fallback contains no exception text; retry remounts; works without a DSN |
|
||||
| Sentry initialisation | Skipped without a DSN; tunnel option set rather than a direct ingest URL |
|
||||
|
||||
All use the existing Vitest, Testing Library and MSW setup. Note that `pnpm run lint` is still failing for pre-existing reasons until U5 — so lint should be run on the **changed files** during this unit, to avoid new violations hiding among the five existing ones.
|
||||
Reference in New Issue
Block a user