consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Server.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Request to upload a symbol manifest.
|
||||
/// </summary>
|
||||
public sealed record UploadSymbolManifestRequest(
|
||||
string DebugId,
|
||||
string BinaryName,
|
||||
string? CodeId,
|
||||
string? Platform,
|
||||
BinaryFormat Format,
|
||||
IReadOnlyList<SymbolEntryDto> Symbols,
|
||||
IReadOnlyList<SourceMappingDto>? SourceMappings);
|
||||
|
||||
/// <summary>
|
||||
/// Symbol entry DTO for API.
|
||||
/// </summary>
|
||||
public sealed record SymbolEntryDto(
|
||||
ulong Address,
|
||||
ulong Size,
|
||||
string MangledName,
|
||||
string? DemangledName,
|
||||
SymbolType Type,
|
||||
SymbolBinding Binding,
|
||||
string? SourceFile,
|
||||
int? SourceLine,
|
||||
string? ContentHash);
|
||||
|
||||
/// <summary>
|
||||
/// Source mapping DTO for API.
|
||||
/// </summary>
|
||||
public sealed record SourceMappingDto(
|
||||
string CompiledPath,
|
||||
string SourcePath,
|
||||
string? ContentHash);
|
||||
|
||||
/// <summary>
|
||||
/// Response from manifest upload.
|
||||
/// </summary>
|
||||
public sealed record UploadSymbolManifestResponse(
|
||||
string ManifestId,
|
||||
string DebugId,
|
||||
string BinaryName,
|
||||
string? BlobUri,
|
||||
int SymbolCount,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Request to resolve symbols.
|
||||
/// </summary>
|
||||
public sealed record ResolveSymbolsRequest(
|
||||
string DebugId,
|
||||
IReadOnlyList<ulong> Addresses);
|
||||
|
||||
/// <summary>
|
||||
/// Response from symbol resolution.
|
||||
/// </summary>
|
||||
public sealed record ResolveSymbolsResponse(
|
||||
string DebugId,
|
||||
IReadOnlyList<SymbolResolutionDto> Resolutions);
|
||||
|
||||
/// <summary>
|
||||
/// Symbol resolution DTO.
|
||||
/// </summary>
|
||||
public sealed record SymbolResolutionDto(
|
||||
ulong Address,
|
||||
bool Found,
|
||||
string? MangledName,
|
||||
string? DemangledName,
|
||||
ulong Offset,
|
||||
string? SourceFile,
|
||||
int? SourceLine,
|
||||
double Confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Symbol manifest list response.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestListResponse(
|
||||
IReadOnlyList<SymbolManifestSummary> Manifests,
|
||||
int TotalCount,
|
||||
int Offset,
|
||||
int Limit);
|
||||
|
||||
/// <summary>
|
||||
/// Summary of a symbol manifest.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestSummary(
|
||||
string ManifestId,
|
||||
string DebugId,
|
||||
string? CodeId,
|
||||
string BinaryName,
|
||||
string? Platform,
|
||||
BinaryFormat Format,
|
||||
int SymbolCount,
|
||||
bool HasDsse,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Detailed manifest response.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestDetailResponse(
|
||||
string ManifestId,
|
||||
string DebugId,
|
||||
string? CodeId,
|
||||
string BinaryName,
|
||||
string? Platform,
|
||||
BinaryFormat Format,
|
||||
string TenantId,
|
||||
string? BlobUri,
|
||||
string? DsseDigest,
|
||||
long? RekorLogIndex,
|
||||
int SymbolCount,
|
||||
IReadOnlyList<SymbolEntryDto> Symbols,
|
||||
IReadOnlyList<SourceMappingDto>? SourceMappings,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Health check response.
|
||||
/// </summary>
|
||||
public sealed record SymbolsHealthResponse(
|
||||
string Status,
|
||||
string Version,
|
||||
DateTimeOffset Timestamp,
|
||||
SymbolsHealthMetrics? Metrics);
|
||||
|
||||
/// <summary>
|
||||
/// Health metrics.
|
||||
/// </summary>
|
||||
public sealed record SymbolsHealthMetrics(
|
||||
long TotalManifests,
|
||||
long TotalSymbols,
|
||||
long TotalBlobBytes);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
343
src/BinaryIndex/StellaOps.Symbols.Server/Program.cs
Normal file
343
src/BinaryIndex/StellaOps.Symbols.Server/Program.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
using StellaOps.Symbols.Core.Abstractions;
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
using StellaOps.Symbols.Infrastructure;
|
||||
using StellaOps.Symbols.Infrastructure.Hashing;
|
||||
using StellaOps.Symbols.Marketplace.Scoring;
|
||||
using StellaOps.Symbols.Server.Contracts;
|
||||
using StellaOps.Symbols.Server.Endpoints;
|
||||
using StellaOps.Symbols.Server.Security;
|
||||
|
||||
using StellaOps.Router.AspNet;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Authentication and Authorization
|
||||
builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configure: options =>
|
||||
{
|
||||
options.RequiredScopes.Clear();
|
||||
});
|
||||
builder.Services.AddStellaOpsTenantServices();
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddStellaOpsScopePolicy(SymbolsPolicies.Read, StellaOpsScopes.SymbolsRead);
|
||||
options.AddStellaOpsScopePolicy(SymbolsPolicies.Write, StellaOpsScopes.SymbolsWrite);
|
||||
});
|
||||
|
||||
// Symbols services (in-memory for development)
|
||||
builder.Services.AddSymbolsInMemory();
|
||||
|
||||
// Marketplace services
|
||||
builder.Services.AddSingleton<ISymbolSourceTrustScorer, DefaultSymbolSourceTrustScorer>();
|
||||
builder.Services.AddSingleton<StellaOps.Symbols.Marketplace.Repositories.ISymbolSourceReadRepository, StellaOps.Symbols.Server.Endpoints.InMemorySymbolSourceReadRepository>();
|
||||
builder.Services.AddSingleton<StellaOps.Symbols.Marketplace.Repositories.IMarketplaceCatalogRepository, StellaOps.Symbols.Server.Endpoints.InMemoryMarketplaceCatalogRepository>();
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
// Stella Router integration
|
||||
var routerEnabled = builder.Services.AddRouterMicroservice(
|
||||
builder.Configuration,
|
||||
serviceName: "symbols",
|
||||
version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0",
|
||||
routerOptionsSection: "Router");
|
||||
builder.TryAddStellaOpsLocalBinding("symbols");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("symbols");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseStellaOpsTenantMiddleware();
|
||||
app.TryUseStellaRouter(routerEnabled);
|
||||
|
||||
// Health endpoint (anonymous)
|
||||
app.MapGet("/health", () =>
|
||||
{
|
||||
return TypedResults.Ok(new SymbolsHealthResponse(
|
||||
Status: "healthy",
|
||||
Version: "1.0.0",
|
||||
Timestamp: DateTimeOffset.UtcNow,
|
||||
Metrics: null));
|
||||
})
|
||||
.AllowAnonymous()
|
||||
.WithName("GetHealth")
|
||||
.WithSummary("Health check endpoint");
|
||||
|
||||
// Upload symbol manifest
|
||||
app.MapPost("/v1/symbols/manifests", async Task<Results<Created<UploadSymbolManifestResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
UploadSymbolManifestRequest request,
|
||||
ISymbolRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var symbols = request.Symbols.Select(s => new SymbolEntry
|
||||
{
|
||||
Address = s.Address,
|
||||
Size = s.Size,
|
||||
MangledName = s.MangledName,
|
||||
DemangledName = s.DemangledName,
|
||||
Type = s.Type,
|
||||
Binding = s.Binding,
|
||||
SourceFile = s.SourceFile,
|
||||
SourceLine = s.SourceLine,
|
||||
ContentHash = s.ContentHash
|
||||
}).ToList();
|
||||
|
||||
var sourceMappings = request.SourceMappings?.Select(m => new SourceMapping
|
||||
{
|
||||
CompiledPath = m.CompiledPath,
|
||||
SourcePath = m.SourcePath,
|
||||
ContentHash = m.ContentHash
|
||||
}).ToList();
|
||||
|
||||
var manifestId = ComputeManifestId(request.DebugId, tenantId, symbols);
|
||||
|
||||
var manifest = new SymbolManifest
|
||||
{
|
||||
ManifestId = manifestId,
|
||||
DebugId = request.DebugId,
|
||||
CodeId = request.CodeId,
|
||||
BinaryName = request.BinaryName,
|
||||
Platform = request.Platform,
|
||||
Format = request.Format,
|
||||
Symbols = symbols,
|
||||
SourceMappings = sourceMappings,
|
||||
TenantId = tenantId,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await repository.StoreManifestAsync(manifest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new UploadSymbolManifestResponse(
|
||||
ManifestId: manifestId,
|
||||
DebugId: request.DebugId,
|
||||
BinaryName: request.BinaryName,
|
||||
BlobUri: manifest.BlobUri,
|
||||
SymbolCount: symbols.Count,
|
||||
CreatedAt: manifest.CreatedAt);
|
||||
|
||||
return TypedResults.Created($"/v1/symbols/manifests/{manifestId}", response);
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Write)
|
||||
.WithName("UploadSymbolManifest")
|
||||
.WithSummary("Upload a symbol manifest")
|
||||
.Produces(StatusCodes.Status201Created)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Get manifest by ID
|
||||
app.MapGet("/v1/symbols/manifests/{manifestId}", async Task<Results<Ok<SymbolManifestDetailResponse>, NotFound, ProblemHttpResult>> (
|
||||
string manifestId,
|
||||
ISymbolRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var manifest = await repository.GetManifestAsync(manifestId, cancellationToken).ConfigureAwait(false);
|
||||
if (manifest is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var response = MapToDetailResponse(manifest);
|
||||
return TypedResults.Ok(response);
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.WithName("GetSymbolManifest")
|
||||
.WithSummary("Get symbol manifest by ID")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Query manifests
|
||||
app.MapGet("/v1/symbols/manifests", async Task<Results<Ok<SymbolManifestListResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
ISymbolRepository repository,
|
||||
string? debugId,
|
||||
string? codeId,
|
||||
string? binaryName,
|
||||
string? platform,
|
||||
int? limit,
|
||||
int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var query = new SymbolQuery
|
||||
{
|
||||
TenantId = tenantId,
|
||||
DebugId = debugId,
|
||||
CodeId = codeId,
|
||||
BinaryName = binaryName,
|
||||
Platform = platform,
|
||||
Limit = limit ?? 50,
|
||||
Offset = offset ?? 0
|
||||
};
|
||||
|
||||
var result = await repository.QueryManifestsAsync(query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var summaries = result.Manifests.Select(m => new SymbolManifestSummary(
|
||||
ManifestId: m.ManifestId,
|
||||
DebugId: m.DebugId,
|
||||
CodeId: m.CodeId,
|
||||
BinaryName: m.BinaryName,
|
||||
Platform: m.Platform,
|
||||
Format: m.Format,
|
||||
SymbolCount: m.Symbols.Count,
|
||||
HasDsse: !string.IsNullOrEmpty(m.DsseDigest),
|
||||
CreatedAt: m.CreatedAt)).ToList();
|
||||
|
||||
return TypedResults.Ok(new SymbolManifestListResponse(
|
||||
Manifests: summaries,
|
||||
TotalCount: result.TotalCount,
|
||||
Offset: result.Offset,
|
||||
Limit: result.Limit));
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.WithName("QuerySymbolManifests")
|
||||
.WithSummary("Query symbol manifests")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Resolve symbols
|
||||
app.MapPost("/v1/symbols/resolve", async Task<Results<Ok<ResolveSymbolsResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
ResolveSymbolsRequest request,
|
||||
ISymbolResolver resolver,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var resolutions = await resolver.ResolveBatchAsync(
|
||||
request.DebugId,
|
||||
request.Addresses,
|
||||
tenantId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var dtos = resolutions.Select(r => new SymbolResolutionDto(
|
||||
Address: r.Address,
|
||||
Found: r.Found,
|
||||
MangledName: r.Symbol?.MangledName,
|
||||
DemangledName: r.Symbol?.DemangledName,
|
||||
Offset: r.Offset,
|
||||
SourceFile: r.Symbol?.SourceFile,
|
||||
SourceLine: r.Symbol?.SourceLine,
|
||||
Confidence: r.Confidence)).ToList();
|
||||
|
||||
return TypedResults.Ok(new ResolveSymbolsResponse(
|
||||
DebugId: request.DebugId,
|
||||
Resolutions: dtos));
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.WithName("ResolveSymbols")
|
||||
.WithSummary("Resolve symbol addresses")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Symbol source and marketplace endpoints
|
||||
app.MapSymbolSourceEndpoints();
|
||||
|
||||
// Get manifests by debug ID
|
||||
app.MapGet("/v1/symbols/by-debug-id/{debugId}", async Task<Results<Ok<SymbolManifestListResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
string debugId,
|
||||
ISymbolRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var manifests = await repository.GetManifestsByDebugIdAsync(debugId, tenantId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var summaries = manifests.Select(m => new SymbolManifestSummary(
|
||||
ManifestId: m.ManifestId,
|
||||
DebugId: m.DebugId,
|
||||
CodeId: m.CodeId,
|
||||
BinaryName: m.BinaryName,
|
||||
Platform: m.Platform,
|
||||
Format: m.Format,
|
||||
SymbolCount: m.Symbols.Count,
|
||||
HasDsse: !string.IsNullOrEmpty(m.DsseDigest),
|
||||
CreatedAt: m.CreatedAt)).ToList();
|
||||
|
||||
return TypedResults.Ok(new SymbolManifestListResponse(
|
||||
Manifests: summaries,
|
||||
TotalCount: summaries.Count,
|
||||
Offset: 0,
|
||||
Limit: summaries.Count));
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.WithName("GetManifestsByDebugId")
|
||||
.WithSummary("Get manifests by debug ID")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.TryRefreshStellaRouterEndpoints(routerEnabled);
|
||||
app.Run();
|
||||
|
||||
static bool TryGetTenant(HttpContext httpContext, out ProblemHttpResult? problem, out string tenantId)
|
||||
{
|
||||
tenantId = string.Empty;
|
||||
if (!httpContext.Request.Headers.TryGetValue("X-Stella-Tenant", out var tenantValues) ||
|
||||
string.IsNullOrWhiteSpace(tenantValues))
|
||||
{
|
||||
problem = TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_tenant");
|
||||
return false;
|
||||
}
|
||||
|
||||
tenantId = tenantValues.ToString();
|
||||
problem = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
static string ComputeManifestId(string debugId, string tenantId, IReadOnlyList<SymbolEntry> symbols)
|
||||
{
|
||||
return SymbolHashing.ComputeManifestId(debugId, tenantId, symbols);
|
||||
}
|
||||
|
||||
static SymbolManifestDetailResponse MapToDetailResponse(SymbolManifest manifest)
|
||||
{
|
||||
return new SymbolManifestDetailResponse(
|
||||
ManifestId: manifest.ManifestId,
|
||||
DebugId: manifest.DebugId,
|
||||
CodeId: manifest.CodeId,
|
||||
BinaryName: manifest.BinaryName,
|
||||
Platform: manifest.Platform,
|
||||
Format: manifest.Format,
|
||||
TenantId: manifest.TenantId,
|
||||
BlobUri: manifest.BlobUri,
|
||||
DsseDigest: manifest.DsseDigest,
|
||||
RekorLogIndex: manifest.RekorLogIndex,
|
||||
SymbolCount: manifest.Symbols.Count,
|
||||
Symbols: manifest.Symbols.Select(s => new SymbolEntryDto(
|
||||
s.Address, s.Size, s.MangledName, s.DemangledName,
|
||||
s.Type, s.Binding, s.SourceFile, s.SourceLine, s.ContentHash)).ToList(),
|
||||
SourceMappings: manifest.SourceMappings?.Select(m => new SourceMappingDto(
|
||||
m.CompiledPath, m.SourcePath, m.ContentHash)).ToList(),
|
||||
CreatedAt: manifest.CreatedAt);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"profiles": {
|
||||
"StellaOps.Symbols.Server": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:10380;http://localhost:10381"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
namespace StellaOps.Symbols.Server.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Named authorization policy constants for the Symbols service.
|
||||
/// Policies are registered via AddStellaOpsScopePolicy in Program.cs.
|
||||
/// </summary>
|
||||
internal static class SymbolsPolicies
|
||||
{
|
||||
/// <summary>Policy for querying symbol manifests. Requires symbols:read scope.</summary>
|
||||
public const string Read = "Symbols.Read";
|
||||
|
||||
/// <summary>Policy for uploading symbol manifests and resolving symbols. Requires symbols:write scope.</summary>
|
||||
public const string Write = "Symbols.Write";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Symbols.Infrastructure\StellaOps.Symbols.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Symbols.Marketplace\StellaOps.Symbols.Marketplace.csproj" />
|
||||
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="StellaOpsReleaseVersion">
|
||||
<Version>1.0.0-alpha1</Version>
|
||||
<InformationalVersion>1.0.0-alpha1</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
8
src/BinaryIndex/StellaOps.Symbols.Server/TASKS.md
Normal file
8
src/BinaryIndex/StellaOps.Symbols.Server/TASKS.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# StellaOps.Symbols.Server Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Symbols/StellaOps.Symbols.Server/StellaOps.Symbols.Server.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
Reference in New Issue
Block a user