Prepares the single-host layout for deployment. The customer's public website moves from wwwroot/ to wwwroot/web/, so a CMS deploy can no longer overwrite content it does not own: with the website in its own directory, the release directory can be swapped without touching it. Each front-end gets its own file provider, and both tolerate a missing directory at startup — a fresh deployment has no website until a separate workspace deploys one, and the CMS must still serve /admin and the API. When the website's index.html is absent, an embedded placeholder is served instead of a 404, which also doubles as proof the CMS itself is running. The placeholder is embedded in the assembly rather than shipped into wwwroot/web/, because that directory is owned and overwritten by the website workspace. Adds GET /health for uptime monitoring. It reports infrastructure liveness only and is deliberately NOT the same thing as /api/v1/Availability/status or /api/v1/System/capabilities: those are CMS domain state that also serve the master/slave protocol. A healthy instance can be switched off by design, and a switched-on instance can be unhealthy, so conflating them would alert on business state and stay silent on real outages. /health is on the availability gate's bypass list for the same reason. Fixes a real defect in the gate's admin bypass. It parsed the bearer token with ReadJwtToken, which reads claims without verifying the signature, so an unauthenticated caller could forge an unsigned token carrying an Owner role claim and bypass the gate that suspends a customer's site. Protected endpoints still rejected them, so nothing leaked — but the gate itself was bypassable. The token is now fully validated against the same parameters as the bearer scheme, resolved from one shared source so the two cannot drift apart. Host wiring for these changes lands with the data-durability commit, since both units touch the same lines of Program.cs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
5.9 KiB
Domain Entities — U1 Hosting & Serving
Note: U1 introduces no persisted entity. It adds no table, no migration and no database column. Its "domain" consists of in-memory configuration descriptors and a response model. They are documented here because they are the data structures the unit's logic operates on, and because reviewers should be able to confirm that nothing is being persisted.
Concept Relationships
graph TD
host["Host application"]
mountweb["StaticMount: website<br/>request path /"]
mountadmin["StaticMount: admin SPA<br/>request path /admin"]
provider["File provider<br/>per mount"]
fallbackweb["SPA fallback: website"]
fallbackadmin["SPA fallback: admin"]
placeholder["Placeholder page<br/>embedded resource"]
report["HealthReport<br/>response model"]
orchestrator["ModuleOrchestrator<br/>existing"]
bypass["Bypass prefix list<br/>existing, extended"]
tokenparams["Token validation parameters<br/>existing, now shared"]
host -->|"registers"| mountweb
host -->|"registers"| mountadmin
mountweb -->|"resolves files via"| provider
mountadmin -->|"resolves files via"| provider
mountweb -->|"falls back to"| fallbackweb
mountadmin -->|"falls back to"| fallbackadmin
fallbackweb -->|"substitutes when index absent"| placeholder
host -->|"exposes"| report
report -->|"reads module names from"| orchestrator
host -->|"configures"| bypass
host -->|"shares"| tokenparams
bypass -->|"used by availability gate"| tokenparams
classDef hostnode fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef mount fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef model fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
classDef existing fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000;
class host hostnode;
class mountweb,mountadmin,provider,fallbackweb,fallbackadmin,placeholder mount;
class report model;
class orchestrator,bypass,tokenparams existing;
Text alternative: the host registers two independent static mounts each with its own file provider and SPA fallback, plus a health response model that reads module names from the existing orchestrator; the bypass list and token validation parameters are existing structures this unit extends and shares.
Static Mount Descriptor (configuration, in-memory)
Not a class to be persisted — this describes what each UseStaticFiles registration is configured with.
| Field | Website mount | Admin mount |
|---|---|---|
| Physical root | {contentRoot}/wwwroot/web |
{contentRoot}/wwwroot/admin |
| Request path | "" (root) |
/admin |
| Default file | index.html |
index.html |
| Directory browsing | Disabled | Disabled |
| Tolerates missing root | Yes — logs a warning | Yes — logs a warning |
| Registration order | Second | First |
Registration order matters: the admin mount must be registered first, or /admin/... would be resolved against the website root.
HealthReport (response model, not persisted)
| Field | Type | Purpose |
|---|---|---|
status |
string | "Healthy". A running process always reports healthy; absence of a response is the unhealthy signal |
timestamp |
timestamp with offset | When the report was produced, so a cached response is recognisable |
version |
string | The application's informational version, so a deploy can be confirmed without host access |
modules |
string array | Names of modules loaded by ModuleOrchestrator |
Validation and constraints:
- Every field is derived from in-process state. No field may require a database query, file read or network call (BR-U1-15).
- No field may contain configuration values, paths, connection details or environment variable contents (BR-U1-18).
modulesis read from the existingModuleOrchestrator.ModuleNames, which is already exposed anonymously by/api/v1/System/capabilities— so this field introduces no new disclosure.
Placeholder Page (embedded static content)
| Property | Value |
|---|---|
| Storage | Embedded resource in the assembly, not a file in wwwroot |
| Served when | The website fallback is needed and wwwroot/web/index.html is absent |
| Status code | 200 |
| Content | A statement that no website has been deployed yet, the expected target path, and a link to /admin |
Why embedded rather than a file: a file in wwwroot/web/ would be inside the directory a website workspace owns and overwrites — it would be deleted by the first real website deployment, or worse, mistaken for part of the customer's site. Embedding keeps it outside that boundary entirely.
What it must not contain: no version, no environment name, no module list, no configuration. It is served anonymously to any visitor of the site root, which is a wider audience than /health.
Extended Existing Structures
Bypass prefix list (AvailabilityMiddleware)
| Aspect | Detail |
|---|---|
| Current contents | /api/v1/Availability/status, /api/v1/Auth/, /api/v1/Setup/status, /api/v1/master/, /api/v1/SlaveStatus |
| Added by U1 | /health |
| Matching | Case-insensitive prefix match, unchanged |
Shared token validation parameters
| Aspect | Detail |
|---|---|
| Currently | Configured once inside AddJwtBearer in ServiceCollectionExtensions.AddCoreInfrastructure |
| Change | Extracted so the same instance is used by both the bearer scheme and the availability gate |
| Constraint | Exactly one definition. Two copies could drift, and a gate more permissive than the scheme would silently re-open the hole FR-24 closes |
Persistence Summary
| Question | Answer |
|---|---|
| New tables? | None |
| New migrations? | None |
| New columns? | None |
| New configuration sections? | None — U1 adds no appsettings section |
| Anything written to disk at runtime? | No |
All persistence work in Round 1 belongs to U2, which adds the Data Protection keys table and the automatic Core migration.