up
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);
|
||||
323
src/Symbols/StellaOps.Symbols.Server/Program.cs
Normal file
323
src/Symbols/StellaOps.Symbols.Server/Program.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Symbols.Core.Abstractions;
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
using StellaOps.Symbols.Infrastructure;
|
||||
using StellaOps.Symbols.Server.Contracts;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Authentication and Authorization
|
||||
builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configure: options =>
|
||||
{
|
||||
options.RequiredScopes.Clear();
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.DefaultPolicy = new AuthorizationPolicyBuilder()
|
||||
.RequireAuthenticatedUser()
|
||||
.Build();
|
||||
options.FallbackPolicy = options.DefaultPolicy;
|
||||
});
|
||||
|
||||
// Symbols services (in-memory for development)
|
||||
builder.Services.AddSymbolsInMemory();
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
const string SymbolsReadPolicy = "symbols:read";
|
||||
const string SymbolsWritePolicy = "symbols:write";
|
||||
|
||||
// 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()
|
||||
.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()
|
||||
.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()
|
||||
.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()
|
||||
.WithName("ResolveSymbols")
|
||||
.WithSummary("Resolve symbol addresses")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// 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()
|
||||
.WithName("GetManifestsByDebugId")
|
||||
.WithSummary("Get manifests by debug ID")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
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)
|
||||
{
|
||||
// Simplified hash computation (should use BLAKE3 in production)
|
||||
var combined = $"{debugId}:{tenantId}:{symbols.Count}:{DateTimeOffset.UtcNow.Ticks}";
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(combined));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant()[..32];
|
||||
}
|
||||
|
||||
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,22 @@
|
||||
<?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>false</TreatWarningsAsErrors>
|
||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Symbols.Infrastructure\StellaOps.Symbols.Infrastructure.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>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user