consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,133 @@
using StellaOps.Symbols.Marketplace.Models;
using StellaOps.Symbols.Marketplace.Repositories;
namespace StellaOps.Symbols.Server.Endpoints;
/// <summary>
/// In-memory marketplace catalog repository for development.
/// </summary>
internal sealed class InMemoryMarketplaceCatalogRepository : IMarketplaceCatalogRepository
{
private readonly List<SymbolPackCatalogEntry> _catalog =
[
new()
{
Id = Guid.Parse("b0000000-0000-0000-0000-000000000001"),
SourceId = Guid.Parse("a0000000-0000-0000-0000-000000000001"),
PackId = "pkg:nuget/System.Runtime@10.0.0",
Platform = "any",
Components = ["System.Runtime"],
DsseDigest = "sha256:aabbccdd",
Version = "10.0.0",
SizeBytes = 5_200_000,
Installed = false,
PublishedAt = DateTimeOffset.UtcNow.AddDays(-7),
},
new()
{
Id = Guid.Parse("b0000000-0000-0000-0000-000000000002"),
SourceId = Guid.Parse("a0000000-0000-0000-0000-000000000002"),
PackId = "pkg:deb/ubuntu/libc6-dbg@2.35-0ubuntu3",
Platform = "linux/amd64",
Components = ["libc6", "ld-linux"],
DsseDigest = "sha256:11223344",
Version = "2.35-0ubuntu3",
SizeBytes = 15_000_000,
Installed = true,
PublishedAt = DateTimeOffset.UtcNow.AddDays(-14),
InstalledAt = DateTimeOffset.UtcNow.AddDays(-3),
},
];
private readonly HashSet<string> _installedKeys = new(["default:b0000000-0000-0000-0000-000000000002"]);
public Task<IReadOnlyList<SymbolPackCatalogEntry>> ListCatalogAsync(
Guid? sourceId,
string? search,
int limit,
int offset,
CancellationToken cancellationToken = default)
{
var query = _catalog.AsEnumerable();
if (sourceId.HasValue)
{
query = query.Where(e => e.SourceId == sourceId.Value);
}
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim();
query = query.Where(e =>
e.PackId.Contains(term, StringComparison.OrdinalIgnoreCase) ||
e.Platform.Contains(term, StringComparison.OrdinalIgnoreCase) ||
e.Components.Any(c => c.Contains(term, StringComparison.OrdinalIgnoreCase)));
}
IReadOnlyList<SymbolPackCatalogEntry> result = query
.Skip(offset)
.Take(limit)
.ToList();
return Task.FromResult(result);
}
public Task<SymbolPackCatalogEntry?> GetCatalogEntryAsync(
Guid entryId,
CancellationToken cancellationToken = default)
{
var entry = _catalog.FirstOrDefault(e => e.Id == entryId);
return Task.FromResult(entry);
}
public Task InstallPackAsync(
Guid entryId,
string tenantId,
CancellationToken cancellationToken = default)
{
_installedKeys.Add($"{tenantId}:{entryId}");
var idx = _catalog.FindIndex(e => e.Id == entryId);
if (idx >= 0)
{
_catalog[idx] = _catalog[idx] with
{
Installed = true,
InstalledAt = DateTimeOffset.UtcNow,
};
}
return Task.CompletedTask;
}
public Task UninstallPackAsync(
Guid entryId,
string tenantId,
CancellationToken cancellationToken = default)
{
_installedKeys.Remove($"{tenantId}:{entryId}");
var idx = _catalog.FindIndex(e => e.Id == entryId);
if (idx >= 0)
{
_catalog[idx] = _catalog[idx] with
{
Installed = false,
InstalledAt = null,
};
}
return Task.CompletedTask;
}
public Task<IReadOnlyList<SymbolPackCatalogEntry>> ListInstalledAsync(
string tenantId,
CancellationToken cancellationToken = default)
{
IReadOnlyList<SymbolPackCatalogEntry> result = _catalog
.Where(e => _installedKeys.Contains($"{tenantId}:{e.Id}"))
.ToList();
return Task.FromResult(result);
}
}

View File

@@ -0,0 +1,77 @@
using StellaOps.Symbols.Marketplace.Models;
using StellaOps.Symbols.Marketplace.Repositories;
namespace StellaOps.Symbols.Server.Endpoints;
/// <summary>
/// In-memory symbol source read repository for development.
/// </summary>
internal sealed class InMemorySymbolSourceReadRepository : ISymbolSourceReadRepository
{
private readonly List<SymbolSourceFreshnessRecord> _sources =
[
new(
SourceId: Guid.Parse("a0000000-0000-0000-0000-000000000001"),
SourceKey: "microsoft-symbols",
SourceName: "Microsoft Public Symbols",
SourceType: "vendor",
SourceUrl: "https://msdl.microsoft.com/download/symbols",
Priority: 1,
Enabled: true,
LastSyncAt: DateTimeOffset.UtcNow.AddMinutes(-30),
LastSuccessAt: DateTimeOffset.UtcNow.AddMinutes(-30),
LastError: null,
SyncCount: 120,
ErrorCount: 2,
FreshnessSlaSeconds: 21600,
WarningRatio: 0.80m,
FreshnessAgeSeconds: 1800,
FreshnessStatus: "healthy",
SignatureStatus: "signed",
TotalPacks: 450,
SignedPacks: 445,
UnsignedPacks: 5,
SignatureFailureCount: 0),
new(
SourceId: Guid.Parse("a0000000-0000-0000-0000-000000000002"),
SourceKey: "ubuntu-debuginfod",
SourceName: "Ubuntu Debuginfod",
SourceType: "distro",
SourceUrl: "https://debuginfod.ubuntu.com",
Priority: 2,
Enabled: true,
LastSyncAt: DateTimeOffset.UtcNow.AddHours(-2),
LastSuccessAt: DateTimeOffset.UtcNow.AddHours(-2),
LastError: null,
SyncCount: 85,
ErrorCount: 5,
FreshnessSlaSeconds: 21600,
WarningRatio: 0.80m,
FreshnessAgeSeconds: 7200,
FreshnessStatus: "healthy",
SignatureStatus: "signed",
TotalPacks: 280,
SignedPacks: 260,
UnsignedPacks: 20,
SignatureFailureCount: 1),
];
public Task<IReadOnlyList<SymbolSourceFreshnessRecord>> ListSourcesAsync(
bool includeDisabled,
CancellationToken cancellationToken = default)
{
IReadOnlyList<SymbolSourceFreshnessRecord> result = includeDisabled
? _sources
: _sources.Where(s => s.Enabled).ToList();
return Task.FromResult(result);
}
public Task<SymbolSourceFreshnessRecord?> GetSourceByIdAsync(
Guid sourceId,
CancellationToken cancellationToken = default)
{
var source = _sources.FirstOrDefault(s => s.SourceId == sourceId);
return Task.FromResult(source);
}
}

View File

@@ -0,0 +1,355 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using StellaOps.Symbols.Marketplace.Models;
using StellaOps.Symbols.Marketplace.Repositories;
using StellaOps.Symbols.Marketplace.Scoring;
using StellaOps.Symbols.Server.Security;
using StellaOps.Auth.ServerIntegration.Tenancy;
namespace StellaOps.Symbols.Server.Endpoints;
/// <summary>
/// Symbol source and marketplace catalog endpoints.
/// </summary>
public static class SymbolSourceEndpoints
{
private const int DefaultLimit = 50;
private const int MaxLimit = 200;
public static IEndpointRouteBuilder MapSymbolSourceEndpoints(this IEndpointRouteBuilder app)
{
// --- Symbol Sources ---
var sources = app.MapGroup("/api/v1/symbols/sources")
.WithTags("Symbol Sources")
.RequireAuthorization(SymbolsPolicies.Read)
.RequireTenant();
sources.MapGet(string.Empty, async (
ISymbolSourceReadRepository repository,
[FromQuery] bool includeDisabled,
CancellationToken cancellationToken) =>
{
var items = await repository.ListSourcesAsync(includeDisabled, cancellationToken)
.ConfigureAwait(false);
return Results.Ok(new
{
items,
totalCount = items.Count,
dataAsOf = DateTimeOffset.UtcNow,
});
})
.WithName("ListSymbolSources")
.WithSummary("List symbol sources with freshness projections")
.WithDescription("Returns all configured symbol pack sources with their freshness projections, sync state, and trust metadata. Optionally includes disabled sources when includeDisabled is true. Requires authentication.");
sources.MapGet("/summary", async (
ISymbolSourceReadRepository repository,
ISymbolSourceTrustScorer scorer,
CancellationToken cancellationToken) =>
{
var items = await repository.ListSourcesAsync(false, cancellationToken)
.ConfigureAwait(false);
var healthy = items.Count(s => s.FreshnessStatus == "healthy");
var warning = items.Count(s => s.FreshnessStatus == "warning");
var stale = items.Count(s => s.FreshnessStatus == "stale");
var unavailable = items.Count(s => s.FreshnessStatus == "unavailable");
var avgTrust = items.Count > 0
? items.Average(s => scorer.CalculateTrust(s).Overall)
: 0.0;
return Results.Ok(new
{
totalSources = items.Count,
healthySources = healthy,
warningSources = warning,
staleSources = stale,
unavailableSources = unavailable,
averageTrustScore = Math.Round(avgTrust, 4),
dataAsOf = DateTimeOffset.UtcNow,
});
})
.WithName("GetSymbolSourceSummary")
.WithSummary("Get symbol source summary cards")
.WithDescription("Returns aggregated health summary cards for all enabled symbol sources including counts of healthy, warning, stale, and unavailable sources and the average trust score across the fleet. Requires authentication.");
sources.MapGet("/{id:guid}", async (
Guid id,
ISymbolSourceReadRepository repository,
ISymbolSourceTrustScorer scorer,
CancellationToken cancellationToken) =>
{
var source = await repository.GetSourceByIdAsync(id, cancellationToken)
.ConfigureAwait(false);
if (source is null)
{
return Results.NotFound(new { error = "source_not_found", id });
}
var trust = scorer.CalculateTrust(source);
return Results.Ok(new
{
source,
trust,
dataAsOf = DateTimeOffset.UtcNow,
});
})
.WithName("GetSymbolSource")
.WithSummary("Get symbol source detail with trust score")
.WithDescription("Returns the full symbol source record by ID including its sync state, freshness projection, and computed trust score breakdown. Returns 404 if the source is not found. Requires authentication.");
sources.MapGet("/{id:guid}/freshness", async (
Guid id,
ISymbolSourceReadRepository repository,
CancellationToken cancellationToken) =>
{
var source = await repository.GetSourceByIdAsync(id, cancellationToken)
.ConfigureAwait(false);
if (source is null)
{
return Results.NotFound(new { error = "source_not_found", id });
}
return Results.Ok(new
{
source.SourceId,
source.SourceKey,
source.FreshnessStatus,
source.FreshnessAgeSeconds,
source.FreshnessSlaSeconds,
source.LastSyncAt,
source.LastSuccessAt,
source.LastError,
source.SyncCount,
source.ErrorCount,
dataAsOf = DateTimeOffset.UtcNow,
});
})
.WithName("GetSymbolSourceFreshness")
.WithSummary("Get symbol source freshness detail")
.WithDescription("Returns freshness detail for a specific symbol source including status, age in seconds, SLA threshold, last sync time, last successful sync, last error, and cumulative sync and error counts. Returns 404 if the source is not found. Requires authentication.");
sources.MapPost(string.Empty, (SymbolPackSource request) =>
{
// Placeholder: in production, persist via write repository.
var created = request with
{
Id = request.Id == Guid.Empty ? Guid.NewGuid() : request.Id,
CreatedAt = DateTimeOffset.UtcNow,
};
return Results.Created($"/api/v1/symbols/sources/{created.Id}", created);
})
.WithName("CreateSymbolSource")
.WithSummary("Create a new symbol source")
.WithDescription("Creates a new symbol pack source with the provided configuration. Assigns a new ID if not supplied. Returns 201 Created with the created source record. Requires authentication.");
sources.MapPut("/{id:guid}", (Guid id, SymbolPackSource request) =>
{
var updated = request with
{
Id = id,
UpdatedAt = DateTimeOffset.UtcNow,
};
return Results.Ok(updated);
})
.WithName("UpdateSymbolSource")
.WithSummary("Update a symbol source")
.WithDescription("Replaces the configuration of an existing symbol source by ID, updating its metadata, freshness SLA, and enabled state. Returns 200 with the updated source record. Requires authentication.");
sources.MapDelete("/{id:guid}", (Guid id) =>
{
return Results.NoContent();
})
.WithName("DisableSymbolSource")
.WithSummary("Disable (soft-delete) a symbol source")
.WithDescription("Soft-deletes (disables) a symbol source by ID, preventing it from appearing in default listings without permanently removing its history. Returns 204 No Content on success. Requires authentication.");
// --- Marketplace Catalog ---
var marketplace = app.MapGroup("/api/v1/symbols/marketplace")
.WithTags("Symbol Marketplace")
.RequireAuthorization(SymbolsPolicies.Read)
.RequireTenant();
marketplace.MapGet(string.Empty, async (
IMarketplaceCatalogRepository repository,
[FromQuery] Guid? sourceId,
[FromQuery] string? search,
[FromQuery] int? limit,
[FromQuery] int? offset,
CancellationToken cancellationToken) =>
{
var normalizedLimit = NormalizeLimit(limit);
var normalizedOffset = NormalizeOffset(offset);
var items = await repository.ListCatalogAsync(
sourceId, search, normalizedLimit, normalizedOffset, cancellationToken)
.ConfigureAwait(false);
return Results.Ok(new
{
items,
totalCount = items.Count,
limit = normalizedLimit,
offset = normalizedOffset,
dataAsOf = DateTimeOffset.UtcNow,
});
})
.WithName("ListMarketplaceCatalog")
.WithSummary("List symbol pack catalog entries")
.WithDescription("Returns a paginated list of symbol pack catalog entries, optionally filtered by source ID and a free-text search term. Results include pack metadata and are bounded by the configured limit and offset. Requires authentication.");
marketplace.MapGet("/search", async (
IMarketplaceCatalogRepository repository,
[FromQuery] string? q,
[FromQuery] string? platform,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
var normalizedLimit = NormalizeLimit(limit);
var searchTerm = string.IsNullOrWhiteSpace(q) ? platform : q;
var items = await repository.ListCatalogAsync(
null, searchTerm, normalizedLimit, 0, cancellationToken)
.ConfigureAwait(false);
return Results.Ok(new
{
items,
totalCount = items.Count,
dataAsOf = DateTimeOffset.UtcNow,
});
})
.WithName("SearchMarketplaceCatalog")
.WithSummary("Search catalog by PURL or platform")
.WithDescription("Searches the symbol pack marketplace catalog by a free-text query (q) or platform string. Falls back to platform if q is empty. Returns matching catalog entries up to the specified limit. Requires authentication.");
marketplace.MapGet("/{entryId:guid}", async (
Guid entryId,
IMarketplaceCatalogRepository repository,
CancellationToken cancellationToken) =>
{
var entry = await repository.GetCatalogEntryAsync(entryId, cancellationToken)
.ConfigureAwait(false);
if (entry is null)
{
return Results.NotFound(new { error = "catalog_entry_not_found", entryId });
}
return Results.Ok(new { entry, dataAsOf = DateTimeOffset.UtcNow });
})
.WithName("GetMarketplaceCatalogEntry")
.WithSummary("Get catalog entry detail")
.WithDescription("Returns the full catalog entry record for a specific marketplace entry ID including pack metadata, publisher, version, and install eligibility. Returns 404 if the entry is not found. Requires authentication.");
marketplace.MapPost("/{entryId:guid}/install", async (
HttpContext httpContext,
Guid entryId,
IMarketplaceCatalogRepository repository,
CancellationToken cancellationToken) =>
{
var tenantId = httpContext.Request.Headers["X-Stella-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "missing_tenant" });
}
await repository.InstallPackAsync(entryId, tenantId, cancellationToken)
.ConfigureAwait(false);
return Results.Ok(new { entryId, status = "installed", dataAsOf = DateTimeOffset.UtcNow });
})
.WithName("InstallMarketplacePack")
.WithSummary("Install a symbol pack from the marketplace")
.WithDescription("Installs a symbol pack from the marketplace catalog for the requesting tenant, recording the installation against the specified catalog entry ID. Returns 200 with the installation status. Returns 400 if the X-Stella-Tenant header is missing. Requires authentication.");
marketplace.MapPost("/{entryId:guid}/uninstall", async (
HttpContext httpContext,
Guid entryId,
IMarketplaceCatalogRepository repository,
CancellationToken cancellationToken) =>
{
var tenantId = httpContext.Request.Headers["X-Stella-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "missing_tenant" });
}
await repository.UninstallPackAsync(entryId, tenantId, cancellationToken)
.ConfigureAwait(false);
return Results.Ok(new { entryId, status = "uninstalled", dataAsOf = DateTimeOffset.UtcNow });
})
.WithName("UninstallMarketplacePack")
.WithSummary("Uninstall a symbol pack")
.WithDescription("Removes the installation of a symbol pack for the requesting tenant by catalog entry ID. Returns 200 with the uninstall status. Returns 400 if the X-Stella-Tenant header is missing. Requires authentication.");
marketplace.MapGet("/installed", async (
HttpContext httpContext,
IMarketplaceCatalogRepository repository,
CancellationToken cancellationToken) =>
{
var tenantId = httpContext.Request.Headers["X-Stella-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "missing_tenant" });
}
var items = await repository.ListInstalledAsync(tenantId, cancellationToken)
.ConfigureAwait(false);
return Results.Ok(new
{
items,
totalCount = items.Count,
dataAsOf = DateTimeOffset.UtcNow,
});
})
.WithName("ListInstalledPacks")
.WithSummary("List installed symbol packs")
.WithDescription("Returns all symbol packs currently installed for the requesting tenant. Returns 400 if the X-Stella-Tenant header is missing. Requires authentication.");
marketplace.MapPost("/sync", (HttpContext httpContext) =>
{
var tenantId = httpContext.Request.Headers["X-Stella-Tenant"].ToString();
if (string.IsNullOrWhiteSpace(tenantId))
{
return Results.BadRequest(new { error = "missing_tenant" });
}
return Results.Accepted(value: new
{
status = "sync_queued",
tenantId,
dataAsOf = DateTimeOffset.UtcNow,
});
})
.WithName("TriggerMarketplaceSync")
.WithSummary("Trigger marketplace sync from configured sources")
.WithDescription("Enqueues a marketplace sync job to refresh the symbol pack catalog from all configured sources for the requesting tenant. Returns 202 Accepted with the queued status. Returns 400 if the X-Stella-Tenant header is missing. Requires authentication.");
return app;
}
private static int NormalizeLimit(int? value)
{
return value switch
{
null => DefaultLimit,
< 1 => 1,
> MaxLimit => MaxLimit,
_ => value.Value,
};
}
private static int NormalizeOffset(int? value) => value is null or < 0 ? 0 : value.Value;
}