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(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); 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.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, 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, 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, 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, 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, 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 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); }