#nullable enable using ErsatzTV.Filters; using ErsatzTV.Services; using Microsoft.AspNetCore.OpenApi; using Microsoft.OpenApi; namespace ErsatzTV.Serialization; /// /// OpenAPI operation transformer that documents the API-key contract by construction: for /// every operation that actually requires the X-Api-Key header it injects the /// ApiKey security requirement and a documented 401 response. The "requires a key" /// decision is the exact same predicate the runtime filter enforces /// (), so the spec can never claim an /// endpoint is open when it is gated (or vice-versa). The ApiKey scheme itself and the /// ProblemDetails schema the 401 references are declared by /// . See issues #286/#287. /// public sealed class ApiSecurityOperationTransformer(IApiKeyProvider apiKeyProvider) : IOpenApiOperationTransformer { public const string SchemeName = "ApiKey"; public Task TransformAsync( OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken) { string method = context.Description.HttpMethod ?? string.Empty; IEnumerable metadata = context.Description.ActionDescriptor.EndpointMetadata; if (!ApiAuthorizationFilter.EndpointRequiresKey(method, metadata, apiKeyProvider.RequireKeyForReads)) { return Task.CompletedTask; } operation.Security ??= new List(); operation.Security.Add(new OpenApiSecurityRequirement { [new OpenApiSecuritySchemeReference(SchemeName)] = new List() }); operation.Responses ??= new OpenApiResponses(); if (!operation.Responses.ContainsKey("401")) { operation.Responses["401"] = new OpenApiResponse { Description = "API key missing or invalid.", Content = new Dictionary { ["application/json"] = new OpenApiMediaType { Schema = new OpenApiSchemaReference("ProblemDetails") } } }; } return Task.CompletedTask; } }