using AspNet.Security.OAuth.GitHub; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using MongoDB.Driver; using Nalu.Web.Data; using Nalu.Web.Data.Repositories; using Nalu.Web.Endpoints; using Nalu.Web.Enrichers; using Nalu.Web.Infrastructure; using Nalu.Web.Mcp; using Nalu.Web.PostProcessors; using Nalu.Web.Services; using Nalu.Web.Services.LlmRouter; using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); // ── Razor Pages (site) ─────────────────────────────────────────────────────── builder.Services.AddRazorPages(); // ── OpenAPI / Scalar ────────────────────────────────────────────────────────── builder.Services.AddOpenApi("v1", options => { options.AddDocumentTransformer((doc, _, _) => { doc.Info.Title = "NALU AI API"; doc.Info.Version = "v1"; doc.Info.Description = "Natural Language Understanding — extrai intenções reais de diálogos agente/usuário."; return Task.CompletedTask; }); }); // ── MongoDB ─────────────────────────────────────────────────────────────────── builder.Services.AddSingleton(sp => { var connStr = builder.Configuration.GetConnectionString("MongoDB") ?? string.Empty; if (string.IsNullOrWhiteSpace(connStr)) return null!; var settings = MongoClientSettings.FromConnectionString( connStr + (connStr.Contains('?') ? "&" : "?") + "maxPoolSize=200&minPoolSize=10&maxIdleTimeMS=30000"); return new MongoClient(settings); }); builder.Services.AddSingleton(); // ── Repositories ────────────────────────────────────────────────────────────── builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // ── Authentication ─────────────────────────────────────────────────────────── // Site = Cookie (Razor Pages). API = Bearer API key (/v1/*). DO NOT mix. builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => { options.LoginPath = "/login"; options.LogoutPath = "/auth/logout"; options.Cookie.HttpOnly = true; options.Cookie.SameSite = SameSiteMode.Lax; options.ExpireTimeSpan = TimeSpan.FromDays(30); options.SlidingExpiration = true; }) // Temporary cookie for external OAuth handshake .AddCookie("ExternalCookie", options => { options.Cookie.Name = "nalu.external"; options.ExpireTimeSpan = TimeSpan.FromMinutes(10); }) // API key scheme — used explicitly by /v1/* endpoints .AddScheme(ApiKeyAuthScheme.Name, null) .AddGoogle(GoogleDefaults.AuthenticationScheme, options => { options.ClientId = builder.Configuration["OAuth:Google:ClientId"] ?? string.Empty; options.ClientSecret = builder.Configuration["OAuth:Google:ClientSecret"] ?? string.Empty; options.CallbackPath = "/signin-google"; options.SignInScheme = "ExternalCookie"; options.SaveTokens = false; }) .AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options => { options.ClientId = builder.Configuration["OAuth:Microsoft:ClientId"] ?? string.Empty; options.ClientSecret = builder.Configuration["OAuth:Microsoft:ClientSecret"] ?? string.Empty; options.CallbackPath = "/signin-microsoft"; options.SignInScheme = "ExternalCookie"; options.SaveTokens = false; }) .AddGitHub(GitHubAuthenticationDefaults.AuthenticationScheme, options => { options.ClientId = builder.Configuration["OAuth:GitHub:ClientId"] ?? string.Empty; options.ClientSecret = builder.Configuration["OAuth:GitHub:ClientSecret"] ?? string.Empty; options.CallbackPath = "/signin-github"; options.SignInScheme = "ExternalCookie"; options.Scope.Add("user:email"); options.SaveTokens = false; }); builder.Services.AddAuthorization(options => { // API endpoints — must present a valid Bearer API key options.AddPolicy("ApiKey", policy => policy.AddAuthenticationSchemes(ApiKeyAuthScheme.Name) .RequireAuthenticatedUser()); }); // ── Session (required for OAuth state) ─────────────────────────────────────── builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.IdleTimeout = TimeSpan.FromMinutes(10); }); // ── Caching ────────────────────────────────────────────────────────────────── builder.Services.AddMemoryCache(); // ── HTTP clients ───────────────────────────────────────────────────────────── builder.Services.AddHttpClient(client => { var baseUrl = builder.Configuration["Groq:BaseUrl"] ?? "https://api.groq.com/openai/v1"; client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + '/'); client.DefaultRequestHeaders.Add( "Authorization", $"Bearer {builder.Configuration["Groq:ApiKey"]}"); client.Timeout = TimeSpan.FromSeconds(30); }); // ── LLM Router (Groq → OpenRouter → Google AI) ─────────────────────────────── builder.Services.AddHttpClient(client => { var baseUrl = builder.Configuration["Groq:BaseUrl"] ?? "https://api.groq.com/openai/v1"; client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + '/'); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {builder.Configuration["Groq:ApiKey"]}"); client.Timeout = TimeSpan.FromSeconds(30); }); builder.Services.AddHttpClient(client => { var baseUrl = builder.Configuration["OpenRouter:BaseUrl"] ?? "https://openrouter.ai/api/v1"; client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + '/'); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {builder.Configuration["OpenRouter:ApiKey"]}"); client.DefaultRequestHeaders.Add("HTTP-Referer", "https://naluai.com"); client.DefaultRequestHeaders.Add("X-Title", "NALU AI"); client.Timeout = TimeSpan.FromSeconds(30); }); builder.Services.AddHttpClient(client => { var baseUrl = builder.Configuration["GoogleAi:BaseUrl"] ?? "https://generativelanguage.googleapis.com/v1beta/openai/"; client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + '/'); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {builder.Configuration["GoogleAi:ApiKey"]}"); client.Timeout = TimeSpan.FromSeconds(30); }); builder.Services.AddSingleton(sp => { var providers = new List { sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService() }; return new LlmRouter(providers, sp.GetRequiredService>()); }); // ViaCEP enricher — no fixed base address (calls multiple URLs) builder.Services.AddHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(10); client.DefaultRequestHeaders.Add("User-Agent", "nalu-ai/1.0"); }); builder.Services.AddTransient(sp => sp.GetRequiredService()); // ── Post-processors ─────────────────────────────────────────────────────────── builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // ── Domain services ────────────────────────────────────────────────────────── builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // EnrichmentService is Scoped because ViaCepEnricher is Transient (HttpClient lifecycle) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // ── MCP server ──────────────────────────────────────────────────────────────── builder.Services.AddSingleton(); // ── App ─────────────────────────────────────────────────────────────────────── var app = builder.Build(); // Initialize MongoDB indexes on startup var mongo = app.Services.GetRequiredService(); await mongo.InitializeAsync(); app.UseSession(); app.UseAuthentication(); app.UseAuthorization(); app.MapOpenApi(); app.MapScalarApiReference(options => { options.Title = "NALU AI"; options.DefaultHttpClient = new(ScalarTarget.Http, ScalarClient.HttpClient); }); app.MapRazorPages(); app.MapExtractEndpoints(); app.MapValidatorsEndpoints(); // ── Auth endpoints ──────────────────────────────────────────────────────────── app.MapGet("/auth/logout", async (HttpContext ctx) => { await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Results.Redirect("/"); }).AllowAnonymous(); // ── Health check ────────────────────────────────────────────────────────────── app.MapGet("/health", () => Results.Ok(new { status = "ok", ts = DateTime.UtcNow })) .WithTags("Health") .WithSummary("Health check") .AllowAnonymous(); // ── MCP endpoint ────────────────────────────────────────────────────────────── app.MapPost("/mcp", async (HttpContext ctx, McpServer mcp, CancellationToken ct) => await mcp.HandleAsync(ctx, ct)) .RequireAuthorization("ApiKey") .WithName("McpEndpoint") .WithSummary("MCP Server (JSON-RPC 2.0 / Streamable HTTP)") .WithTags("MCP") .WithOpenApi(); app.Run(); // Exposed for WebApplicationFactory in integration tests public partial class Program { }