# Component Methods Method signatures, purpose and input/output types. **Detailed business rules are defined per unit in Functional Design (CONSTRUCTION phase)** — this document establishes the interface contracts only. Signatures are indicative C# and may be refined during Code Generation, but the shape of each contract is a design decision recorded here. --- ## C-01 `SecurityHeadersMiddleware` ```csharp public sealed class SecurityHeadersMiddleware { public SecurityHeadersMiddleware(RequestDelegate next, IOptions options, CspPolicyBuilder policyBuilder); public Task InvokeAsync(HttpContext context); } ``` | Member | Purpose | Input | Output | |---|---|---|---| | `InvokeAsync` | Register a response-start callback that applies the appropriate headers, then continue the pipeline | `HttpContext` | `Task` | **Interface notes**: - Headers are applied through `HttpResponse.OnStarting`, **not** before calling `next`. The response content type is unknown until the response begins, and HTML-only headers (FU1 = A) cannot be decided without it. - The CSP policy for the request path is resolved once per request, before the callback, so path matching does not run at response-start time. - Existing headers are never overwritten — a downstream component that deliberately set one wins. --- ## C-02 `SecurityHeadersOptions` ```csharp public sealed class SecurityHeadersOptions { public List PathPolicies { get; set; } = new(); public string DefaultPolicy { get; set; } = "Relaxed"; public List AllowedScriptOrigins { get; set; } = new(); public List AllowedConnectOrigins { get; set; } = new(); public bool Enabled { get; set; } = true; } public sealed class PathPolicyRule { public string PathPrefix { get; set; } = string.Empty; public string Policy { get; set; } = string.Empty; } ``` | Member | Purpose | |---|---| | `PathPolicies` | Ordered path-prefix to policy-name assignment. Configuration, so paths can be added without code changes (Q6 = B) | | `DefaultPolicy` | Policy applied when no prefix matches — `Relaxed`, covering the public website | | `AllowedScriptOrigins` | Origins added to the CSP `script-src` directive — the Umami script host | | `AllowedConnectOrigins` | Origins added to `connect-src` — the Sentry ingest host | | `Enabled` | Escape hatch for local development or diagnosis | **Design rule (FU2 = A)**: policy *definitions* are in code; only *assignment* and environment-specific *origins* are configuration. A misconfiguration can therefore misroute a path but cannot invent a broken policy. --- ## C-03 `CspPolicyBuilder` ```csharp public sealed class CspPolicyBuilder { public CspPolicyBuilder(IOptions options); public string Build(string policyName); public string ResolvePolicyName(PathString path); } ``` | Method | Purpose | Input | Output | |---|---|---|---| | `Build` | Compose the full CSP header value for a named policy, injecting configured origins | `string policyName` | `string` — the header value | | `ResolvePolicyName` | Determine which policy applies to a request path by prefix match, falling back to `DefaultPolicy` | `PathString` | `string` — policy name | **Interface notes**: - Policy strings are built **once at startup** and cached by name; `Build` returns the cached value. Composing a CSP per request would be wasteful on a static-file-heavy workload. - Two policies are defined in code: `Strict` (baseline `default-src 'self'`) and `Relaxed` (permissive enough that a website author who never saw this repository is not broken by it — D-31). - An unknown policy name is a configuration error and must fail at startup, not silently fall back. --- ## C-04 Health-check registration ```csharp public static IServiceCollection AddCmsHealthChecks(this IServiceCollection services); public static IEndpointRouteBuilder MapCmsHealthChecks(this IEndpointRouteBuilder endpoints); ``` | Method | Purpose | Input | Output | |---|---|---|---| | `AddCmsHealthChecks` | Register framework health-check services | `IServiceCollection` | same, for chaining | | `MapCmsHealthChecks` | Map `GET /health` | `IEndpointRouteBuilder` | same, for chaining | **Interface notes**: - **No database check and no dependency probes** (D-21). The registration takes no options precisely so that "just add one more check" is a visible code change rather than a configuration drift. - Response is the framework default: `200` with `Healthy`, or `503` with `Unhealthy`. - The endpoint is anonymous and exposes no information beyond liveness. --- ## C-05 `CmsDataProtection` registration ```csharp public static IServiceCollection AddCmsDataProtection(this IServiceCollection services, IConfiguration configuration); ``` | Method | Purpose | Input | Output | |---|---|---|---| | `AddCmsDataProtection` | Configure Data Protection to persist keys in `ApplicationDbContext` with a stable application discriminator | `IServiceCollection`, `IConfiguration` | same, for chaining | **Interface notes**: - Replaces the bare `services.AddDataProtection()` calls currently made independently by `AvailabilityModule` and `MasterModule`. Those must be removed, or a later registration could silently override the persistent store. - The application discriminator must be **stable and explicit**. By default it derives from the content root path, which changes with every atomic release-directory switch (FR-06) — which would defeat the entire purpose of FR-12. --- ## C-06 `ApplicationDbContext` extension ```csharp public class ApplicationDbContext : IdentityDbContext<...>, IDataProtectionKeyContext { public DbSet DataProtectionKeys { get; set; } } ``` | Member | Purpose | |---|---| | `DataProtectionKeys` | Backing store for the Data Protection key ring, required by `IDataProtectionKeyContext` | Requires one new Core migration, applied automatically by C-07. --- ## C-07 Startup migration runner ```csharp public static WebApplication MigrateCoreDatabase(this WebApplication app); ``` | Method | Purpose | Input | Output | |---|---|---|---| | `MigrateCoreDatabase` | Apply pending `ApplicationDbContext` migrations before the app serves traffic | `WebApplication` | same, for chaining | **Interface notes**: - **Exceptions propagate (Q8 = A).** No try/catch, no logged-and-continue. A host that cannot reach or migrate its database must not start. - Called before `app.Run()` and before any request is accepted, so no request ever sees a partially migrated schema. - Deliberately covers only `ApplicationDbContext`; the two module contexts already migrate themselves in their `UseModule` implementations, and moving that would change existing behaviour outside this feature's scope. --- ## C-08 `CmsLogging` registration ```csharp public static IHostApplicationBuilder AddCmsLogging(this IHostApplicationBuilder builder); ``` | Method | Purpose | Input | Output | |---|---|---|---| | `AddCmsLogging` | Configure structured console logging with a correlation identifier on every entry | `IHostApplicationBuilder` | same, for chaining | **Interface notes**: - Independent of Sentry (Q10 = B) — structured logging must work with no DSN configured. - The correlation-ID mechanism is **not fixed here**; OPEN-01 is decided in NFR Design for Unit 4. - Must not log secrets, tokens or PII (SECURITY-03). --- ## C-09 `CmsSentry` registration ```csharp public static IHostApplicationBuilder AddCmsSentry(this IHostApplicationBuilder builder); ``` | Method | Purpose | Input | Output | |---|---|---|---| | `AddCmsSentry` | Initialise Sentry when a DSN is configured; do nothing when it is not | `IHostApplicationBuilder` | same, for chaining | **Interface notes**: - Absent DSN is a **normal, supported state**, not an error — local development and any deployment without Sentry must run unchanged with console logging only (FR-14). - Tags events with environment and release. - Security-relevant events for alerting (FR-19) are emitted by application code; what qualifies as alertable is defined in NFR Design for Unit 4. --- ## C-10 Static-file mount composition (`SlpModularCms.Api` only) Composed inline in `Program.cs` rather than behind an abstraction, since it is host-specific and there is exactly one host that needs it. | Registration | Purpose | |---|---| | `UseDefaultFiles` + `UseStaticFiles` with `PhysicalFileProvider(wwwroot/web)` at `/` | Serve the customer's public website | | `UseDefaultFiles` + `UseStaticFiles` with `PhysicalFileProvider(wwwroot/admin)`, `RequestPath = "/admin"` | Serve the admin SPA | | `MapFallbackToFile("/admin/{*path:nonfile}", …)` | Admin SPA client-side routes | | `MapFallbackToFile("{*path:nonfile}", …)` | Public website client-side routes | **Interface notes**: - Order matters: the `/admin` mount must be registered before the root mount, so `/admin/...` is not captured by the root provider. - The `nonfile` constraint is retained on both fallbacks — a missing asset must still `404` rather than receive HTML (existing behaviour worth preserving deliberately). - Directory browsing stays disabled (SECURITY-09). - Both providers must tolerate a **missing directory at startup**: a fresh deployment has no `wwwroot/web/` until a website workspace deploys into it, and the CMS must still start. --- ## C-13 `AvailabilityMiddleware` (modified) ```csharp private static readonly string[] _bypassPrefixes = [ /* existing */, "/health" ]; private bool IsAdminBypass(HttpContext context); ``` | Member | Change | Purpose | |---|---|---| | `_bypassPrefixes` | Add `/health` | The availability gate must never mask infrastructure liveness (FR-10, D-22) | | `IsAdminBypass` | Stop using `ReadJwtToken`; rely on a validated principal | Close the forged-token bypass (FR-24, SECURITY-08) | **Interface notes**: - Signature is unchanged; only the implementation and the constant change. - **Preserved behaviour**: an Owner or Administrator with a valid token still bypasses the gate, so administrators can always reach a disabled instance. - If the implementation moves to reading `HttpContext.User`, note that `AvailabilityMiddleware` currently runs **before** `UseAuthentication()`. Either authentication must run earlier, or the middleware must validate the token itself with the same parameters as the bearer scheme. **This ordering constraint is the substance of the fix and is resolved in Functional Design for Unit 2.** --- ## C-14 Frontend configuration (modified) ```typescript export function getAppConfig(): AppConfig; export interface AppConfig { apiBaseUrl: string; // '' means same-origin appTitle: string; } ``` | Change | Purpose | |---|---| | `apiBaseUrl` accepts empty string | Same-origin default when `VITE_API_BASE_URL` is unset (FR-13) | | Zod schema relaxed | Accept either an empty string or a valid absolute URL — **not** any string, so a malformed value is still caught | **Interface notes**: - `ApiClient` composes request URLs as `${baseUrl}${path}`, so an empty base yields a root-relative URL — same-origin without further change. - Local development against `https://localhost:7221` (master) or `:7222` (slave) must keep working exactly as today. --- ## C-15 Frontend observability (new) | Element | Purpose | |---|---| | Sentry initialisation in `main.tsx` | Error and performance reporting; skipped when `VITE_SENTRY_DSN` is absent | | Umami script component | Analytics; renders nothing when the website ID is absent or in local development | **Interface notes**: both read build-time Vite variables, which is why two separate builds are produced (D-15). --- ## C-11 / C-12 Workflow interfaces ### `deploy-scp.yaml` (and later `deploy-ftps.yaml`) — `workflow_call` inputs | Input | Type | Purpose | |---|---|---| | `artifact_name` | string | Build artifact to download | | `environment` | string | `test` or `production` — used for naming and tagging | | `deploy_path` | string | Target base path on the host | | `release_retention` | number | How many previous releases to retain for fast rollback (FR-06, D-26) | Secrets are inherited. **The input interface is identical across transports (Q11 = B)**, so a caller can switch workflow file without changing arguments. ### `continuous_integration.yaml` — `workflow_dispatch` inputs | Input | Type | Default | Purpose | |---|---|---|---| | `deploy_production` | boolean | `false` | The only route to production (FR-04) |