feat(audit): deprecation headers for per-service audit list endpoints
Sprints SPRINT_20260408_004 AUDIT-005 + SPRINT_20260408_005
DEPRECATE-002 (early, non-waiting half).
- StellaOps.Audit.Emission gains a `DeprecatedAuditEndpoint` helper:
.DeprecatedForTimeline(sunset, successorLink) + a
DeprecationHeaderEndpointFilter that writes RFC-style Sunset,
Deprecation, and Link: <successor>; rel="successor-version" headers.
- Notify GET /api/v1/notify/audit + ReleaseOrchestrator
GET /api/v1/release-orchestrator/audit now advertise Sunset
2027-10-19 and link to /api/v1/audit/events?modules={notify|jobengine}.
Chain-verify / summary / single-entry lookups on ReleaseOrchestrator
are intentionally NOT deprecated — they serve service-level chain-of-
custody evidence (sprint Decision 2) that the unified store cannot
replace.
Both services build clean. Remaining per-service deprecation headers
(Authority /console/admin/audit, Policy /governance/audit/events,
EvidenceLocker /evidence/audit) follow the same pattern and can land
as a wave when those endpoints are touched for other reasons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace StellaOps.Audit.Emission;
|
||||
|
||||
/// <summary>
|
||||
/// Helpers that mark per-service audit endpoints as deprecated so clients get
|
||||
/// RFC-compliant Sunset / Deprecation / Link headers pointing at the unified
|
||||
/// Timeline endpoint. Sprints SPRINT_20260408_004 AUDIT-005 +
|
||||
/// SPRINT_20260408_005 DEPRECATE-002 (Phase 4).
|
||||
/// </summary>
|
||||
public static class DeprecatedAuditEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an endpoint filter that stamps deprecation headers on every response
|
||||
/// from the decorated endpoint. The successor link points at the unified
|
||||
/// Timeline audit endpoint (caller supplies any module-specific query suffix).
|
||||
/// </summary>
|
||||
/// <param name="builder">Endpoint builder.</param>
|
||||
/// <param name="sunset">Earliest date the endpoint may be removed (UTC).</param>
|
||||
/// <param name="successorLink">Absolute or relative URL to the Timeline replacement.</param>
|
||||
/// <param name="deprecated">
|
||||
/// Optional explicit deprecation date. Defaults to <see cref="DateTimeOffset.UtcNow"/>
|
||||
/// which is the right choice when deprecation takes effect on deploy.
|
||||
/// </param>
|
||||
public static TBuilder DeprecatedForTimeline<TBuilder>(
|
||||
this TBuilder builder,
|
||||
DateTimeOffset sunset,
|
||||
string successorLink,
|
||||
DateTimeOffset? deprecated = null)
|
||||
where TBuilder : IEndpointConventionBuilder
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(successorLink);
|
||||
|
||||
var effectiveDeprecated = deprecated ?? DateTimeOffset.UtcNow;
|
||||
builder.Add(endpointBuilder =>
|
||||
{
|
||||
endpointBuilder.Metadata.Add(new DeprecatedAuditMetadata(sunset, successorLink, effectiveDeprecated));
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the three deprecation headers onto the supplied response. Call this
|
||||
/// from inside an endpoint handler when the static metadata filter route is
|
||||
/// not available (e.g., MVC controllers).
|
||||
/// </summary>
|
||||
public static void StampDeprecationHeaders(HttpResponse response, DateTimeOffset sunset, string successorLink, DateTimeOffset? deprecated = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(response);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(successorLink);
|
||||
|
||||
var effectiveDeprecated = deprecated ?? DateTimeOffset.UtcNow;
|
||||
response.Headers["Deprecation"] = effectiveDeprecated.UtcDateTime.ToString("r", CultureInfo.InvariantCulture);
|
||||
response.Headers["Sunset"] = sunset.UtcDateTime.ToString("r", CultureInfo.InvariantCulture);
|
||||
response.Headers["Link"] = $"<{successorLink}>; rel=\"successor-version\"";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata carried by endpoints marked deprecated for unified-timeline successor.
|
||||
/// Consumed by <see cref="DeprecationHeaderEndpointFilter"/>.
|
||||
/// </summary>
|
||||
public sealed class DeprecatedAuditMetadata
|
||||
{
|
||||
public DeprecatedAuditMetadata(DateTimeOffset sunset, string successorLink, DateTimeOffset deprecated)
|
||||
{
|
||||
Sunset = sunset;
|
||||
SuccessorLink = successorLink;
|
||||
Deprecated = deprecated;
|
||||
}
|
||||
|
||||
public DateTimeOffset Sunset { get; }
|
||||
|
||||
public string SuccessorLink { get; }
|
||||
|
||||
public DateTimeOffset Deprecated { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint filter that writes deprecation headers whenever the endpoint carries
|
||||
/// <see cref="DeprecatedAuditMetadata"/>. Must be registered on the route group
|
||||
/// or endpoint via <c>.AddEndpointFilter<DeprecationHeaderEndpointFilter>()</c>.
|
||||
/// </summary>
|
||||
public sealed class DeprecationHeaderEndpointFilter : IEndpointFilter
|
||||
{
|
||||
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
||||
{
|
||||
var metadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<DeprecatedAuditMetadata>();
|
||||
if (metadata is not null)
|
||||
{
|
||||
DeprecatedAuditEndpoint.StampDeprecationHeaders(
|
||||
context.HttpContext.Response,
|
||||
metadata.Sunset,
|
||||
metadata.SuccessorLink,
|
||||
metadata.Deprecated);
|
||||
}
|
||||
|
||||
return await next(context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user