// Licensed to StellaOps under the BUSL-1.1 license. using StellaOps.Auth.ServerIntegration; using Microsoft.AspNetCore.RateLimiting; using Npgsql; using StackExchange.Redis; using StellaOps.ReachGraph.Cache; using StellaOps.ReachGraph.Hashing; using StellaOps.ReachGraph.Persistence; using StellaOps.ReachGraph.Serialization; using StellaOps.ReachGraph.WebService.Services; using System.Threading.RateLimiting; var builder = WebApplication.CreateBuilder(args); // Add services builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new() { Title = "ReachGraph Store API", Version = "v1", Description = "Content-addressed storage for reachability subgraphs" }); }); // PostgreSQL (lazy so integration tests can replace before first resolve) builder.Services.AddSingleton(sp => { var config = sp.GetRequiredService(); var connStr = config.GetConnectionString("PostgreSQL") ?? throw new InvalidOperationException("PostgreSQL connection string not configured"); return new NpgsqlDataSourceBuilder(connStr).Build(); }); // Redis/Valkey (lazy so integration tests can replace before first resolve) builder.Services.AddSingleton(sp => { var config = sp.GetRequiredService(); var redisConnStr = config.GetConnectionString("Redis") ?? "localhost:6379"; return ConnectionMultiplexer.Connect(redisConnStr); }); // Core services builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Persistence builder.Services.AddScoped(); // Cache builder.Services.Configure( builder.Configuration.GetSection("ReachGraphCache")); builder.Services.AddScoped(sp => { var redisMultiplexer = sp.GetRequiredService(); var serializer = sp.GetRequiredService(); var options = Microsoft.Extensions.Options.Options.Create( builder.Configuration.GetSection("ReachGraphCache").Get() ?? new ReachGraphCacheOptions()); var logger = sp.GetRequiredService>(); // TODO: Get tenant from request context return new ReachGraphValkeyCache(redisMultiplexer, serializer, options, logger, "default"); }); // Application services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Rate limiting builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddPolicy("reachgraph-read", ctx => RateLimitPartition.GetFixedWindowLimiter( ctx.User.FindFirst("tenant")?.Value ?? ctx.Request.Headers["X-Tenant-ID"].FirstOrDefault() ?? "anonymous", _ => new FixedWindowRateLimiterOptions { Window = TimeSpan.FromMinutes(1), PermitLimit = 100 })); options.AddPolicy("reachgraph-write", ctx => RateLimitPartition.GetFixedWindowLimiter( ctx.User.FindFirst("tenant")?.Value ?? ctx.Request.Headers["X-Tenant-ID"].FirstOrDefault() ?? "anonymous", _ => new FixedWindowRateLimiterOptions { Window = TimeSpan.FromMinutes(1), PermitLimit = 20 })); }); // Response compression builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; }); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.TryAddStellaOpsLocalBinding("reachgraph"); var app = builder.Build(); app.LogStellaOpsLocalHostname("reachgraph"); // Configure pipeline if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseResponseCompression(); app.UseStellaOpsCors(); app.UseRateLimiter(); app.UseAuthorization(); app.MapControllers(); app.Run(); // Make Program class accessible for integration testing namespace StellaOps.ReachGraph.WebService { public partial class Program { } }