using Microsoft.AspNetCore.Mvc; using StellaOps.Policy.Engine.AirGap; namespace StellaOps.Policy.Engine.Endpoints; /// /// Endpoints for staleness signaling and fallback status per CONTRACT-SEALED-MODE-004. /// public static class StalenessEndpoints { public static IEndpointRouteBuilder MapStalenessSignaling(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/system/airgap/staleness"); group.MapGet("/status", GetStalenessStatusAsync) .WithName("AirGap.GetStalenessStatus") .WithDescription("Get staleness signal status for health monitoring"); group.MapGet("/fallback", GetFallbackStatusAsync) .WithName("AirGap.GetFallbackStatus") .WithDescription("Get fallback mode status and configuration"); group.MapPost("/evaluate", EvaluateStalenessAsync) .WithName("AirGap.EvaluateStaleness") .WithDescription("Trigger staleness evaluation and signaling") .RequireAuthorization(policy => policy.RequireClaim("scope", "airgap:status:read")); group.MapPost("/recover", SignalRecoveryAsync) .WithName("AirGap.SignalRecovery") .WithDescription("Signal staleness recovery after time anchor refresh") .RequireAuthorization(policy => policy.RequireClaim("scope", "airgap:seal")); return routes; } private static async Task GetStalenessStatusAsync( [FromHeader(Name = "X-Tenant-Id")] string? tenantId, IStalenessSignalingService service, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { tenantId = "default"; } var status = await service.GetSignalStatusAsync(tenantId, cancellationToken).ConfigureAwait(false); // Return different status codes based on health if (status.IsBreach) { return Results.Json(status, statusCode: StatusCodes.Status503ServiceUnavailable); } if (status.HasWarning) { // Return 200 but with warning headers return Results.Ok(status); } return Results.Ok(status); } private static async Task GetFallbackStatusAsync( [FromHeader(Name = "X-Tenant-Id")] string? tenantId, IStalenessSignalingService service, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { tenantId = "default"; } var config = await service.GetFallbackConfigurationAsync(tenantId, cancellationToken).ConfigureAwait(false); var isActive = await service.IsFallbackActiveAsync(tenantId, cancellationToken).ConfigureAwait(false); return Results.Ok(new { fallbackActive = isActive, configuration = config }); } private static async Task EvaluateStalenessAsync( [FromHeader(Name = "X-Tenant-Id")] string? tenantId, IStalenessSignalingService service, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { tenantId = "default"; } await service.EvaluateAndSignalAsync(tenantId, cancellationToken).ConfigureAwait(false); var status = await service.GetSignalStatusAsync(tenantId, cancellationToken).ConfigureAwait(false); return Results.Ok(new { evaluated = true, status }); } private static async Task SignalRecoveryAsync( [FromHeader(Name = "X-Tenant-Id")] string? tenantId, IStalenessSignalingService service, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(tenantId)) { tenantId = "default"; } await service.SignalRecoveryAsync(tenantId, cancellationToken).ConfigureAwait(false); return Results.Ok(new { recovered = true, tenantId }); } }