tenant fixes

This commit is contained in:
master
2026-02-23 23:44:50 +02:00
parent bdb1438654
commit 4f947a8b61
159 changed files with 1064 additions and 556 deletions

View File

@@ -0,0 +1,112 @@
using StellaOps.ExportCenter.Core.Domain;
namespace StellaOps.ExportCenter.Core.Persistence;
/// <summary>
/// Repository for managing export distributions.
/// </summary>
public interface IExportDistributionRepository
{
/// <summary>
/// Gets a distribution by ID.
/// </summary>
Task<ExportDistribution?> GetByIdAsync(
Guid tenantId,
Guid distributionId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets a distribution by idempotency key.
/// </summary>
Task<ExportDistribution?> GetByIdempotencyKeyAsync(
Guid tenantId,
string idempotencyKey,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists distributions for a run.
/// </summary>
Task<IReadOnlyList<ExportDistribution>> ListByRunAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists distributions by status.
/// </summary>
Task<IReadOnlyList<ExportDistribution>> ListByStatusAsync(
Guid tenantId,
ExportDistributionStatus status,
int limit = 100,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists distributions due for retention deletion.
/// </summary>
Task<IReadOnlyList<ExportDistribution>> ListExpiredAsync(
DateTimeOffset asOf,
int limit = 100,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new distribution record.
/// </summary>
Task<ExportDistribution> CreateAsync(
ExportDistribution distribution,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates a distribution record.
/// Returns the updated record, or null if not found.
/// </summary>
Task<ExportDistribution?> UpdateAsync(
ExportDistribution distribution,
CancellationToken cancellationToken = default);
/// <summary>
/// Performs an idempotent upsert based on idempotency key.
/// Returns existing distribution if key matches, otherwise creates new.
/// </summary>
Task<(ExportDistribution Distribution, bool WasCreated)> UpsertByIdempotencyKeyAsync(
ExportDistribution distribution,
CancellationToken cancellationToken = default);
/// <summary>
/// Marks a distribution for deletion.
/// </summary>
Task<bool> MarkForDeletionAsync(
Guid tenantId,
Guid distributionId,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a distribution record and returns whether it existed.
/// </summary>
Task<bool> DeleteAsync(
Guid tenantId,
Guid distributionId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets distribution statistics for a run.
/// </summary>
Task<ExportDistributionStats> GetStatsAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Statistics for distributions of a run.
/// </summary>
public sealed record ExportDistributionStats
{
public int Total { get; init; }
public int Pending { get; init; }
public int Distributing { get; init; }
public int Distributed { get; init; }
public int Verified { get; init; }
public int Failed { get; init; }
public int Cancelled { get; init; }
public long TotalSizeBytes { get; init; }
}

View File

@@ -0,0 +1,66 @@
using StellaOps.ExportCenter.Core.Domain;
namespace StellaOps.ExportCenter.Core.Persistence;
/// <summary>
/// Repository for managing export profiles.
/// </summary>
public interface IExportProfileRepository
{
/// <summary>
/// Gets a profile by ID for a tenant.
/// </summary>
Task<ExportProfile?> GetByIdAsync(
Guid tenantId,
Guid profileId,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists profiles for a tenant with optional filtering.
/// </summary>
Task<(IReadOnlyList<ExportProfile> Items, int TotalCount)> ListAsync(
Guid tenantId,
ExportProfileStatus? status = null,
ExportProfileKind? kind = null,
string? search = null,
int offset = 0,
int limit = 50,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new profile.
/// </summary>
Task<ExportProfile> CreateAsync(
ExportProfile profile,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing profile.
/// </summary>
Task<ExportProfile?> UpdateAsync(
ExportProfile profile,
CancellationToken cancellationToken = default);
/// <summary>
/// Archives a profile (soft delete).
/// </summary>
Task<bool> ArchiveAsync(
Guid tenantId,
Guid profileId,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if a profile name is unique within a tenant.
/// </summary>
Task<bool> IsNameUniqueAsync(
Guid tenantId,
string name,
Guid? excludeProfileId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets active scheduled profiles for processing.
/// </summary>
Task<IReadOnlyList<ExportProfile>> GetScheduledProfilesAsync(
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,133 @@
using StellaOps.ExportCenter.Core.Domain;
namespace StellaOps.ExportCenter.Core.Persistence;
/// <summary>
/// Repository for managing export runs.
/// </summary>
public interface IExportRunRepository
{
/// <summary>
/// Gets a run by ID for a tenant.
/// </summary>
Task<ExportRun?> GetByIdAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists runs for a tenant with optional filtering.
/// </summary>
Task<(IReadOnlyList<ExportRun> Items, int TotalCount)> ListAsync(
Guid tenantId,
Guid? profileId = null,
ExportRunStatus? status = null,
ExportRunTrigger? trigger = null,
DateTimeOffset? createdAfter = null,
DateTimeOffset? createdBefore = null,
string? correlationId = null,
int offset = 0,
int limit = 50,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new run.
/// </summary>
Task<ExportRun> CreateAsync(
ExportRun run,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates run status and progress.
/// </summary>
Task<ExportRun?> UpdateAsync(
ExportRun run,
CancellationToken cancellationToken = default);
/// <summary>
/// Cancels a run if it's in a cancellable state.
/// </summary>
Task<bool> CancelAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets active runs count for concurrency checks.
/// </summary>
Task<int> GetActiveRunsCountAsync(
Guid tenantId,
Guid? profileId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets queued runs count.
/// </summary>
Task<int> GetQueuedRunsCountAsync(
Guid tenantId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the next queued run to execute.
/// </summary>
Task<ExportRun?> DequeueNextRunAsync(
Guid tenantId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Repository for managing export artifacts.
/// </summary>
public interface IExportArtifactRepository
{
/// <summary>
/// Gets an artifact by ID.
/// </summary>
Task<ExportArtifact?> GetByIdAsync(
Guid tenantId,
Guid artifactId,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists artifacts for a run.
/// </summary>
Task<IReadOnlyList<ExportArtifact>> ListByRunAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new artifact record.
/// </summary>
Task<ExportArtifact> CreateAsync(
ExportArtifact artifact,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes artifacts for a run.
/// </summary>
Task<int> DeleteByRunAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Represents an export artifact.
/// </summary>
public sealed record ExportArtifact
{
public required Guid ArtifactId { get; init; }
public required Guid RunId { get; init; }
public required Guid TenantId { get; init; }
public required string Name { get; init; }
public required string Kind { get; init; }
public required string Path { get; init; }
public long SizeBytes { get; init; }
public string? ContentType { get; init; }
public required string Checksum { get; init; }
public string ChecksumAlgorithm { get; init; } = "SHA-256";
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset? ExpiresAt { get; init; }
}

View File

@@ -4,7 +4,7 @@ using Npgsql;
using StellaOps.ExportCenter.Core.Domain;
using StellaOps.ExportCenter.Infrastructure.Db;
using StellaOps.ExportCenter.Infrastructure.EfCore.Models;
using StellaOps.ExportCenter.WebService.Distribution;
using StellaOps.ExportCenter.Core.Persistence;
namespace StellaOps.ExportCenter.Infrastructure.Postgres.Repositories;

View File

@@ -4,7 +4,7 @@ using Npgsql;
using StellaOps.ExportCenter.Core.Domain;
using StellaOps.ExportCenter.Infrastructure.Db;
using StellaOps.ExportCenter.Infrastructure.EfCore.Models;
using StellaOps.ExportCenter.WebService.Api;
using StellaOps.ExportCenter.Core.Persistence;
namespace StellaOps.ExportCenter.Infrastructure.Postgres.Repositories;

View File

@@ -4,7 +4,7 @@ using Npgsql;
using StellaOps.ExportCenter.Core.Domain;
using StellaOps.ExportCenter.Infrastructure.Db;
using StellaOps.ExportCenter.Infrastructure.EfCore.Models;
using StellaOps.ExportCenter.WebService.Api;
using StellaOps.ExportCenter.Core.Persistence;
namespace StellaOps.ExportCenter.Infrastructure.Postgres.Repositories;

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.ExportCenter.Core.Domain;
using StellaOps.ExportCenter.Core.Persistence;
using StellaOps.ExportCenter.WebService.Api;
using StellaOps.TestKit;

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.ExportCenter.Core.Persistence;
using StellaOps.ExportCenter.WebService.Api;
using Xunit;

View File

@@ -6,7 +6,9 @@ using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Determinism;
using StellaOps.ExportCenter.Core.Domain;
using StellaOps.ExportCenter.Core.Persistence;
using StellaOps.ExportCenter.Core.Planner;
using IExportProfileRepository = StellaOps.ExportCenter.Core.Persistence.IExportProfileRepository;
using StellaOps.ExportCenter.WebService.Telemetry;
using System.Runtime.CompilerServices;
using System.Security.Claims;

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Determinism;
using StellaOps.ExportCenter.Core.Persistence;
namespace StellaOps.ExportCenter.WebService.Api;

View File

@@ -1,66 +1,3 @@
using StellaOps.ExportCenter.Core.Domain;
namespace StellaOps.ExportCenter.WebService.Api;
/// <summary>
/// Repository for managing export profiles.
/// </summary>
public interface IExportProfileRepository
{
/// <summary>
/// Gets a profile by ID for a tenant.
/// </summary>
Task<ExportProfile?> GetByIdAsync(
Guid tenantId,
Guid profileId,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists profiles for a tenant with optional filtering.
/// </summary>
Task<(IReadOnlyList<ExportProfile> Items, int TotalCount)> ListAsync(
Guid tenantId,
ExportProfileStatus? status = null,
ExportProfileKind? kind = null,
string? search = null,
int offset = 0,
int limit = 50,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new profile.
/// </summary>
Task<ExportProfile> CreateAsync(
ExportProfile profile,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing profile.
/// </summary>
Task<ExportProfile?> UpdateAsync(
ExportProfile profile,
CancellationToken cancellationToken = default);
/// <summary>
/// Archives a profile (soft delete).
/// </summary>
Task<bool> ArchiveAsync(
Guid tenantId,
Guid profileId,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if a profile name is unique within a tenant.
/// </summary>
Task<bool> IsNameUniqueAsync(
Guid tenantId,
string name,
Guid? excludeProfileId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets active scheduled profiles for processing.
/// </summary>
Task<IReadOnlyList<ExportProfile>> GetScheduledProfilesAsync(
CancellationToken cancellationToken = default);
}
// This interface has been moved to StellaOps.ExportCenter.Core.Persistence.
// Import that namespace instead of StellaOps.ExportCenter.WebService.Api for IExportProfileRepository.
// This file is kept for reference only.

View File

@@ -1,133 +1,4 @@
using StellaOps.ExportCenter.Core.Domain;
namespace StellaOps.ExportCenter.WebService.Api;
/// <summary>
/// Repository for managing export runs.
/// </summary>
public interface IExportRunRepository
{
/// <summary>
/// Gets a run by ID for a tenant.
/// </summary>
Task<ExportRun?> GetByIdAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists runs for a tenant with optional filtering.
/// </summary>
Task<(IReadOnlyList<ExportRun> Items, int TotalCount)> ListAsync(
Guid tenantId,
Guid? profileId = null,
ExportRunStatus? status = null,
ExportRunTrigger? trigger = null,
DateTimeOffset? createdAfter = null,
DateTimeOffset? createdBefore = null,
string? correlationId = null,
int offset = 0,
int limit = 50,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new run.
/// </summary>
Task<ExportRun> CreateAsync(
ExportRun run,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates run status and progress.
/// </summary>
Task<ExportRun?> UpdateAsync(
ExportRun run,
CancellationToken cancellationToken = default);
/// <summary>
/// Cancels a run if it's in a cancellable state.
/// </summary>
Task<bool> CancelAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets active runs count for concurrency checks.
/// </summary>
Task<int> GetActiveRunsCountAsync(
Guid tenantId,
Guid? profileId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets queued runs count.
/// </summary>
Task<int> GetQueuedRunsCountAsync(
Guid tenantId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the next queued run to execute.
/// </summary>
Task<ExportRun?> DequeueNextRunAsync(
Guid tenantId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Repository for managing export artifacts.
/// </summary>
public interface IExportArtifactRepository
{
/// <summary>
/// Gets an artifact by ID.
/// </summary>
Task<ExportArtifact?> GetByIdAsync(
Guid tenantId,
Guid artifactId,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists artifacts for a run.
/// </summary>
Task<IReadOnlyList<ExportArtifact>> ListByRunAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new artifact record.
/// </summary>
Task<ExportArtifact> CreateAsync(
ExportArtifact artifact,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes artifacts for a run.
/// </summary>
Task<int> DeleteByRunAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Represents an export artifact.
/// </summary>
public sealed record ExportArtifact
{
public required Guid ArtifactId { get; init; }
public required Guid RunId { get; init; }
public required Guid TenantId { get; init; }
public required string Name { get; init; }
public required string Kind { get; init; }
public required string Path { get; init; }
public long SizeBytes { get; init; }
public string? ContentType { get; init; }
public required string Checksum { get; init; }
public string ChecksumAlgorithm { get; init; } = "SHA-256";
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public DateTimeOffset? ExpiresAt { get; init; }
}
// The IExportRunRepository, IExportArtifactRepository interfaces and ExportArtifact record
// have been moved to StellaOps.ExportCenter.Core.Persistence.
// Import that namespace instead of StellaOps.ExportCenter.WebService.Api.
// This file is kept for reference only.

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using StellaOps.ExportCenter.Core.Domain;
using StellaOps.ExportCenter.Core.Persistence;
using System.Collections.Concurrent;
namespace StellaOps.ExportCenter.WebService.Api;

View File

@@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using StellaOps.Determinism;
using StellaOps.ExportCenter.Core.Domain;
using StellaOps.ExportCenter.Core.Persistence;
using System.Globalization;
using System.Text.Json;

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Determinism;
using StellaOps.ExportCenter.Core.Persistence;
using StellaOps.ExportCenter.WebService.Distribution.Oci;
namespace StellaOps.ExportCenter.WebService.Distribution;

View File

@@ -1,4 +1,5 @@
using StellaOps.ExportCenter.Core.Domain;
using StellaOps.ExportCenter.Core.Persistence;
namespace StellaOps.ExportCenter.WebService.Distribution;

View File

@@ -1,112 +1,4 @@
using StellaOps.ExportCenter.Core.Domain;
namespace StellaOps.ExportCenter.WebService.Distribution;
/// <summary>
/// Repository for managing export distributions.
/// </summary>
public interface IExportDistributionRepository
{
/// <summary>
/// Gets a distribution by ID.
/// </summary>
Task<ExportDistribution?> GetByIdAsync(
Guid tenantId,
Guid distributionId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets a distribution by idempotency key.
/// </summary>
Task<ExportDistribution?> GetByIdempotencyKeyAsync(
Guid tenantId,
string idempotencyKey,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists distributions for a run.
/// </summary>
Task<IReadOnlyList<ExportDistribution>> ListByRunAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists distributions by status.
/// </summary>
Task<IReadOnlyList<ExportDistribution>> ListByStatusAsync(
Guid tenantId,
ExportDistributionStatus status,
int limit = 100,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists distributions due for retention deletion.
/// </summary>
Task<IReadOnlyList<ExportDistribution>> ListExpiredAsync(
DateTimeOffset asOf,
int limit = 100,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new distribution record.
/// </summary>
Task<ExportDistribution> CreateAsync(
ExportDistribution distribution,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates a distribution record.
/// Returns the updated record, or null if not found.
/// </summary>
Task<ExportDistribution?> UpdateAsync(
ExportDistribution distribution,
CancellationToken cancellationToken = default);
/// <summary>
/// Performs an idempotent upsert based on idempotency key.
/// Returns existing distribution if key matches, otherwise creates new.
/// </summary>
Task<(ExportDistribution Distribution, bool WasCreated)> UpsertByIdempotencyKeyAsync(
ExportDistribution distribution,
CancellationToken cancellationToken = default);
/// <summary>
/// Marks a distribution for deletion.
/// </summary>
Task<bool> MarkForDeletionAsync(
Guid tenantId,
Guid distributionId,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a distribution record and returns whether it existed.
/// </summary>
Task<bool> DeleteAsync(
Guid tenantId,
Guid distributionId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets distribution statistics for a run.
/// </summary>
Task<ExportDistributionStats> GetStatsAsync(
Guid tenantId,
Guid runId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Statistics for distributions of a run.
/// </summary>
public sealed record ExportDistributionStats
{
public int Total { get; init; }
public int Pending { get; init; }
public int Distributing { get; init; }
public int Distributed { get; init; }
public int Verified { get; init; }
public int Failed { get; init; }
public int Cancelled { get; init; }
public long TotalSizeBytes { get; init; }
}
// The IExportDistributionRepository interface and ExportDistributionStats record
// have been moved to StellaOps.ExportCenter.Core.Persistence.
// Import that namespace instead of StellaOps.ExportCenter.WebService.Distribution.
// This file is kept for reference only.

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Options;
using StellaOps.ExportCenter.Core.Domain;
using StellaOps.ExportCenter.Core.Persistence;
using System.Collections.Concurrent;
namespace StellaOps.ExportCenter.WebService.Distribution;

View File

@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization;
using StellaOps.AirGap.Policy;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.ExportCenter.WebService;
using StellaOps.ExportCenter.WebService.Api;
using StellaOps.ExportCenter.WebService.Attestation;
@@ -104,6 +105,7 @@ builder.Services.AddExportApiServices(options =>
builder.Services.AddOpenApi();
builder.Services.AddStellaOpsTenantServices();
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
// Stella Router integration
@@ -125,6 +127,7 @@ if (app.Environment.IsDevelopment())
app.UseStellaOpsCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
app.TryUseStellaRouter(routerEnabled);
// OpenAPI discovery endpoints (anonymous)
@@ -162,19 +165,22 @@ app.MapGet("/exports", () => Results.Ok(Array.Empty<object>()))
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportViewer)
.WithDeprecation(DeprecatedEndpointsRegistry.ListExports)
.WithSummary("List exports (DEPRECATED)")
.WithDescription("This endpoint is deprecated. Use GET /v1/exports/profiles instead.");
.WithDescription("This endpoint is deprecated. Use GET /v1/exports/profiles instead.")
.RequireTenant();
app.MapPost("/exports", () => Results.Accepted("/exports", new { status = "scheduled" }))
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportOperator)
.WithDeprecation(DeprecatedEndpointsRegistry.CreateExport)
.WithSummary("Create export (DEPRECATED)")
.WithDescription("This endpoint is deprecated. Use POST /v1/exports/evidence or /v1/exports/attestations instead.");
.WithDescription("This endpoint is deprecated. Use POST /v1/exports/evidence or /v1/exports/attestations instead.")
.RequireTenant();
app.MapDelete("/exports/{id}", (string id) => Results.NoContent())
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportAdmin)
.WithDeprecation(DeprecatedEndpointsRegistry.DeleteExport)
.WithSummary("Delete export (DEPRECATED)")
.WithDescription("This endpoint is deprecated. Use POST /v1/exports/runs/{id}/cancel instead.");
.WithDescription("This endpoint is deprecated. Use POST /v1/exports/runs/{id}/cancel instead.")
.RequireTenant();
// Refresh Router endpoint cache
app.TryRefreshStellaRouterEndpoints(routerEnabled);