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
});
}
}