# Business Rules — U1 Hosting & Serving --- ## Rule Categories ```mermaid graph TD start["Request or startup event"] cat1{"Path resolution ?"} cat2{"Availability gate ?"} cat3{"Health reporting ?"} cat4{"Startup validation ?"} r1["BR-U1-01 to BR-U1-08"] r2["BR-U1-09 to BR-U1-14"] r3["BR-U1-15 to BR-U1-19"] r4["BR-U1-20 to BR-U1-22"] start --> cat1 start --> cat2 start --> cat3 start --> cat4 cat1 --> r1 cat2 --> r2 cat3 --> r3 cat4 --> r4 classDef entry fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000; classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000; classDef rules fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000; class start entry; class cat1,cat2,cat3,cat4 decision; class r1,r2,r3,r4 rules; ``` Text alternative: rules divide into four groups — path resolution, availability gating, health reporting and startup validation. --- ## Path Resolution Rules | ID | Rule | |---|---| | **BR-U1-01** | The `/admin` mount is evaluated **before** the root mount. A path beginning `/admin` is never resolved against `wwwroot/web/`. | | **BR-U1-02** | `wwwroot/admin/` serves paths under `/admin`; `wwwroot/web/` serves paths under `/`. Neither mount may serve files from outside its own directory. | | **BR-U1-03** | A request for the exact path `/admin` (no trailing slash) returns a redirect to `/admin/`. Deeper paths are unaffected. | | **BR-U1-04** | A request whose path contains a file extension and matches no file returns `404`. It is never given an `index.html`. | | **BR-U1-05** | A request whose path contains no file extension and matches no file falls back to an `index.html`: `wwwroot/admin/index.html` when the path begins `/admin`, otherwise the website's. | | **BR-U1-06** | When the website fallback is required but `wwwroot/web/index.html` does not exist, the built-in placeholder page is returned with status `200`. | | **BR-U1-07** | Directory browsing is disabled on both mounts. A request for a directory path returns its default file or falls through to the fallback rules — never a file listing. | | **BR-U1-08** | Static-file responses short-circuit the pipeline. Any behaviour that must apply to them — notably security headers — must be registered before the static-file middleware. | **Rationale for BR-U1-04**: distinguishing "missing asset" from "client-side route" is what keeps a broken deployment visible. Without it, a missing JavaScript bundle would receive an HTML page, and the browser error would point at a parse failure rather than the real cause. --- ## Availability Gate Rules | ID | Rule | |---|---| | **BR-U1-09** | `/health` is on the bypass prefix list. The availability gate never blocks it. | | **BR-U1-10** | The existing bypass prefixes are retained unchanged: `/api/v1/Availability/status`, `/api/v1/Auth/`, `/api/v1/Setup/status`, `/api/v1/master/`, `/api/v1/SlaveStatus`. | | **BR-U1-11** | The admin bypass applies only when the bearer token **validates successfully** against the same `TokenValidationParameters` used by the JWT bearer scheme — signature, issuer, audience and lifetime. | | **BR-U1-12** | A token that fails validation for any reason grants no bypass. The request proceeds to normal availability evaluation as if no token were present. | | **BR-U1-13** | A validated token grants bypass only when it carries the role `Owner` or `Administrator`. | | **BR-U1-14** | Token validation failure is never itself an error response. The gate does not return `401`; that remains the responsibility of the authentication middleware on protected endpoints. | **Rationale for BR-U1-11 and BR-U1-12**: this is the FR-24 fix. Previously the token was parsed but not verified, so an unauthenticated caller could present a self-made token carrying an `Owner` claim and bypass the gate. Protected endpoints still rejected them, so no data was exposed — but the gate itself, the mechanism that suspends a customer's site, was bypassable by anyone who knew the claim name. **Rationale for BR-U1-14**: the gate's job is to decide whether to serve, not to authenticate. Returning `401` from the gate would change the response for anonymous endpoints that are legitimately reachable, such as `/api/v1/Setup/status`. **Single source for validation parameters**: the parameters must be resolved from one shared definition used by both the bearer scheme and the gate. Copying them would allow the two to drift, and a drift in which the gate is *more* permissive than the scheme silently re-opens the hole this rule closes. --- ## Health Reporting Rules | ID | Rule | |---|---| | **BR-U1-15** | `/health` performs no database call and probes no external dependency. Its answer is derived entirely from in-process state. | | **BR-U1-16** | A running process always answers `200`. The unhealthy signal is the **absence** of a response, produced by fail-fast startup (U2). | | **BR-U1-17** | The response body reports status, timestamp, application version and the names of loaded modules. | | **BR-U1-18** | `/health` is anonymous. It must never expose configuration values, connection strings, environment variable contents, file paths, or stack traces. | | **BR-U1-19** | `/health` is never presented as, aliased to, or documented as equivalent to `/api/v1/Availability/status` or `/api/v1/System/capabilities`. Those report CMS domain state; `/health` reports infrastructure liveness. | **Rationale for BR-U1-17**: the module list exists because `ModuleOrchestrator` logs rather than throws when a module fails to load. An instance can therefore start "successfully" with a missing capability, and NFR-06 requires that to be detectable after a deploy without host access. The version field serves the same purpose for the deploy itself — confirming which build is actually running. **Disclosure note**: module names are already publicly available from `/api/v1/System/capabilities`, which is anonymous, so BR-U1-17 adds no new disclosure there. The version field *is* new disclosure. It is accepted deliberately: verifying which build is live is the primary reason the endpoint exists, and the alternative — an authenticated health endpoint — would not work with UptimeRobot. Recorded as a conscious trade-off rather than an oversight. --- ## Startup Validation Rules | ID | Rule | |---|---| | **BR-U1-20** | A missing `wwwroot/web/` directory never prevents startup. | | **BR-U1-21** | A missing `wwwroot/web/` directory is logged as a **warning** at startup, including the absolute path that was expected. | | **BR-U1-22** | A missing `wwwroot/admin/` directory is logged as a warning but likewise does not prevent startup — it indicates a publish problem, not a reason to refuse traffic to `/api/v1`. | **Rationale for BR-U1-21**: normal on a fresh installation, alarming on a running production instance where it means the customer's website has disappeared. A warning is visible in Sentry and the console without cost in the normal case. --- ## Error and Edge-Case Scenarios | Scenario | Expected behaviour | |---|---| | Fresh install, no website deployed, visitor requests `/` | Placeholder page, `200` | | Fresh install, visitor requests `/admin` | Redirect to `/admin/`, then the admin SPA | | Website deployed but `index.html` missing | Placeholder page, `200`. The directory existing is not proof of a valid site | | Request for `/assets/app.js` that does not exist | `404`, never HTML | | Request for `/admin/assets/app.js` that does not exist | `404`, never HTML | | Request for `/some/client/route` with no extension | Website `index.html`, or placeholder if absent | | Request for `/admin/dashboard` | `wwwroot/admin/index.html` | | Instance disabled, request for `/health` | `200` with the health report — the gate does not apply | | Instance disabled, request for `/` where the website exists | The website is served — static files short-circuit before the gate | | Instance disabled, request for `/admin/dashboard` | `503 ProblemDetails` — the fallback is an endpoint, so the gate applies | | Instance disabled, valid Owner token | Bypass granted, request proceeds | | Instance disabled, forged unsigned token claiming Owner | **No bypass.** `503`. This is the FR-24 fix | | Instance disabled, expired but genuine Owner token | No bypass — lifetime validation is part of BR-U1-11 | | Instance disabled, no token | `503 ProblemDetails` | | Malformed `Authorization` header | Treated as no token; no exception surfaces to the caller | | Path traversal attempt, e.g. `/../appsettings.json` | Rejected by the file provider; never resolves outside its mount root | --- ## Security Compliance for U1 | Rule | Status | Notes | |---|---|---| | SECURITY-05 | Compliant | `/health` accepts no input. Path traversal is prevented by the file providers | | SECURITY-08 | **Improved** | BR-U1-11 to BR-U1-14 close the forged-token bypass. `/health` is deliberately anonymous and exposes no resource data | | SECURITY-09 | Compliant | Directory browsing disabled; BR-U1-18 forbids exposing internals; the version disclosure in BR-U1-17 is documented and justified | | SECURITY-15 | Compliant | The gate fails closed — a token that cannot be validated grants nothing |