ui progressing

This commit is contained in:
master
2026-02-20 23:32:20 +02:00
parent ca5e7888d6
commit 1ec797d5e8
191 changed files with 32771 additions and 6504 deletions

View File

@@ -0,0 +1,132 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using System;
using System.Linq;
namespace StellaOps.Platform.WebService.Endpoints;
public static class ContextEndpoints
{
public static IEndpointRouteBuilder MapContextEndpoints(this IEndpointRouteBuilder app)
{
var context = app.MapGroup("/api/v2/context")
.WithTags("Platform Context");
context.MapGet("/regions", async Task<IResult>(
HttpContext httpContext,
PlatformRequestContextResolver resolver,
PlatformContextService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(httpContext, resolver, out _, out var failure))
{
return failure!;
}
var regions = await service.GetRegionsAsync(cancellationToken).ConfigureAwait(false);
return Results.Ok(regions);
})
.WithName("GetPlatformContextRegions")
.WithSummary("List global regions for context selection")
.RequireAuthorization(PlatformPolicies.ContextRead);
context.MapGet("/environments", async Task<IResult>(
HttpContext httpContext,
PlatformRequestContextResolver resolver,
PlatformContextService service,
[FromQuery(Name = "regions")] string? regions,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(httpContext, resolver, out _, out var failure))
{
return failure!;
}
var regionFilter = ParseCsv(regions);
var environments = await service.GetEnvironmentsAsync(regionFilter, cancellationToken).ConfigureAwait(false);
return Results.Ok(environments);
})
.WithName("GetPlatformContextEnvironments")
.WithSummary("List global environments with optional region filter")
.RequireAuthorization(PlatformPolicies.ContextRead);
context.MapGet("/preferences", async Task<IResult>(
HttpContext httpContext,
PlatformRequestContextResolver resolver,
PlatformContextService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(httpContext, resolver, out var requestContext, out var failure))
{
return failure!;
}
var preferences = await service.GetPreferencesAsync(requestContext!, cancellationToken).ConfigureAwait(false);
return Results.Ok(preferences);
})
.WithName("GetPlatformContextPreferences")
.WithSummary("Get persisted context preferences for the current user")
.RequireAuthorization(PlatformPolicies.ContextRead);
context.MapPut("/preferences", async Task<IResult>(
HttpContext httpContext,
PlatformRequestContextResolver resolver,
PlatformContextService service,
PlatformContextPreferencesRequest request,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(httpContext, resolver, out var requestContext, out var failure))
{
return failure!;
}
var preferences = await service.UpsertPreferencesAsync(
requestContext!,
request,
cancellationToken).ConfigureAwait(false);
return Results.Ok(preferences);
})
.WithName("UpdatePlatformContextPreferences")
.WithSummary("Update persisted context preferences for the current user")
.RequireAuthorization(PlatformPolicies.ContextWrite);
return app;
}
private static string[] ParseCsv(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return Array.Empty<string>();
}
return value
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(item => !string.IsNullOrWhiteSpace(item))
.Select(item => item.Trim().ToLowerInvariant())
.Distinct(StringComparer.Ordinal)
.ToArray();
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
}

View File

@@ -0,0 +1,122 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Platform.WebService.Endpoints;
public static class IntegrationReadModelEndpoints
{
public static IEndpointRouteBuilder MapIntegrationReadModelEndpoints(this IEndpointRouteBuilder app)
{
var integrations = app.MapGroup("/api/v2/integrations")
.WithTags("Integrations V2");
integrations.MapGet("/feeds", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
IntegrationsReadModelService service,
TimeProvider timeProvider,
[AsParameters] IntegrationListQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListFeedsAsync(
requestContext!,
query.Region,
query.Environment,
query.Status,
query.SourceType,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<IntegrationFeedProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListIntegrationFeedsV2")
.WithSummary("List advisory feed health/freshness integration projection")
.RequireAuthorization(PlatformPolicies.IntegrationsRead);
integrations.MapGet("/vex-sources", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
IntegrationsReadModelService service,
TimeProvider timeProvider,
[AsParameters] IntegrationListQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListVexSourcesAsync(
requestContext!,
query.Region,
query.Environment,
query.Status,
query.SourceType,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<IntegrationVexSourceProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListIntegrationVexSourcesV2")
.WithSummary("List VEX source health/freshness integration projection")
.RequireAuthorization(PlatformPolicies.IntegrationsVexRead);
return app;
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
public sealed record IntegrationListQuery(
string? Region,
string? Environment,
string? Status,
string? SourceType,
int? Limit,
int? Offset);
}

View File

@@ -0,0 +1,557 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Platform.WebService.Endpoints;
public static class LegacyAliasEndpoints
{
public static IEndpointRouteBuilder MapLegacyAliasEndpoints(this IEndpointRouteBuilder app)
{
var legacy = app.MapGroup("/api/v1")
.WithTags("Pack22 Legacy Aliases");
legacy.MapGet("/context/regions", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
PlatformContextService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out _, out var failure))
{
return failure!;
}
var regions = await service.GetRegionsAsync(cancellationToken).ConfigureAwait(false);
return Results.Ok(regions);
})
.WithName("GetPlatformContextRegionsV1Alias")
.WithSummary("Legacy alias for v2 context regions")
.RequireAuthorization(PlatformPolicies.ContextRead);
legacy.MapGet("/releases", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
[AsParameters] LegacyReleaseListQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListReleasesAsync(
requestContext!,
query.Region,
query.Environment,
query.Type,
query.Status,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<ReleaseProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListReleasesV1Alias")
.WithSummary("Legacy alias for v2 releases projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
var runAliases = legacy.MapGroup("/releases/runs");
runAliases.MapGet(string.Empty, async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
[AsParameters] LegacyRunListQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListRunsAsync(
requestContext!,
query.Status,
query.Lane,
query.Environment,
query.Region,
query.Outcome,
query.NeedsApproval,
query.BlockedByDataIntegrity,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<ReleaseRunProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListReleaseRunsV1Alias")
.WithSummary("Legacy alias for v2 run list projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunDetailAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunDetailV1Alias")
.WithSummary("Legacy alias for v2 run detail projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}/timeline", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunTimelineAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunTimelineV1Alias")
.WithSummary("Legacy alias for v2 run timeline projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}/gate-decision", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunGateDecisionAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunGateDecisionV1Alias")
.WithSummary("Legacy alias for v2 run gate decision projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}/approvals", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunApprovalsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunApprovalsV1Alias")
.WithSummary("Legacy alias for v2 run approvals projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}/deployments", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunDeploymentsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunDeploymentsV1Alias")
.WithSummary("Legacy alias for v2 run deployments projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}/security-inputs", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunSecurityInputsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunSecurityInputsV1Alias")
.WithSummary("Legacy alias for v2 run security inputs projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}/evidence", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunEvidenceAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunEvidenceV1Alias")
.WithSummary("Legacy alias for v2 run evidence projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}/rollback", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunRollbackAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunRollbackV1Alias")
.WithSummary("Legacy alias for v2 run rollback projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}/replay", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunReplayAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunReplayV1Alias")
.WithSummary("Legacy alias for v2 run replay projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runAliases.MapGet("/{runId:guid}/audit", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunAuditAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return ToRunItemResponse(requestContext!, timeProvider, runId, item);
})
.WithName("GetReleaseRunAuditV1Alias")
.WithSummary("Legacy alias for v2 run audit projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
legacy.MapGet("/topology/regions", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
TopologyReadModelService service,
TimeProvider timeProvider,
[AsParameters] LegacyTopologyRegionQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListRegionsAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<TopologyRegionProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListTopologyRegionsV1Alias")
.WithSummary("Legacy alias for v2 topology regions projection")
.RequireAuthorization(PlatformPolicies.TopologyRead);
legacy.MapGet("/security/findings", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
SecurityReadModelService service,
TimeProvider timeProvider,
[AsParameters] LegacySecurityFindingsQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListFindingsAsync(
requestContext!,
query.Pivot,
query.Region,
query.Environment,
query.Severity,
query.Disposition,
query.Search,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new SecurityFindingsResponse(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset,
page.Pivot,
page.PivotBuckets,
page.Facets));
})
.WithName("ListSecurityFindingsV1Alias")
.WithSummary("Legacy alias for v2 security findings projection")
.RequireAuthorization(PlatformPolicies.SecurityRead);
legacy.MapGet("/integrations/feeds", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
IntegrationsReadModelService service,
TimeProvider timeProvider,
[AsParameters] LegacyIntegrationQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListFeedsAsync(
requestContext!,
query.Region,
query.Environment,
query.Status,
query.SourceType,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<IntegrationFeedProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListIntegrationFeedsV1Alias")
.WithSummary("Legacy alias for v2 integrations feed projection")
.RequireAuthorization(PlatformPolicies.IntegrationsRead);
legacy.MapGet("/integrations/vex-sources", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
IntegrationsReadModelService service,
TimeProvider timeProvider,
[AsParameters] LegacyIntegrationQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListVexSourcesAsync(
requestContext!,
query.Region,
query.Environment,
query.Status,
query.SourceType,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<IntegrationVexSourceProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListIntegrationVexSourcesV1Alias")
.WithSummary("Legacy alias for v2 integrations VEX source projection")
.RequireAuthorization(PlatformPolicies.IntegrationsVexRead);
return app;
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
private static IResult ToRunItemResponse<TProjection>(
PlatformRequestContext requestContext,
TimeProvider timeProvider,
Guid runId,
TProjection? projection)
where TProjection : class
{
if (projection is null)
{
return Results.NotFound(new { error = "run_not_found", runId });
}
return Results.Ok(new PlatformItemResponse<TProjection>(
requestContext.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
projection));
}
public sealed record LegacyReleaseListQuery(
string? Region,
string? Environment,
string? Type,
string? Status,
int? Limit,
int? Offset);
public sealed record LegacyRunListQuery(
string? Status,
string? Lane,
string? Environment,
string? Region,
string? Outcome,
bool? NeedsApproval,
bool? BlockedByDataIntegrity,
int? Limit,
int? Offset);
public sealed record LegacyTopologyRegionQuery(
string? Region,
string? Environment,
int? Limit,
int? Offset);
public sealed record LegacySecurityFindingsQuery(
string? Pivot,
string? Region,
string? Environment,
string? Severity,
string? Disposition,
string? Search,
int? Limit,
int? Offset);
public sealed record LegacyIntegrationQuery(
string? Region,
string? Environment,
string? Status,
string? SourceType,
int? Limit,
int? Offset);
}

View File

@@ -0,0 +1,539 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Platform.WebService.Endpoints;
public static class ReleaseReadModelEndpoints
{
public static IEndpointRouteBuilder MapReleaseReadModelEndpoints(this IEndpointRouteBuilder app)
{
var releases = app.MapGroup("/api/v2/releases")
.WithTags("Releases V2");
releases.MapGet(string.Empty, async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
[AsParameters] ReleaseListQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListReleasesAsync(
requestContext!,
query.Region,
query.Environment,
query.Type,
query.Status,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<ReleaseProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListReleasesV2")
.WithSummary("List Pack-22 release projections")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
releases.MapGet("/{releaseId:guid}", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid releaseId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var detail = await service.GetReleaseDetailAsync(
requestContext!,
releaseId,
cancellationToken).ConfigureAwait(false);
if (detail is null)
{
return Results.NotFound(new { error = "release_not_found", releaseId });
}
return Results.Ok(new PlatformItemResponse<ReleaseDetailProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
detail));
})
.WithName("GetReleaseDetailV2")
.WithSummary("Get Pack-22 release detail projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
releases.MapGet("/activity", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
[AsParameters] ReleaseActivityQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListActivityAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<ReleaseActivityProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListReleaseActivityV2")
.WithSummary("List cross-release activity timeline")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
releases.MapGet("/approvals", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
[AsParameters] ReleaseApprovalsQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListApprovalsAsync(
requestContext!,
query.Status,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<ReleaseApprovalProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListReleaseApprovalsV2")
.WithSummary("List cross-release approvals queue projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
var runs = releases.MapGroup("/runs");
runs.MapGet(string.Empty, async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
[AsParameters] ReleaseRunListQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListRunsAsync(
requestContext!,
query.Status,
query.Lane,
query.Environment,
query.Region,
query.Outcome,
query.NeedsApproval,
query.BlockedByDataIntegrity,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<ReleaseRunProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListReleaseRunsV2")
.WithSummary("List run-centric release projections for Pack-22 contracts")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunDetailAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunDetailProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunDetailV2")
.WithSummary("Get canonical release run detail projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}/timeline", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunTimelineAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunTimelineProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunTimelineV2")
.WithSummary("Get release run timeline projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}/gate-decision", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunGateDecisionAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunGateDecisionProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunGateDecisionV2")
.WithSummary("Get release run gate decision projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}/approvals", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunApprovalsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunApprovalsProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunApprovalsV2")
.WithSummary("Get release run approvals checkpoints projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}/deployments", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunDeploymentsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunDeploymentsProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunDeploymentsV2")
.WithSummary("Get release run deployments projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}/security-inputs", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunSecurityInputsAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunSecurityInputsProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunSecurityInputsV2")
.WithSummary("Get release run security inputs projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}/evidence", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunEvidenceAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunEvidenceProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunEvidenceV2")
.WithSummary("Get release run evidence capsule projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}/rollback", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunRollbackAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunRollbackProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunRollbackV2")
.WithSummary("Get release run rollback projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}/replay", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunReplayAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunReplayProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunReplayV2")
.WithSummary("Get release run replay projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
runs.MapGet("/{runId:guid}/audit", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
ReleaseReadModelService service,
TimeProvider timeProvider,
Guid runId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetRunAuditAsync(requestContext!, runId, cancellationToken).ConfigureAwait(false);
return item is null
? Results.NotFound(new { error = "run_not_found", runId })
: Results.Ok(new PlatformItemResponse<ReleaseRunAuditProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetReleaseRunAuditV2")
.WithSummary("Get release run audit projection")
.RequireAuthorization(PlatformPolicies.ReleaseControlRead);
return app;
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
public sealed record ReleaseListQuery(
string? Region,
string? Environment,
string? Type,
string? Status,
int? Limit,
int? Offset);
public sealed record ReleaseActivityQuery(
string? Region,
string? Environment,
int? Limit,
int? Offset);
public sealed record ReleaseApprovalsQuery(
string? Status,
string? Region,
string? Environment,
int? Limit,
int? Offset);
public sealed record ReleaseRunListQuery(
string? Status,
string? Lane,
string? Environment,
string? Region,
string? Outcome,
bool? NeedsApproval,
bool? BlockedByDataIntegrity,
int? Limit,
int? Offset);
}

View File

@@ -0,0 +1,221 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Platform.WebService.Endpoints;
public static class SecurityReadModelEndpoints
{
public static IEndpointRouteBuilder MapSecurityReadModelEndpoints(this IEndpointRouteBuilder app)
{
var security = app.MapGroup("/api/v2/security")
.WithTags("Security V2");
security.MapGet("/findings", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
SecurityReadModelService service,
TimeProvider timeProvider,
[AsParameters] SecurityFindingsQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListFindingsAsync(
requestContext!,
query.Pivot,
query.Region,
query.Environment,
query.Severity,
query.Disposition,
query.Search,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new SecurityFindingsResponse(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset,
page.Pivot,
page.PivotBuckets,
page.Facets));
})
.WithName("ListSecurityFindingsV2")
.WithSummary("List consolidated security findings with pivot/facet schema")
.RequireAuthorization(PlatformPolicies.SecurityRead);
security.MapGet("/disposition", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
SecurityReadModelService service,
TimeProvider timeProvider,
[AsParameters] SecurityDispositionQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListDispositionAsync(
requestContext!,
query.Region,
query.Environment,
query.Status,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<SecurityDispositionProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListSecurityDispositionV2")
.WithSummary("List consolidated security disposition projection (VEX + exceptions read-join)")
.RequireAuthorization(PlatformPolicies.SecurityRead);
security.MapGet("/disposition/{findingId}", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
SecurityReadModelService service,
TimeProvider timeProvider,
string findingId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var item = await service.GetDispositionAsync(
requestContext!,
findingId,
cancellationToken).ConfigureAwait(false);
if (item is null)
{
return Results.NotFound(new { error = "finding_not_found", findingId });
}
return Results.Ok(new PlatformItemResponse<SecurityDispositionProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
item));
})
.WithName("GetSecurityDispositionV2")
.WithSummary("Get consolidated security disposition by finding id")
.RequireAuthorization(PlatformPolicies.SecurityRead);
security.MapGet("/sbom-explorer", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
SecurityReadModelService service,
TimeProvider timeProvider,
[AsParameters] SecuritySbomExplorerQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetSbomExplorerAsync(
requestContext!,
query.Mode,
query.Region,
query.Environment,
query.LeftReleaseId,
query.RightReleaseId,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new SecuritySbomExplorerResponse(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
Mode: result.Mode,
Table: result.Table,
GraphNodes: result.GraphNodes,
GraphEdges: result.GraphEdges,
Diff: result.Diff,
TotalComponents: result.TotalComponents,
Limit: result.Limit,
Offset: result.Offset));
})
.WithName("GetSecuritySbomExplorerV2")
.WithSummary("Get consolidated SBOM explorer projection (table/graph/diff)")
.RequireAuthorization(PlatformPolicies.SecurityRead);
return app;
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
public sealed record SecurityFindingsQuery(
string? Pivot,
string? Region,
string? Environment,
string? Severity,
string? Disposition,
string? Search,
int? Limit,
int? Offset);
public sealed record SecurityDispositionQuery(
string? Region,
string? Environment,
string? Status,
int? Limit,
int? Offset);
public sealed record SecuritySbomExplorerQuery(
string? Mode,
string? Region,
string? Environment,
string? LeftReleaseId,
string? RightReleaseId,
int? Limit,
int? Offset);
}

View File

@@ -0,0 +1,332 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Platform.WebService.Constants;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Platform.WebService.Endpoints;
public static class TopologyReadModelEndpoints
{
public static IEndpointRouteBuilder MapTopologyReadModelEndpoints(this IEndpointRouteBuilder app)
{
var topology = app.MapGroup("/api/v2/topology")
.WithTags("Topology V2");
topology.MapGet("/regions", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
TopologyReadModelService service,
TimeProvider timeProvider,
[AsParameters] TopologyQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListRegionsAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<TopologyRegionProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListTopologyRegionsV2")
.WithSummary("List topology regions")
.RequireAuthorization(PlatformPolicies.TopologyRead);
topology.MapGet("/environments", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
TopologyReadModelService service,
TimeProvider timeProvider,
[AsParameters] TopologyQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListEnvironmentsAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<TopologyEnvironmentProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListTopologyEnvironmentsV2")
.WithSummary("List topology environments")
.RequireAuthorization(PlatformPolicies.TopologyRead);
topology.MapGet("/targets", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
TopologyReadModelService service,
TimeProvider timeProvider,
[AsParameters] TopologyQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListTargetsAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<TopologyTargetProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListTopologyTargetsV2")
.WithSummary("List topology targets")
.RequireAuthorization(PlatformPolicies.TopologyRead);
topology.MapGet("/hosts", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
TopologyReadModelService service,
TimeProvider timeProvider,
[AsParameters] TopologyQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListHostsAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<TopologyHostProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListTopologyHostsV2")
.WithSummary("List topology hosts")
.RequireAuthorization(PlatformPolicies.TopologyRead);
topology.MapGet("/agents", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
TopologyReadModelService service,
TimeProvider timeProvider,
[AsParameters] TopologyQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListAgentsAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<TopologyAgentProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListTopologyAgentsV2")
.WithSummary("List topology agents")
.RequireAuthorization(PlatformPolicies.TopologyRead);
topology.MapGet("/promotion-paths", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
TopologyReadModelService service,
TimeProvider timeProvider,
[AsParameters] TopologyQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListPromotionPathsAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<TopologyPromotionPathProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListTopologyPromotionPathsV2")
.WithSummary("List topology promotion paths")
.RequireAuthorization(PlatformPolicies.TopologyRead);
topology.MapGet("/workflows", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
TopologyReadModelService service,
TimeProvider timeProvider,
[AsParameters] TopologyQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListWorkflowsAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<TopologyWorkflowProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListTopologyWorkflowsV2")
.WithSummary("List topology workflows")
.RequireAuthorization(PlatformPolicies.TopologyRead);
topology.MapGet("/gate-profiles", async Task<IResult>(
HttpContext context,
PlatformRequestContextResolver resolver,
TopologyReadModelService service,
TimeProvider timeProvider,
[AsParameters] TopologyQuery query,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var page = await service.ListGateProfilesAsync(
requestContext!,
query.Region,
query.Environment,
query.Limit,
query.Offset,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new PlatformListResponse<TopologyGateProfileProjection>(
requestContext!.TenantId,
requestContext.ActorId,
timeProvider.GetUtcNow(),
Cached: false,
CacheTtlSeconds: 0,
page.Items,
page.Total,
page.Limit,
page.Offset));
})
.WithName("ListTopologyGateProfilesV2")
.WithSummary("List topology gate profiles")
.RequireAuthorization(PlatformPolicies.TopologyRead);
return app;
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
out PlatformRequestContext? requestContext,
out IResult? failure)
{
if (resolver.TryResolve(context, out requestContext, out var error))
{
failure = null;
return true;
}
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
return false;
}
public sealed record TopologyQuery(
string? Region,
string? Environment,
int? Limit,
int? Offset);
}