// ----------------------------------------------------------------------------- // Program.cs // Sprint: SPRINT_20260125_002_Attestor_trust_automation // Task: PROXY-002 - Implement tile-proxy service // Description: Tile proxy web service entry point // ----------------------------------------------------------------------------- using Microsoft.Extensions.Options; using Serilog; using StellaOps.Attestor.TileProxy; using StellaOps.Attestor.TileProxy.Endpoints; using StellaOps.Attestor.TileProxy.Jobs; using StellaOps.Attestor.TileProxy.Services; const string ConfigurationSection = "tile_proxy"; var builder = WebApplication.CreateBuilder(args); // Configure logging builder.Host.UseSerilog((context, config) => { config .ReadFrom.Configuration(context.Configuration) .Enrich.FromLogContext() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"); }); // Load configuration builder.Configuration .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables("TILE_PROXY__"); // Configure options builder.Services.Configure(builder.Configuration.GetSection(ConfigurationSection)); // Validate options builder.Services.AddSingleton, TileProxyOptionsValidator>(); // Register services builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Register sync job as hosted service builder.Services.AddHostedService(); // Configure HTTP client for upstream builder.Services.AddHttpClient((sp, client) => { var options = sp.GetRequiredService>().Value; client.BaseAddress = new Uri(options.UpstreamUrl); client.Timeout = TimeSpan.FromSeconds(options.Request.TimeoutSeconds); client.DefaultRequestHeaders.Add("User-Agent", "StellaOps-TileProxy/1.0"); }); // Add OpenAPI builder.Services.AddEndpointsApiExplorer(); var app = builder.Build(); // Validate options on startup var optionsValidator = app.Services.GetRequiredService>(); var options = app.Services.GetRequiredService>().Value; var validationResult = optionsValidator.Validate(null, options); if (validationResult.Failed) { throw new InvalidOperationException($"Configuration validation failed: {validationResult.FailureMessage}"); } // Configure pipeline app.UseSerilogRequestLogging(); // Map endpoints app.MapTileProxyEndpoints(); // Startup message var logger = app.Services.GetRequiredService>(); logger.LogInformation( "Tile Proxy starting - Upstream: {Upstream}, Cache: {CachePath}", options.UpstreamUrl, options.Cache.BasePath); app.Run(); /// /// Options validator for tile proxy configuration. /// public sealed class TileProxyOptionsValidator : IValidateOptions { public ValidateOptionsResult Validate(string? name, TileProxyOptions options) { var errors = new List(); if (string.IsNullOrWhiteSpace(options.UpstreamUrl)) { errors.Add("UpstreamUrl is required"); } else if (!Uri.TryCreate(options.UpstreamUrl, UriKind.Absolute, out _)) { errors.Add("UpstreamUrl must be a valid absolute URI"); } if (string.IsNullOrWhiteSpace(options.Origin)) { errors.Add("Origin is required"); } if (options.Cache.MaxSizeGb < 0) { errors.Add("Cache.MaxSizeGb cannot be negative"); } if (options.Cache.CheckpointTtlMinutes < 1) { errors.Add("Cache.CheckpointTtlMinutes must be at least 1"); } if (options.Request.TimeoutSeconds < 1) { errors.Add("Request.TimeoutSeconds must be at least 1"); } if (options.Tuf.Enabled && string.IsNullOrWhiteSpace(options.Tuf.Url)) { errors.Add("Tuf.Url is required when TUF is enabled"); } return errors.Count > 0 ? ValidateOptionsResult.Fail(errors) : ValidateOptionsResult.Success; } } public partial class Program { }