Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled

This commit is contained in:
master
2025-12-11 11:00:51 +02:00
596 changed files with 95428 additions and 15743 deletions

View File

@@ -1,7 +1,9 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core.Evidence;
using StellaOps.Excititor.Core.Storage;
using StellaOps.Excititor.WebService.Services;
using static Program;
@@ -9,16 +11,22 @@ using static Program;
namespace StellaOps.Excititor.WebService.Endpoints;
/// <summary>
/// Attestation API endpoints (temporarily disabled while Mongo is removed and Postgres storage is adopted).
/// Attestation API endpoints for listing and retrieving DSSE attestations.
/// </summary>
public static class AttestationEndpoints
{
public static void MapAttestationEndpoints(this WebApplication app)
{
// GET /attestations/vex/list
app.MapGet("/attestations/vex/list", (
app.MapGet("/attestations/vex/list", async (
HttpContext context,
IOptions<VexStorageOptions> storageOptions) =>
[FromQuery] string? since,
[FromQuery] string? until,
[FromQuery] int? limit,
[FromQuery] int? offset,
IOptions<VexStorageOptions> storageOptions,
[FromServices] IVexAttestationStore? attestationStore,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
@@ -26,22 +34,55 @@ public static class AttestationEndpoints
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out _, out var tenantError))
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
{
return tenantError;
}
return Results.Problem(
detail: "Attestation listing is temporarily unavailable during Postgres migration (Mongo/BSON removed).",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
if (attestationStore is null)
{
return Results.Problem(
detail: "Attestation store is not configured.",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
}
var parsedSince = ParseTimestamp(since);
var parsedUntil = ParseTimestamp(until);
var query = new VexAttestationQuery(
tenant!,
parsedSince,
parsedUntil,
limit ?? 100,
offset ?? 0);
var result = await attestationStore.ListAsync(query, cancellationToken).ConfigureAwait(false);
var items = result.Items
.Select(a => new AttestationListItemDto(
a.AttestationId,
a.ManifestId,
a.MerkleRoot,
a.ItemCount,
a.AttestedAt))
.ToList();
var response = new AttestationListResponse(
items,
result.TotalCount,
result.HasMore);
return Results.Ok(response);
}).WithName("ListVexAttestations");
// GET /attestations/vex/{attestationId}
app.MapGet("/attestations/vex/{attestationId}", (
app.MapGet("/attestations/vex/{attestationId}", async (
HttpContext context,
string attestationId,
IOptions<VexStorageOptions> storageOptions) =>
IOptions<VexStorageOptions> storageOptions,
[FromServices] IVexAttestationStore? attestationStore,
CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null)
@@ -49,7 +90,7 @@ public static class AttestationEndpoints
return scopeResult;
}
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out _, out var tenantError))
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
{
return tenantError;
}
@@ -62,10 +103,69 @@ public static class AttestationEndpoints
title: "Validation error");
}
return Results.Problem(
detail: "Attestation retrieval is temporarily unavailable during Postgres migration (Mongo/BSON removed).",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
if (attestationStore is null)
{
return Results.Problem(
detail: "Attestation store is not configured.",
statusCode: StatusCodes.Status503ServiceUnavailable,
title: "Service unavailable");
}
var attestation = await attestationStore.FindByIdAsync(tenant!, attestationId, cancellationToken).ConfigureAwait(false);
if (attestation is null)
{
return Results.NotFound(new
{
error = new { code = "ERR_ATTESTATION_NOT_FOUND", message = $"Attestation '{attestationId}' not found" }
});
}
var response = new AttestationDetailResponse(
attestation.AttestationId,
attestation.Tenant,
attestation.ManifestId,
attestation.MerkleRoot,
attestation.DsseEnvelopeJson,
attestation.DsseEnvelopeHash,
attestation.ItemCount,
attestation.AttestedAt,
attestation.Metadata);
return Results.Ok(response);
}).WithName("GetVexAttestation");
}
private static DateTimeOffset? ParseTimestamp(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return DateTimeOffset.TryParse(value, out var parsed) ? parsed : null;
}
}
// Response DTOs
public sealed record AttestationListItemDto(
[property: JsonPropertyName("attestationId")] string AttestationId,
[property: JsonPropertyName("manifestId")] string ManifestId,
[property: JsonPropertyName("merkleRoot")] string MerkleRoot,
[property: JsonPropertyName("itemCount")] int ItemCount,
[property: JsonPropertyName("attestedAt")] DateTimeOffset AttestedAt);
public sealed record AttestationListResponse(
[property: JsonPropertyName("items")] IReadOnlyList<AttestationListItemDto> Items,
[property: JsonPropertyName("totalCount")] int TotalCount,
[property: JsonPropertyName("hasMore")] bool HasMore);
public sealed record AttestationDetailResponse(
[property: JsonPropertyName("attestationId")] string AttestationId,
[property: JsonPropertyName("tenant")] string Tenant,
[property: JsonPropertyName("manifestId")] string ManifestId,
[property: JsonPropertyName("merkleRoot")] string MerkleRoot,
[property: JsonPropertyName("dsseEnvelopeJson")] string DsseEnvelopeJson,
[property: JsonPropertyName("dsseEnvelopeHash")] string DsseEnvelopeHash,
[property: JsonPropertyName("itemCount")] int ItemCount,
[property: JsonPropertyName("attestedAt")] DateTimeOffset AttestedAt,
[property: JsonPropertyName("metadata")] IReadOnlyDictionary<string, string> Metadata);

View File

@@ -82,6 +82,9 @@ services.AddSingleton<IGraphOverlayStore>(sp =>
});
services.AddSingleton<IVexEvidenceLockerService, VexEvidenceLockerService>();
services.AddSingleton<IVexEvidenceAttestor, StellaOps.Excititor.Attestation.Evidence.VexEvidenceAttestor>();
// OBS-52/53/54: Attestation storage and timeline event recording
services.TryAddSingleton<IVexAttestationStore, InMemoryVexAttestationStore>();
services.TryAddSingleton<IVexTimelineEventRecorder, VexTimelineEventRecorder>();
services.AddScoped<IVexIngestOrchestrator, VexIngestOrchestrator>();
services.AddSingleton<VexStatementBackfillService>();
services.AddOptions<ExcititorObservabilityOptions>()