stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -45,3 +45,7 @@
|
||||
- NuGet: use `.nuget/packages/` cache; avoid floating versions.
|
||||
- Linting/analyzers: keep nullable enabled; treat warnings as errors where feasible.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10450, http://localhost:10451
|
||||
- Local alias: https://advisoryai.stella-ops.local, http://advisoryai.stella-ops.local
|
||||
- Env var: STELLAOPS_ADVISORYAI_URL
|
||||
|
||||
@@ -24,6 +24,7 @@ using StellaOps.AdvisoryAI.WebService.Contracts;
|
||||
using StellaOps.AdvisoryAI.WebService.Endpoints;
|
||||
using StellaOps.AdvisoryAI.WebService.Services;
|
||||
using StellaOps.Evidence.Pack;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Router.AspNet;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
@@ -77,6 +78,8 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
@@ -100,7 +103,9 @@ builder.Services.AddRateLimiter(options =>
|
||||
});
|
||||
});
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("advisoryai");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("advisoryai");
|
||||
|
||||
app.UseExceptionHandler(static options => options.Run(async context =>
|
||||
{
|
||||
@@ -113,6 +118,7 @@ if (app.Environment.IsDevelopment())
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseRateLimiter();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62501;http://localhost:62502"
|
||||
"applicationUrl": "https://localhost:10450;http://localhost:10451"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,3 +40,14 @@
|
||||
- When contracts/schemas change, update docs under `docs/modules/airgap/**` and link from sprint Decisions & Risks.
|
||||
- If a decision is needed, mark BLOCKED in the sprint and record the decision ask; continue with other unblocked work.
|
||||
|
||||
## Service Endpoints
|
||||
|
||||
### AirGap Controller (Slot 32)
|
||||
- Development: https://localhost:10320, http://localhost:10321
|
||||
- Local alias: https://airgap-controller.stella-ops.local, http://airgap-controller.stella-ops.local
|
||||
- Env var: STELLAOPS_AIRGAP_CONTROLLER_URL
|
||||
|
||||
### AirGap Time (Slot 33)
|
||||
- Development: https://localhost:10330, http://localhost:10331
|
||||
- Local alias: https://airgap-time.stella-ops.local, http://airgap-time.stella-ops.local
|
||||
- Env var: STELLAOPS_AIRGAP_TIME_URL
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.AirGap.Controller.Auth;
|
||||
using StellaOps.AirGap.Controller.DependencyInjection;
|
||||
using StellaOps.AirGap.Controller.Endpoints;
|
||||
@@ -15,8 +16,13 @@ builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
|
||||
|
||||
builder.Services.AddAirGapController(builder.Configuration);
|
||||
|
||||
var app = builder.Build();
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("airgap-controller");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("airgap-controller");
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapAirGapEndpoints();
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62500;http://localhost:62503"
|
||||
"applicationUrl": "https://localhost:10320;http://localhost:10321"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.AirGap.Time.Config;
|
||||
using StellaOps.AirGap.Time.Health;
|
||||
using StellaOps.AirGap.Time.Hooks;
|
||||
@@ -34,8 +35,13 @@ builder.Services.AddHealthChecks().AddCheck<TimeAnchorHealthCheck>("time_anchor"
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
var app = builder.Build();
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("airgap-time");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("airgap-time");
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/healthz/ready");
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62505;http://localhost:62506"
|
||||
"applicationUrl": "https://localhost:10330;http://localhost:10331"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,3 +59,7 @@ Manage the attestation and proof chain infrastructure for StellaOps:
|
||||
- Keep Offline Kit parity in mind???document air-gapped workflows for any new feature.
|
||||
- Update runbooks/observability assets when operational characteristics change.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10040, http://localhost:10041
|
||||
- Local alias: https://attestor.stella-ops.local, http://attestor.stella-ops.local
|
||||
- Env var: STELLAOPS_ATTESTOR_URL
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.WebService;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Router.AspNet;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
@@ -27,6 +28,8 @@ var clientCertificateAuthorities = AttestorWebServiceComposition.LoadClientCerti
|
||||
builder.AddAttestorWebService(attestorOptions, ConfigurationSection);
|
||||
builder.WebHost.ConfigureAttestorKestrel(attestorOptions, clientCertificateAuthorities);
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
// Stella Router integration
|
||||
var routerOptions = builder.Configuration.GetSection("Attestor:Router").Get<StellaRouterOptionsBase>();
|
||||
builder.Services.TryAddStellaRouter(
|
||||
@@ -34,8 +37,11 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("attestor");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("attestor");
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAttestorWebService(attestorOptions, routerOptions);
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62507;http://localhost:62508"
|
||||
"applicationUrl": "https://localhost:10040;http://localhost:10041"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,8 @@
|
||||
|
||||
## Sprint Discipline
|
||||
- Record decisions and risks for security-sensitive changes in the sprint file.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10020, http://localhost:10021
|
||||
- Local alias: https://authority.stella-ops.local, http://authority.stella-ops.local
|
||||
- Env var: STELLAOPS_AUTHORITY_URL
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<ProjectReference Include="..\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.AspNet.Extensions/StellaOps.AspNet.Extensions.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under BUSL-1.1. See LICENSE in the project root.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Auth.ServerIntegration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides two extension methods for the <c>.stella-ops.local</c> hostname convention:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <see cref="TryAddStellaOpsLocalBinding"/> — called on <see cref="WebApplicationBuilder"/>
|
||||
/// before <c>Build()</c>; binds both <c>https://{serviceName}.stella-ops.local</c> (port 443)
|
||||
/// and <c>http://{serviceName}.stella-ops.local</c> (port 80).
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="LogStellaOpsLocalHostname"/> — called on <see cref="WebApplication"/>
|
||||
/// after <c>Build()</c>; checks DNS for the friendly hostname and logs the result.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static class StellaOpsLocalHostnameExtensions
|
||||
{
|
||||
private const string DomainSuffix = ".stella-ops.local";
|
||||
private const int HttpsPort = 443;
|
||||
private const int HttpPort = 80;
|
||||
private const string SetupDocPath = "docs/technical/architecture/port-registry.md";
|
||||
|
||||
/// <summary>
|
||||
/// Configuration key used to communicate local-binding status
|
||||
/// from the builder phase to the app phase.
|
||||
/// </summary>
|
||||
internal const string LocalBindingBoundKey = "StellaOps:LocalBindingBound";
|
||||
|
||||
/// <summary>
|
||||
/// Configuration key storing the service name for use in the app phase.
|
||||
/// </summary>
|
||||
internal const string LocalBindingServiceKey = "StellaOps:LocalBindingService";
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <c>{serviceName}.stella-ops.local</c> to its dedicated loopback IP
|
||||
/// (from the hosts file), then binds <c>https://{hostname}</c> (port 443) and
|
||||
/// <c>http://{hostname}</c> (port 80) on that IP. Each service uses a unique
|
||||
/// loopback address (e.g. 127.1.0.2) so ports never collide.
|
||||
/// </summary>
|
||||
public static WebApplicationBuilder TryAddStellaOpsLocalBinding(
|
||||
this WebApplicationBuilder builder, string serviceName)
|
||||
{
|
||||
builder.Configuration[LocalBindingServiceKey] = serviceName;
|
||||
var hostname = serviceName + DomainSuffix;
|
||||
|
||||
// Resolve the hostname to find its dedicated loopback IP from the hosts file.
|
||||
IPAddress? resolvedIp = null;
|
||||
try
|
||||
{
|
||||
var addresses = Dns.GetHostAddresses(hostname);
|
||||
resolvedIp = addresses.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Hostname does not resolve; skip binding (will warn at startup).
|
||||
}
|
||||
|
||||
if (resolvedIp == null)
|
||||
{
|
||||
builder.Configuration[LocalBindingBoundKey] = "false";
|
||||
return builder;
|
||||
}
|
||||
|
||||
var httpsAvailable = IsPortAvailable(HttpsPort, resolvedIp);
|
||||
var httpAvailable = IsPortAvailable(HttpPort, resolvedIp);
|
||||
|
||||
if (!httpsAvailable && !httpAvailable)
|
||||
{
|
||||
builder.Configuration[LocalBindingBoundKey] = "false";
|
||||
return builder;
|
||||
}
|
||||
|
||||
builder.Configuration[LocalBindingBoundKey] = "true";
|
||||
|
||||
// Bind to the specific loopback IP (not hostname) so Kestrel uses only
|
||||
// this address, leaving other 127.1.0.x IPs available for other services.
|
||||
// UseUrls("https://hostname") would bind to [::]:443 (all interfaces).
|
||||
//
|
||||
// When ConfigureKestrel uses explicit Listen() calls, Kestrel ignores UseUrls.
|
||||
// So we must also re-add the dev-port bindings from launchSettings.json.
|
||||
var currentUrls = builder.WebHost.GetSetting(WebHostDefaults.ServerUrlsKey) ?? "";
|
||||
var ip = resolvedIp;
|
||||
builder.WebHost.ConfigureKestrel((context, kestrel) =>
|
||||
{
|
||||
// Re-add dev-port bindings from launchSettings.json / ASPNETCORE_URLS
|
||||
foreach (var rawUrl in currentUrls.Split(';', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (Uri.TryCreate(rawUrl.Trim(), UriKind.Absolute, out var uri))
|
||||
{
|
||||
var isHttps = string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase);
|
||||
var addr = uri.Host == "localhost"
|
||||
? IPAddress.Loopback
|
||||
: IPAddress.Parse(uri.Host);
|
||||
|
||||
if (isHttps)
|
||||
{
|
||||
kestrel.Listen(addr, uri.Port, lo => lo.UseHttps());
|
||||
}
|
||||
else
|
||||
{
|
||||
kestrel.Listen(addr, uri.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add .stella-ops.local bindings on the dedicated loopback IP
|
||||
if (httpsAvailable)
|
||||
{
|
||||
kestrel.Listen(ip, HttpsPort, listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps();
|
||||
});
|
||||
}
|
||||
|
||||
if (httpAvailable)
|
||||
{
|
||||
kestrel.Listen(ip, HttpPort);
|
||||
}
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backwards-compatible overload — reads the service name from configuration
|
||||
/// set by <see cref="TryAddStellaOpsLocalBinding"/>.
|
||||
/// </summary>
|
||||
[System.Obsolete("Use TryAddStellaOpsLocalBinding(builder, serviceName) instead of TryAddStellaOpsSharedPort(). This overload will be removed.")]
|
||||
public static WebApplicationBuilder TryAddStellaOpsSharedPort(
|
||||
this WebApplicationBuilder builder)
|
||||
{
|
||||
// No-op fallback for any callers not yet migrated; the binding happens
|
||||
// in TryAddStellaOpsLocalBinding which should be called instead.
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a startup callback that checks DNS for
|
||||
/// <c>{serviceName}.stella-ops.local</c> and logs the result.
|
||||
/// Also warns if the local bindings were skipped.
|
||||
/// </summary>
|
||||
public static WebApplication LogStellaOpsLocalHostname(
|
||||
this WebApplication app, string serviceName)
|
||||
{
|
||||
var hostname = serviceName + DomainSuffix;
|
||||
var localBindingBound = string.Equals(
|
||||
app.Configuration[LocalBindingBoundKey], "true", StringComparison.OrdinalIgnoreCase);
|
||||
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
|
||||
|
||||
lifetime.ApplicationStarted.Register(() =>
|
||||
{
|
||||
var logger = app.Services.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger("StellaOps.LocalHostname");
|
||||
|
||||
if (!localBindingBound)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Ports {HttpsPort}/{HttpPort} are already in use; skipping {Hostname} bindings. " +
|
||||
"This is expected when running multiple services locally.",
|
||||
HttpsPort, HttpPort, hostname);
|
||||
}
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
var resolved = await Dns.GetHostAddressesAsync(hostname, cts.Token);
|
||||
|
||||
if (resolved.Length > 0)
|
||||
{
|
||||
if (localBindingBound)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Also accessible at https://{Hostname} and http://{Hostname}",
|
||||
hostname, hostname);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Hostname {Hostname} resolves but ports {HttpsPort}/{HttpPort} are unavailable; " +
|
||||
"use the dev port instead.",
|
||||
hostname, HttpsPort, HttpPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
var hostsPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? @"C:\Windows\System32\drivers\etc\hosts"
|
||||
: "/etc/hosts";
|
||||
|
||||
logger.LogWarning(
|
||||
"Hostname {Hostname} does not resolve. " +
|
||||
"To enable friendly .stella-ops.local URLs, add hosts-file entries " +
|
||||
"as described in {SetupDoc}. " +
|
||||
"Edit {HostsFile} and add a unique 127.1.0.x entry for {Hostname}",
|
||||
hostname, SetupDocPath, hostsPath, hostname);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static bool IsPortAvailable(int port, IPAddress address)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var listener = new TcpListener(address, port);
|
||||
listener.Start();
|
||||
listener.Stop();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,6 +290,7 @@ var pluginRegistrationSummary = AuthorityPluginLoader.RegisterPlugins(
|
||||
|
||||
builder.Services.AddSingleton(pluginRegistrationSummary);
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddAuthentication();
|
||||
@@ -414,7 +415,9 @@ builder.Services.Configure<OpenIddictServerOptions>(options =>
|
||||
options.DisableRollingRefreshTokens = false;
|
||||
});
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("authority");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("authority");
|
||||
|
||||
var serviceAccountStore = app.Services.GetRequiredService<IAuthorityServiceAccountStore>();
|
||||
if (authorityOptions.Delegation.ServiceAccounts.Count > 0)
|
||||
@@ -1716,6 +1719,7 @@ app.UseLegacyAuthDeprecation();
|
||||
app.UseRouting();
|
||||
app.UseAuthorityRateLimiterContext();
|
||||
app.UseRateLimiter();
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5165",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7182;http://localhost:5165",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:10021",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:10020;http://localhost:10021",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography.Kms/StellaOps.Cryptography.Kms.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration.AuthorityPlugin/StellaOps.Configuration.AuthorityPlugin.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="../../../Attestor/StellaOps.Attestation/StellaOps.Attestation.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -99,3 +99,7 @@ stella deltasig inspect # Inspect signature or envelope
|
||||
- **Golden tests** - Known CVE signature verification
|
||||
- **Integration tests** - End-to-end pipeline tests
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10360, http://localhost:10361
|
||||
- Local alias: https://binaryindex.stella-ops.local, http://binaryindex.stella-ops.local
|
||||
- Env var: STELLAOPS_BINARYINDEX_URL
|
||||
|
||||
@@ -8,6 +8,7 @@ using StellaOps.BinaryIndex.Core.Resolution;
|
||||
using StellaOps.BinaryIndex.VexBridge;
|
||||
using StellaOps.BinaryIndex.WebService.Middleware;
|
||||
using StellaOps.BinaryIndex.WebService.Services;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.BinaryIndex.WebService.Telemetry;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -58,7 +59,11 @@ builder.Services.AddResolutionRateLimiting(options =>
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddRedis(redisConnectionString, name: "redis");
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("binaryindex");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("binaryindex");
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
@@ -67,6 +72,7 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseResolutionRateLimiting();
|
||||
app.UseAuthorization();
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:49948;http://localhost:49949"
|
||||
"applicationUrl": "https://localhost:10360;http://localhost:10361"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,8 @@
|
||||
|
||||
## Sprint Discipline
|
||||
- Record decisions and risks for contract changes in the sprint file.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10210, http://localhost:10211
|
||||
- Local alias: https://cartographer.stella-ops.local, http://cartographer.stella-ops.local
|
||||
- Env var: STELLAOPS_CARTOGRAPHER_URL
|
||||
|
||||
@@ -70,7 +70,11 @@ if (authorityOptions.Enabled)
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck("cartographer_ready", () => HealthCheckResult.Healthy(), tags: new[] { "ready" });
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("cartographer");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("cartographer");
|
||||
|
||||
if (!authorityOptions.Enabled)
|
||||
{
|
||||
@@ -81,6 +85,7 @@ else if (authorityOptions.AllowAnonymousFallback)
|
||||
app.Logger.LogWarning("Cartographer Authority allows anonymous fallback; disable fallback before production rollout.");
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
if (authorityOptions.Enabled)
|
||||
{
|
||||
app.UseAuthentication();
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62509;http://localhost:62510"
|
||||
"applicationUrl": "https://localhost:10210;http://localhost:10211"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration.AuthorityPlugin/StellaOps.Configuration.AuthorityPlugin.csproj" />
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Authority.Plugins.Abstractions/StellaOps.Authority.Plugins.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.Kms/StellaOps.Cryptography.Kms.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
|
||||
|
||||
@@ -83,3 +83,7 @@ Version comparators must be tested with 50+ cases per distro. See:
|
||||
- If a design decision is needed, mark the task `BLOCKED` in the sprint doc and record the decision ask???do not pause the codebase.
|
||||
- When changing contracts (APIs, schemas, telemetry, exports), update corresponding docs and link them from the sprint Decisions & Risks section.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10090, http://localhost:10091
|
||||
- Local alias: https://concelier.stella-ops.local, http://concelier.stella-ops.local
|
||||
- Env var: STELLAOPS_CONCELIER_URL
|
||||
|
||||
@@ -806,8 +806,11 @@ var pluginHostOptions = BuildPluginOptions(concelierOptions, builder.Environment
|
||||
builder.Services.RegisterPluginRoutines(builder.Configuration, pluginHostOptions);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("concelier");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("concelier");
|
||||
var appTimeProvider = app.Services.GetRequiredService<TimeProvider>();
|
||||
var swaggerEnabled = app.Configuration.GetValue<bool>("Swagger:Enabled");
|
||||
|
||||
@@ -837,6 +840,8 @@ if (resolvedAuthority.Enabled && resolvedAuthority.AllowAnonymousFallback)
|
||||
"Authority authentication is configured but anonymous fallback remains enabled. Set authority.allowAnonymousFallback to false before 2025-12-31 to complete the rollout.");
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
|
||||
if (authorityConfigured)
|
||||
{
|
||||
app.UseAuthentication();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"profiles": {
|
||||
"StellaOps.Concelier.WebService": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:50411;http://localhost:50412"
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"profiles": {
|
||||
"StellaOps.Concelier.WebService": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:10090;http://localhost:10091"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0212-M | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0212-T | DONE | Revalidated 2026-01-06. |
|
||||
| AUDIT-0212-A | DONE | Waived (test project; revalidated 2026-01-06). |
|
||||
| REMED-08 | DONE | Added ingestion telemetry metric tag tests; `dotnet test src/Concelier/__Tests/StellaOps.Concelier.Core.Tests/StellaOps.Concelier.Core.Tests.csproj` passed (2026-02-03). |
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using StellaOps.Ingestion.Telemetry;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Tests.Telemetry;
|
||||
|
||||
internal static class IngestionTelemetryMetricsTestHelpers
|
||||
{
|
||||
internal static List<KeyValuePair<string, object?>[]> CaptureCounter(
|
||||
string instrumentName,
|
||||
Action action)
|
||||
{
|
||||
var measurements = new List<KeyValuePair<string, object?>[]>();
|
||||
using var listener = new MeterListener();
|
||||
listener.InstrumentPublished += (instrument, meterListener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == IngestionTelemetry.MeterName && instrument.Name == instrumentName)
|
||||
{
|
||||
meterListener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
};
|
||||
|
||||
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
if (instrument.Name == instrumentName)
|
||||
{
|
||||
measurements.Add(tags.ToArray());
|
||||
}
|
||||
});
|
||||
|
||||
listener.Start();
|
||||
action();
|
||||
return measurements;
|
||||
}
|
||||
|
||||
internal static List<KeyValuePair<string, object?>[]> CaptureHistogram(
|
||||
string instrumentName,
|
||||
Action action)
|
||||
{
|
||||
var measurements = new List<KeyValuePair<string, object?>[]>();
|
||||
using var listener = new MeterListener();
|
||||
listener.InstrumentPublished += (instrument, meterListener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == IngestionTelemetry.MeterName && instrument.Name == instrumentName)
|
||||
{
|
||||
meterListener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
};
|
||||
|
||||
listener.SetMeasurementEventCallback<double>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
if (instrument.Name == instrumentName)
|
||||
{
|
||||
measurements.Add(tags.ToArray());
|
||||
}
|
||||
});
|
||||
|
||||
listener.Start();
|
||||
action();
|
||||
return measurements;
|
||||
}
|
||||
|
||||
internal static object? GetTagValue(IEnumerable<KeyValuePair<string, object?>> tags, string key)
|
||||
{
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (string.Equals(tag.Key, key, StringComparison.Ordinal))
|
||||
{
|
||||
return tag.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using StellaOps.Ingestion.Telemetry;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Tests.Telemetry;
|
||||
|
||||
public sealed class IngestionTelemetryMetricsTests
|
||||
{
|
||||
[Fact]
|
||||
public void RecordWriteAttempt_EmitsResultTag()
|
||||
{
|
||||
var measurements = IngestionTelemetryMetricsTestHelpers.CaptureCounter(
|
||||
"ingestion_write_total",
|
||||
() => IngestionTelemetry.RecordWriteAttempt("tenant-1", "source-1", IngestionTelemetry.ResultOk));
|
||||
|
||||
Assert.Contains(
|
||||
measurements,
|
||||
tags => string.Equals(
|
||||
IngestionTelemetryMetricsTestHelpers.GetTagValue(tags, "result") as string,
|
||||
IngestionTelemetry.ResultOk,
|
||||
StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordWriteAttempt_BlankResult_DoesNotEmit()
|
||||
{
|
||||
var measurements = IngestionTelemetryMetricsTestHelpers.CaptureCounter(
|
||||
"ingestion_write_total",
|
||||
() => IngestionTelemetry.RecordWriteAttempt("tenant-1", "source-1", " "));
|
||||
|
||||
Assert.Empty(measurements);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordViolation_EmitsCodeTag()
|
||||
{
|
||||
var measurements = IngestionTelemetryMetricsTestHelpers.CaptureCounter(
|
||||
"aoc_violation_total",
|
||||
() => IngestionTelemetry.RecordViolation("tenant-1", "source-1", "ERR_AOC_001"));
|
||||
|
||||
Assert.Contains(
|
||||
measurements,
|
||||
tags => string.Equals(
|
||||
IngestionTelemetryMetricsTestHelpers.GetTagValue(tags, "code") as string,
|
||||
"ERR_AOC_001",
|
||||
StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordLatency_EmitsPhaseTag()
|
||||
{
|
||||
var measurements = IngestionTelemetryMetricsTestHelpers.CaptureHistogram(
|
||||
"ingestion_latency_seconds",
|
||||
() => IngestionTelemetry.RecordLatency(
|
||||
"tenant-1",
|
||||
"source-1",
|
||||
IngestionTelemetry.PhaseFetch,
|
||||
TimeSpan.FromSeconds(1)));
|
||||
|
||||
Assert.Contains(
|
||||
measurements,
|
||||
tags => string.Equals(
|
||||
IngestionTelemetryMetricsTestHelpers.GetTagValue(tags, "phase") as string,
|
||||
IngestionTelemetry.PhaseFetch,
|
||||
StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordLatency_BlankPhase_DoesNotEmit()
|
||||
{
|
||||
var measurements = IngestionTelemetryMetricsTestHelpers.CaptureHistogram(
|
||||
"ingestion_latency_seconds",
|
||||
() => IngestionTelemetry.RecordLatency("tenant-1", "source-1", "", TimeSpan.FromSeconds(1)));
|
||||
|
||||
Assert.Empty(measurements);
|
||||
}
|
||||
}
|
||||
@@ -46,3 +46,8 @@
|
||||
|
||||
## Contacts/ownership
|
||||
- Module owner: Doctor Guild
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10260, http://localhost:10261
|
||||
- Local alias: https://doctor.stella-ops.local, http://doctor.stella-ops.local
|
||||
- Env var: STELLAOPS_DOCTOR_URL
|
||||
|
||||
@@ -71,7 +71,7 @@ public sealed class DoctorAuthorityOptions
|
||||
/// <summary>
|
||||
/// Gets or sets the issuer URL.
|
||||
/// </summary>
|
||||
public string Issuer { get; set; } = "https://auth.stellaops.local";
|
||||
public string Issuer { get; set; } = "https://authority.stella-ops.local";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata address.
|
||||
@@ -86,7 +86,7 @@ public sealed class DoctorAuthorityOptions
|
||||
/// <summary>
|
||||
/// Gets or sets the valid audiences.
|
||||
/// </summary>
|
||||
public List<string> Audiences { get; set; } = new() { "stellaops-api" };
|
||||
public List<string> Audiences { get; set; } = new() { "stella-ops-api" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the required scopes.
|
||||
|
||||
@@ -56,6 +56,7 @@ builder.Services.AddOptions<DoctorServiceOptions>()
|
||||
})
|
||||
.ValidateOnStart();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddOpenApi();
|
||||
@@ -146,7 +147,9 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("doctor");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("doctor");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -154,6 +157,7 @@ if (app.Environment.IsDevelopment())
|
||||
}
|
||||
|
||||
app.UseStellaOpsTelemetryContext();
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:64478;http://localhost:64480"
|
||||
"applicationUrl": "https://localhost:10260;http://localhost:10261"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,3 +34,8 @@
|
||||
- Schema evolution tests for bundle compatibility.
|
||||
- Tests for transparency and timestamp reference serialization.
|
||||
- Tests for Object Lock configuration validation.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10060, http://localhost:10061
|
||||
- Local alias: https://evidencelocker.stella-ops.local, http://evidencelocker.stella-ops.local
|
||||
- Env var: STELLAOPS_EVIDENCELOCKER_URL
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62511;http://localhost:62512"
|
||||
"applicationUrl": "https://localhost:10070;http://localhost:10071"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ builder.Services.AddAuthorization(options =>
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
// Stella Router integration
|
||||
var routerOptions = builder.Configuration.GetSection("EvidenceLocker:Router").Get<StellaRouterOptionsBase>();
|
||||
builder.Services.TryAddStellaRouter(
|
||||
@@ -52,7 +54,9 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(StellaOps.EvidenceLocker.WebService.Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("evidencelocker");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("evidencelocker");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -60,6 +64,7 @@ if (app.Environment.IsDevelopment())
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5115",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7010;http://localhost:5115",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:10061",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:10060;http://localhost:10061",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,3 +79,7 @@ The Excititor module handles VEX candidate emission for Smart-Diff:
|
||||
- Signing/verifier hooks rely on Evidence Locker contract fixtures under `docs/modules/evidence-locker/`.
|
||||
- Sealed-mode tests should run with `EXCITITOR_SEALED=1` (env var) to enforce offline code paths.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10100, http://localhost:10101
|
||||
- Local alias: https://excititor.stella-ops.local, http://excititor.stella-ops.local
|
||||
- Env var: STELLAOPS_EXCITITOR_URL
|
||||
|
||||
@@ -27,6 +27,7 @@ using StellaOps.Excititor.Persistence.Extensions;
|
||||
using StellaOps.Excititor.Persistence.Postgres;
|
||||
using StellaOps.Excititor.Policy;
|
||||
using StellaOps.Excititor.WebService.Contracts;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Excititor.WebService.Endpoints;
|
||||
using StellaOps.Excititor.WebService.Extensions;
|
||||
using StellaOps.Excititor.WebService.Graph;
|
||||
@@ -197,8 +198,13 @@ services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
var app = builder.Build();
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("excititor");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("excititor");
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62513;http://localhost:62514"
|
||||
"applicationUrl": "https://localhost:10100;http://localhost:10101"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,3 +67,7 @@
|
||||
- When contracts or schemas change (API, manifest, provenance, adapter outputs), update module docs and link them from the sprint.
|
||||
- Retain deterministic retention/pruning behavior; document feature flags and defaults in `docs/modules/export-center/operations/*.md` when modified.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10400, http://localhost:10401
|
||||
- Local alias: https://exportcenter.stella-ops.local, http://exportcenter.stella-ops.local
|
||||
- Env var: STELLAOPS_EXPORTCENTER_URL
|
||||
|
||||
@@ -100,6 +100,8 @@ builder.Services.AddExportApiServices(options =>
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
// Stella Router integration
|
||||
var routerOptions = builder.Configuration.GetSection("ExportCenter:Router").Get<StellaRouterOptionsBase>();
|
||||
builder.Services.TryAddStellaRouter(
|
||||
@@ -107,7 +109,9 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(StellaOps.ExportCenter.WebService.Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("exportcenter");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("exportcenter");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -115,6 +119,7 @@ if (app.Environment.IsDevelopment())
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5269",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7218;http://localhost:5269",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:10401",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:10400;http://localhost:10401",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,3 +70,7 @@
|
||||
- Replay harness run (or planned) for determinism-impacting changes; attach/report results.
|
||||
- Docs updated when contracts or workflows change (module docs, observability policy, sprint Decisions & Risks).
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10250, http://localhost:10251
|
||||
- Local alias: https://findings.stella-ops.local, http://findings.stella-ops.local
|
||||
- Env var: STELLAOPS_FINDINGS_LEDGER_URL
|
||||
|
||||
@@ -254,7 +254,11 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("findings");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("findings");
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseExceptionHandler(exceptionApp =>
|
||||
@@ -275,6 +279,7 @@ app.UseExceptionHandler(exceptionApp =>
|
||||
});
|
||||
});
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62523;http://localhost:62524"
|
||||
"applicationUrl": "https://localhost:10250;http://localhost:10251"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,3 +42,7 @@
|
||||
- Update sprint status in docs/implplan/SPRINT_*.md when starting/finishing work.
|
||||
- If blocked by missing contracts or docs, mark the task BLOCKED in the sprint and record in Decisions & Risks.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10030, http://localhost:10031
|
||||
- Local alias: https://gateway.stella-ops.local, http://gateway.stella-ops.local
|
||||
- Env var: STELLAOPS_GATEWAY_URL
|
||||
|
||||
@@ -94,9 +94,14 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("gateway");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("gateway");
|
||||
|
||||
app.UseMiddleware<CorrelationIdMiddleware>();
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseMiddleware<SenderConstraintMiddleware>();
|
||||
// IdentityHeaderPolicyMiddleware replaces TenantMiddleware and ClaimsPropagationMiddleware
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62515;http://localhost:62516"
|
||||
"applicationUrl": "https://localhost:10030;http://localhost:10031"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,3 +62,7 @@
|
||||
- Keep artefacts deterministic; attach manifest hashes in PR/sprint notes when delivering exports or snapshots.
|
||||
- Document new metrics/routes/schemas under `docs/modules/graph` and link from sprint Decisions & Risks.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10200, http://localhost:10201
|
||||
- Local alias: https://graph.stella-ops.local, http://graph.stella-ops.local
|
||||
- Env var: STELLAOPS_GRAPH_URL
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Graph.Api.Contracts;
|
||||
using StellaOps.Graph.Api.Services;
|
||||
|
||||
@@ -15,8 +16,12 @@ builder.Services.AddScoped<IGraphExportService, InMemoryGraphExportService>();
|
||||
builder.Services.AddSingleton<IRateLimiter>(_ => new RateLimiterService(limitPerWindow: 120));
|
||||
builder.Services.AddSingleton<IAuditLogger, InMemoryAuditLogger>();
|
||||
builder.Services.AddSingleton<IGraphMetrics, GraphMetrics>();
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
builder.TryAddStellaOpsLocalBinding("graph");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("graph");
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseRouting();
|
||||
|
||||
app.MapPost("/graph/search", async (HttpContext context, GraphSearchRequest request, IGraphSearchService service, CancellationToken ct) =>
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62517;http://localhost:62518"
|
||||
"applicationUrl": "https://localhost:10200;http://localhost:10201"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,3 +82,8 @@ public interface IIntegrationConnectorPlugin : IAvailabilityPlugin
|
||||
- Unit tests in `__Tests/StellaOps.Integrations.Tests`
|
||||
- Each plugin has its own test class mocking external APIs
|
||||
- Integration tests use `StellaOps.Integrations.Plugin.InMemory`
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10420, http://localhost:10421
|
||||
- Local alias: https://integrations.stella-ops.local, http://integrations.stella-ops.local
|
||||
- Env var: STELLAOPS_INTEGRATIONS_URL
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Integrations.Persistence;
|
||||
using StellaOps.Integrations.WebService;
|
||||
using StellaOps.Integrations.WebService.Infrastructure;
|
||||
@@ -51,18 +52,11 @@ builder.Services.AddScoped<IAuthRefResolver, StubAuthRefResolver>();
|
||||
// Core service
|
||||
builder.Services.AddScoped<IntegrationService>();
|
||||
|
||||
// CORS for Angular dev server
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.WithOrigins("http://localhost:4200", "https://localhost:4200")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("integrations");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("integrations");
|
||||
|
||||
// Configure pipeline
|
||||
if (app.Environment.IsDevelopment())
|
||||
@@ -71,7 +65,7 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCors();
|
||||
app.UseStellaOpsCors();
|
||||
|
||||
// Map endpoints
|
||||
app.MapIntegrationEndpoints();
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:52411;http://localhost:52416"
|
||||
"applicationUrl": "https://localhost:10420;http://localhost:10421"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Integrations.Persistence\StellaOps.Integrations.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||
<ProjectReference Include="..\..\Router\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -22,3 +22,8 @@
|
||||
|
||||
## Sprint Discipline
|
||||
- Link contract changes in sprint Decisions & Risks.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10370, http://localhost:10371
|
||||
- Local alias: https://issuerdirectory.stella-ops.local, http://issuerdirectory.stella-ops.local
|
||||
- Env var: STELLAOPS_ISSUERDIRECTORY_URL
|
||||
|
||||
@@ -100,6 +100,8 @@ builder.Services.AddOpenTelemetry()
|
||||
.AddRuntimeInstrumentation())
|
||||
.WithTracing(tracing => tracing.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation());
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
// Stella Router integration
|
||||
var routerOptions = builder.Configuration.GetSection("IssuerDirectory:Router").Get<StellaRouterOptionsBase>();
|
||||
builder.Services.TryAddStellaRouter(
|
||||
@@ -107,9 +109,12 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("issuerdirectory");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("issuerdirectory");
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62527;http://localhost:62528"
|
||||
"applicationUrl": "https://localhost:10370;http://localhost:10371"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,3 +22,8 @@
|
||||
|
||||
## Sprint Discipline
|
||||
- Record delivery workflow changes in sprint Decisions & Risks.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10280, http://localhost:10281
|
||||
- Local alias: https://notifier.stella-ops.local, http://notifier.stella-ops.local
|
||||
- Env var: STELLAOPS_NOTIFIER_URL
|
||||
|
||||
@@ -33,6 +33,7 @@ using WorkerTemplateRenderer = StellaOps.Notifier.Worker.Dispatch.INotifyTemplat
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Queue;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Router.AspNet;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -106,6 +107,8 @@ builder.Services.AddNotifierTenancy(builder.Configuration);
|
||||
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
// Stella Router integration
|
||||
var routerOptions = builder.Configuration.GetSection("Notifier:Router").Get<StellaRouterOptionsBase>();
|
||||
builder.Services.TryAddStellaRouter(
|
||||
@@ -113,7 +116,11 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("notifier");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("notifier");
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
|
||||
// Enable WebSocket support for live incident feed
|
||||
app.UseWebSockets(new WebSocketOptions
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5124",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7202;http://localhost:5124",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:10281",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:10280;http://localhost:10281",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,3 +17,8 @@ Deliver and operate the Notify module across WebService, Worker, and storage lay
|
||||
- Storage: keep schema/tests aligned to `notify` schema; when running tests locally ensure Docker/WSL integration for Testcontainers.
|
||||
- Testing: prefer integration suites under `src/Notify/__Tests/StellaOps.Notify.Persistence.Tests`; add coverage for new repositories or state transitions; keep results under `out/test-results/` when capturing evidence.
|
||||
- Cross-module edits require explicit sprint note; otherwise stay within `src/Notify/**` and shared libraries listed in module docs.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10290, http://localhost:10291
|
||||
- Local alias: https://notify.stella-ops.local, http://notify.stella-ops.local
|
||||
- Env var: STELLAOPS_NOTIFY_URL
|
||||
|
||||
@@ -111,7 +111,11 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("notify");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("notify");
|
||||
|
||||
var readyStatus = app.Services.GetRequiredService<ServiceStatus>();
|
||||
|
||||
@@ -345,6 +349,7 @@ static void ConfigureRequestPipeline(WebApplication app, NotifyWebServiceOptions
|
||||
});
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseRateLimiter();
|
||||
app.UseAuthorization();
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62530;http://localhost:62531"
|
||||
"applicationUrl": "https://localhost:10290;http://localhost:10291"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,8 @@
|
||||
|
||||
## Sprint Discipline
|
||||
- Update sprint tracker status and log decisions for schema changes.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10270, http://localhost:10271
|
||||
- Local alias: https://opsmemory.stella-ops.local, http://opsmemory.stella-ops.local
|
||||
- Env var: STELLAOPS_OPSMEMORY_URL
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using Npgsql;
|
||||
using StellaOps.OpsMemory.Playbook;
|
||||
using StellaOps.OpsMemory.Similarity;
|
||||
@@ -31,7 +32,11 @@ builder.Services.AddSwaggerGen(options =>
|
||||
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("opsmemory");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("opsmemory");
|
||||
|
||||
// Configure the HTTP request pipeline
|
||||
if (app.Environment.IsDevelopment())
|
||||
@@ -40,6 +45,7 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
// Map endpoints
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:64476;http://localhost:64482"
|
||||
"applicationUrl": "https://localhost:10270;http://localhost:10271"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,8 @@
|
||||
|
||||
## Sprint Discipline
|
||||
- Track task status in sprint tracker and local TASKS boards.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10170, http://localhost:10171
|
||||
- Local alias: https://orchestrator.stella-ops.local, http://orchestrator.stella-ops.local
|
||||
- Env var: STELLAOPS_ORCHESTRATOR_URL
|
||||
|
||||
@@ -9,11 +9,13 @@ using StellaOps.Orchestrator.Infrastructure.Services;
|
||||
using StellaOps.Orchestrator.WebService.Endpoints;
|
||||
using StellaOps.Orchestrator.WebService.Services;
|
||||
using StellaOps.Orchestrator.WebService.Streaming;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Router.AspNet;
|
||||
using StellaOps.Telemetry.Core;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddOpenApi();
|
||||
@@ -100,13 +102,17 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("orchestrator");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("orchestrator");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
|
||||
// Enable telemetry context propagation (extracts tenant/actor/correlation from headers)
|
||||
// Per ORCH-OBS-50-001
|
||||
app.UseStellaOpsTelemetryContext();
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5151",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7228;http://localhost:5151",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:10171",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:10170;http://localhost:10171",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,3 +22,8 @@
|
||||
|
||||
## Sprint Discipline
|
||||
- Record pack contract changes in sprint Decisions & Risks.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10340, http://localhost:10341
|
||||
- Local alias: https://packsregistry.stella-ops.local, http://packsregistry.stella-ops.local
|
||||
- Env var: STELLAOPS_PACKSREGISTRY_URL
|
||||
|
||||
@@ -8,6 +8,7 @@ using StellaOps.PacksRegistry.Infrastructure.Verification;
|
||||
using StellaOps.PacksRegistry.WebService;
|
||||
using StellaOps.PacksRegistry.WebService.Contracts;
|
||||
using StellaOps.PacksRegistry.WebService.Options;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Router.AspNet;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -56,6 +57,8 @@ builder.Services.AddSingleton(TimeProvider.System);
|
||||
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
// Stella Router integration
|
||||
var routerOptions = builder.Configuration.GetSection("PacksRegistry:Router").Get<StellaRouterOptionsBase>();
|
||||
builder.Services.TryAddStellaRouter(
|
||||
@@ -63,13 +66,16 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("packsregistry");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("packsregistry");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.MapHealthChecks("/healthz");
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62525;http://localhost:62526"
|
||||
"applicationUrl": "https://localhost:10340;http://localhost:10341"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,3 +34,8 @@ Define and deliver the Platform Service that aggregates cross-service views for
|
||||
- Update sprint status in `docs/implplan/SPRINT_*.md` when starting/stopping work.
|
||||
- Document cross-module contract changes in sprint Decisions & Risks.
|
||||
- Avoid non-deterministic data ordering or timestamps in responses.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10010, http://localhost:10011
|
||||
- Local alias: https://platform.stella-ops.local, http://platform.stella-ops.local
|
||||
- Env var: STELLAOPS_PLATFORM_URL
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Platform.WebService.Options;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Post-configures <see cref="PlatformServiceOptions"/> by scanning environment variables
|
||||
/// matching <c>STELLAOPS_*_URL</c> and mapping them into <c>EnvironmentSettings.ApiBaseUrls</c>.
|
||||
/// <para>
|
||||
/// This is Layer 1 (lowest priority). Values are only set for keys NOT already present
|
||||
/// in the options (i.e. YAML/JSON config wins over env vars).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class StellaOpsEnvVarPostConfigure : IPostConfigureOptions<PlatformServiceOptions>
|
||||
{
|
||||
private static readonly Regex EnvVarPattern = new(
|
||||
@"^STELLAOPS_(.+)_URL$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public void PostConfigure(string? name, PlatformServiceOptions options)
|
||||
{
|
||||
var envSettings = options.EnvironmentSettings;
|
||||
var envVars = Environment.GetEnvironmentVariables();
|
||||
|
||||
foreach (var key in envVars.Keys)
|
||||
{
|
||||
if (key is not string keyStr)
|
||||
continue;
|
||||
|
||||
var match = EnvVarPattern.Match(keyStr);
|
||||
if (!match.Success)
|
||||
continue;
|
||||
|
||||
var value = envVars[key] as string;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
continue;
|
||||
|
||||
// STELLAOPS_SCANNER_URL -> "scanner"
|
||||
// STELLAOPS_POLICY_ENGINE_URL -> "policyEngine"
|
||||
var serviceName = NormalizeServiceName(match.Groups[1].Value);
|
||||
|
||||
// Only set if not already present (YAML wins over env vars)
|
||||
if (!envSettings.ApiBaseUrls.ContainsKey(serviceName))
|
||||
{
|
||||
envSettings.ApiBaseUrls[serviceName] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an env var segment like <c>POLICY_ENGINE</c> to camelCase <c>policyEngine</c>.
|
||||
/// </summary>
|
||||
internal static string NormalizeServiceName(string envSegment)
|
||||
{
|
||||
var parts = envSegment.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0)
|
||||
return envSegment.ToLowerInvariant();
|
||||
|
||||
// First part is all lowercase, subsequent parts are PascalCase
|
||||
var result = parts[0].ToLowerInvariant();
|
||||
for (var i = 1; i < parts.Length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
if (part.Length == 0) continue;
|
||||
result += char.ToUpperInvariant(part[0]) + part[1..].ToLowerInvariant();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under BUSL-1.1. See LICENSE in the project root.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Environment settings response served at <c>/platform/envsettings.json</c>.
|
||||
/// Schema matches the Angular <c>AppConfig</c> interface so the frontend can
|
||||
/// consume the payload without transformation.
|
||||
/// </summary>
|
||||
public sealed class EnvironmentSettingsResponse
|
||||
{
|
||||
[JsonPropertyName("authority")]
|
||||
public required EnvironmentAuthoritySettings Authority { get; init; }
|
||||
|
||||
[JsonPropertyName("apiBaseUrls")]
|
||||
public required Dictionary<string, string> ApiBaseUrls { get; init; }
|
||||
|
||||
[JsonPropertyName("telemetry")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public EnvironmentTelemetrySettings? Telemetry { get; init; }
|
||||
|
||||
[JsonPropertyName("welcome")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public EnvironmentWelcomeSettings? Welcome { get; init; }
|
||||
|
||||
[JsonPropertyName("doctor")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public EnvironmentDoctorSettings? Doctor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Setup state indicator for the frontend setup wizard.
|
||||
/// <list type="bullet">
|
||||
/// <item><c>null</c> (absent from JSON) — setup required (fresh install or no DB)</item>
|
||||
/// <item><c>"complete"</c> — setup done, proceed normally</item>
|
||||
/// <item>Any other value — a <c>SetupStepId</c> indicating setup in progress; resume at that step</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[JsonPropertyName("setup")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Setup { get; init; }
|
||||
}
|
||||
|
||||
public sealed class EnvironmentAuthoritySettings
|
||||
{
|
||||
[JsonPropertyName("issuer")]
|
||||
public required string Issuer { get; init; }
|
||||
|
||||
[JsonPropertyName("clientId")]
|
||||
public required string ClientId { get; init; }
|
||||
|
||||
[JsonPropertyName("authorizeEndpoint")]
|
||||
public required string AuthorizeEndpoint { get; init; }
|
||||
|
||||
[JsonPropertyName("tokenEndpoint")]
|
||||
public required string TokenEndpoint { get; init; }
|
||||
|
||||
[JsonPropertyName("logoutEndpoint")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? LogoutEndpoint { get; init; }
|
||||
|
||||
[JsonPropertyName("redirectUri")]
|
||||
public required string RedirectUri { get; init; }
|
||||
|
||||
[JsonPropertyName("postLogoutRedirectUri")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? PostLogoutRedirectUri { get; init; }
|
||||
|
||||
[JsonPropertyName("scope")]
|
||||
public required string Scope { get; init; }
|
||||
|
||||
[JsonPropertyName("audience")]
|
||||
public required string Audience { get; init; }
|
||||
|
||||
[JsonPropertyName("dpopAlgorithms")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public IReadOnlyList<string>? DpopAlgorithms { get; init; }
|
||||
|
||||
[JsonPropertyName("refreshLeewaySeconds")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public int RefreshLeewaySeconds { get; init; }
|
||||
}
|
||||
|
||||
public sealed class EnvironmentTelemetrySettings
|
||||
{
|
||||
[JsonPropertyName("otlpEndpoint")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? OtlpEndpoint { get; init; }
|
||||
|
||||
[JsonPropertyName("sampleRate")]
|
||||
public double SampleRate { get; init; }
|
||||
}
|
||||
|
||||
public sealed class EnvironmentWelcomeSettings
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Title { get; init; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Message { get; init; }
|
||||
|
||||
[JsonPropertyName("docsUrl")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? DocsUrl { get; init; }
|
||||
}
|
||||
|
||||
public sealed class EnvironmentDoctorSettings
|
||||
{
|
||||
[JsonPropertyName("fixEnabled")]
|
||||
public bool FixEnabled { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Admin endpoints for managing DB-layer environment settings (Layer 3).
|
||||
/// All endpoints require <c>SetupRead</c> or <c>SetupAdmin</c> authorization.
|
||||
/// </summary>
|
||||
public static class EnvironmentSettingsAdminEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapEnvironmentSettingsAdminEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/platform/envsettings/db")
|
||||
.WithTags("Environment Settings Admin");
|
||||
|
||||
group.MapGet("/", async (IEnvironmentSettingsStore store, CancellationToken ct) =>
|
||||
{
|
||||
var all = await store.GetAllAsync(ct);
|
||||
return Results.Ok(all);
|
||||
})
|
||||
.WithName("ListDbEnvironmentSettings")
|
||||
.WithSummary("List all DB-layer environment settings")
|
||||
.Produces<IReadOnlyDictionary<string, string>>(StatusCodes.Status200OK)
|
||||
.RequireAuthorization(PlatformPolicies.SetupRead);
|
||||
|
||||
group.MapPut("/{key}", async (string key, SettingValueRequest body, IEnvironmentSettingsStore store, CancellationToken ct) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return Results.BadRequest(new { error = "Key must not be empty." });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body.Value))
|
||||
return Results.BadRequest(new { error = "Value must not be empty." });
|
||||
|
||||
await store.SetAsync(key, body.Value, body.UpdatedBy ?? "admin", ct);
|
||||
return Results.Ok(new { key, value = body.Value });
|
||||
})
|
||||
.WithName("UpsertDbEnvironmentSetting")
|
||||
.WithSummary("Create or update a DB-layer environment setting")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization(PlatformPolicies.SetupAdmin);
|
||||
|
||||
group.MapDelete("/{key}", async (string key, IEnvironmentSettingsStore store, CancellationToken ct) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return Results.BadRequest(new { error = "Key must not be empty." });
|
||||
|
||||
await store.DeleteAsync(key, ct);
|
||||
return Results.NoContent();
|
||||
})
|
||||
.WithName("DeleteDbEnvironmentSetting")
|
||||
.WithSummary("Delete a DB-layer environment setting")
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization(PlatformPolicies.SetupAdmin);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
public sealed record SettingValueRequest(string Value, string? UpdatedBy = null);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under BUSL-1.1. See LICENSE in the project root.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using StellaOps.Platform.WebService.Options;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Serves <c>GET /platform/envsettings.json</c> — an anonymous endpoint that returns
|
||||
/// the Angular frontend's <see cref="EnvironmentSettingsResponse"/> (matching <c>AppConfig</c>).
|
||||
/// The payload is assembled from three layers: env vars (Layer 1), YAML/JSON config (Layer 2),
|
||||
/// and database overrides (Layer 3) via <see cref="EnvironmentSettingsComposer"/>.
|
||||
/// </summary>
|
||||
public static class EnvironmentSettingsEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapEnvironmentSettingsEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
// Primary route (used when accessed via gateway: /platform/envsettings.json)
|
||||
// and alias route (used when accessing the Platform service directly: /envsettings.json)
|
||||
app.MapGet("/envsettings.json", Handler)
|
||||
.WithTags("Environment Settings")
|
||||
.WithName("GetEnvironmentSettingsAlias")
|
||||
.WithSummary("Alias for /platform/envsettings.json (direct service access)")
|
||||
.Produces<EnvironmentSettingsResponse>(StatusCodes.Status200OK)
|
||||
.AllowAnonymous()
|
||||
.ExcludeFromDescription();
|
||||
|
||||
app.MapGet("/platform/envsettings.json", Handler)
|
||||
.WithTags("Environment Settings")
|
||||
.WithName("GetEnvironmentSettings")
|
||||
.WithSummary("Returns frontend environment configuration (AppConfig)")
|
||||
.WithDescription(
|
||||
"Anonymous endpoint that returns the Angular frontend's AppConfig payload. " +
|
||||
"The response merges three configuration layers: environment variables (lowest), " +
|
||||
"YAML/JSON config, and database overrides (highest). Includes OIDC authority settings, " +
|
||||
"API base URLs, and optional telemetry/welcome/doctor configuration.")
|
||||
.Produces<EnvironmentSettingsResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.AllowAnonymous();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<IResult> Handler(
|
||||
IOptions<PlatformServiceOptions> options,
|
||||
EnvironmentSettingsComposer composer,
|
||||
IEnvironmentSettingsStore envSettingsStore,
|
||||
SetupStateDetector setupDetector,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var platform = options.Value;
|
||||
|
||||
var env = await composer.ComposeAsync(ct);
|
||||
|
||||
// Detect setup state from DB settings (cached, no extra round-trip)
|
||||
var dbSettings = await envSettingsStore.GetAllAsync(ct);
|
||||
var setupState = setupDetector.Detect(platform.Storage, dbSettings);
|
||||
|
||||
var authority = new EnvironmentAuthoritySettings
|
||||
{
|
||||
Issuer = platform.Authority.Issuer,
|
||||
ClientId = env.ClientId,
|
||||
AuthorizeEndpoint = env.AuthorizeEndpoint
|
||||
?? $"{platform.Authority.Issuer}/connect/authorize",
|
||||
TokenEndpoint = env.TokenEndpoint
|
||||
?? $"{platform.Authority.Issuer}/connect/token",
|
||||
LogoutEndpoint = env.LogoutEndpoint,
|
||||
RedirectUri = env.RedirectUri,
|
||||
PostLogoutRedirectUri = env.PostLogoutRedirectUri,
|
||||
Scope = env.Scope,
|
||||
Audience = env.Audience
|
||||
?? platform.Authority.Audiences.FirstOrDefault()
|
||||
?? "stella-ops-api",
|
||||
DpopAlgorithms = env.DpopAlgorithms.Count > 0 ? env.DpopAlgorithms : null,
|
||||
RefreshLeewaySeconds = env.RefreshLeewaySeconds,
|
||||
};
|
||||
|
||||
EnvironmentTelemetrySettings? telemetry = null;
|
||||
if (!string.IsNullOrWhiteSpace(env.OtlpEndpoint) || env.TelemetrySampleRate > 0)
|
||||
{
|
||||
telemetry = new EnvironmentTelemetrySettings
|
||||
{
|
||||
OtlpEndpoint = env.OtlpEndpoint,
|
||||
SampleRate = env.TelemetrySampleRate,
|
||||
};
|
||||
}
|
||||
|
||||
EnvironmentWelcomeSettings? welcome = null;
|
||||
if (env.WelcomeTitle is not null || env.WelcomeMessage is not null || env.WelcomeDocsUrl is not null)
|
||||
{
|
||||
welcome = new EnvironmentWelcomeSettings
|
||||
{
|
||||
Title = env.WelcomeTitle,
|
||||
Message = env.WelcomeMessage,
|
||||
DocsUrl = env.WelcomeDocsUrl,
|
||||
};
|
||||
}
|
||||
|
||||
EnvironmentDoctorSettings? doctor = null;
|
||||
if (env.DoctorFixEnabled)
|
||||
{
|
||||
doctor = new EnvironmentDoctorSettings
|
||||
{
|
||||
FixEnabled = env.DoctorFixEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
var response = new EnvironmentSettingsResponse
|
||||
{
|
||||
Authority = authority,
|
||||
ApiBaseUrls = new Dictionary<string, string>(env.ApiBaseUrls),
|
||||
Telemetry = telemetry,
|
||||
Welcome = welcome,
|
||||
Doctor = doctor,
|
||||
Setup = setupState,
|
||||
};
|
||||
|
||||
return Results.Json(response, statusCode: StatusCodes.Status200OK);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ public sealed class PlatformServiceOptions
|
||||
public PlatformSearchOptions Search { get; set; } = new();
|
||||
public PlatformMetadataOptions Metadata { get; set; } = new();
|
||||
public PlatformStorageOptions Storage { get; set; } = new();
|
||||
public PlatformEnvironmentSettingsOptions EnvironmentSettings { get; set; } = new();
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
@@ -22,15 +23,16 @@ public sealed class PlatformServiceOptions
|
||||
Search.Validate();
|
||||
Metadata.Validate();
|
||||
Storage.Validate();
|
||||
EnvironmentSettings.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlatformAuthorityOptions
|
||||
{
|
||||
public string Issuer { get; set; } = "https://auth.stellaops.local";
|
||||
public string Issuer { get; set; } = "https://authority.stella-ops.local";
|
||||
public string? MetadataAddress { get; set; }
|
||||
public bool RequireHttpsMetadata { get; set; } = true;
|
||||
public List<string> Audiences { get; set; } = new() { "stellaops-api" };
|
||||
public List<string> Audiences { get; set; } = new() { "stella-ops-api" };
|
||||
public List<string> RequiredScopes { get; set; } = new();
|
||||
public List<string> RequiredTenants { get; set; } = new();
|
||||
public List<string> BypassNetworks { get; set; } = new();
|
||||
@@ -56,6 +58,7 @@ public sealed class PlatformCacheOptions
|
||||
public int SearchSeconds { get; set; } = 20;
|
||||
public int MetadataSeconds { get; set; } = 60;
|
||||
public int AnalyticsSeconds { get; set; } = 300;
|
||||
public int EnvironmentSettingsRefreshSeconds { get; set; } = 60;
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
@@ -69,6 +72,7 @@ public sealed class PlatformCacheOptions
|
||||
RequireNonNegative(SearchSeconds, nameof(SearchSeconds));
|
||||
RequireNonNegative(MetadataSeconds, nameof(MetadataSeconds));
|
||||
RequireNonNegative(AnalyticsSeconds, nameof(AnalyticsSeconds));
|
||||
RequireNonNegative(EnvironmentSettingsRefreshSeconds, nameof(EnvironmentSettingsRefreshSeconds));
|
||||
}
|
||||
|
||||
private static void RequireNonNegative(int value, string name)
|
||||
@@ -157,3 +161,42 @@ public sealed class PlatformAnalyticsMaintenanceOptions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the anonymous <c>GET /platform/envsettings.json</c> endpoint
|
||||
/// that serves the Angular frontend's <c>AppConfig</c> payload.
|
||||
/// Bound from <c>Platform:EnvironmentSettings</c>.
|
||||
/// </summary>
|
||||
public sealed class PlatformEnvironmentSettingsOptions
|
||||
{
|
||||
// --- Authority (frontend OIDC client) ---
|
||||
public string ClientId { get; set; } = "stella-ops-ui";
|
||||
public string? AuthorizeEndpoint { get; set; }
|
||||
public string? TokenEndpoint { get; set; }
|
||||
public string? LogoutEndpoint { get; set; }
|
||||
public string RedirectUri { get; set; } = string.Empty;
|
||||
public string? PostLogoutRedirectUri { get; set; }
|
||||
public string Scope { get; set; } = "openid profile email ui.read";
|
||||
public string? Audience { get; set; }
|
||||
public List<string> DpopAlgorithms { get; set; } = new() { "ES256" };
|
||||
public int RefreshLeewaySeconds { get; set; } = 60;
|
||||
|
||||
// --- API base URLs ---
|
||||
public Dictionary<string, string> ApiBaseUrls { get; set; } = new();
|
||||
|
||||
// --- Telemetry (optional) ---
|
||||
public string? OtlpEndpoint { get; set; }
|
||||
public double TelemetrySampleRate { get; set; }
|
||||
|
||||
// --- Welcome (optional) ---
|
||||
public string? WelcomeTitle { get; set; }
|
||||
public string? WelcomeMessage { get; set; }
|
||||
public string? WelcomeDocsUrl { get; set; }
|
||||
|
||||
// --- Doctor (optional) ---
|
||||
public bool DoctorFixEnabled { get; set; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Configuration;
|
||||
using StellaOps.Messaging.DependencyInjection;
|
||||
using StellaOps.Platform.Analytics;
|
||||
using StellaOps.Platform.WebService.Configuration;
|
||||
using StellaOps.Platform.WebService.Constants;
|
||||
using StellaOps.Platform.WebService.Endpoints;
|
||||
using StellaOps.Platform.WebService.Options;
|
||||
@@ -40,6 +42,10 @@ builder.Services.AddOptions<PlatformServiceOptions>()
|
||||
})
|
||||
.ValidateOnStart();
|
||||
|
||||
// Layer 1: env var post-configure (STELLAOPS_*_URL -> ApiBaseUrls, lowest priority after YAML)
|
||||
builder.Services.AddSingleton<IPostConfigureOptions<PlatformServiceOptions>, StellaOpsEnvVarPostConfigure>();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddOpenApi();
|
||||
@@ -162,12 +168,19 @@ if (!string.IsNullOrWhiteSpace(bootstrapOptions.Storage.PostgresConnectionString
|
||||
builder.Services.AddSingleton(
|
||||
Npgsql.NpgsqlDataSource.Create(bootstrapOptions.Storage.PostgresConnectionString));
|
||||
builder.Services.AddSingleton<IScoreHistoryStore, PostgresScoreHistoryStore>();
|
||||
builder.Services.AddSingleton<IEnvironmentSettingsStore, PostgresEnvironmentSettingsStore>();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddSingleton<IScoreHistoryStore, InMemoryScoreHistoryStore>();
|
||||
builder.Services.AddSingleton<IEnvironmentSettingsStore, InMemoryEnvironmentSettingsStore>();
|
||||
}
|
||||
|
||||
// Environment settings composer (3-layer merge: env vars -> YAML -> DB)
|
||||
builder.Services.AddSingleton<EnvironmentSettingsComposer>();
|
||||
builder.Services.AddSingleton<SetupStateDetector>();
|
||||
builder.Services.AddHostedService<EnvironmentSettingsRefreshService>();
|
||||
|
||||
builder.Services.AddSingleton<IScoreEvaluationService, ScoreEvaluationService>();
|
||||
|
||||
// Policy interop services (import/export between JSON PolicyPack v2 and OPA/Rego)
|
||||
@@ -184,7 +197,9 @@ builder.Services.TryAddStellaRouter(
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: routerOptions);
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("platform");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("platform");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -196,11 +211,14 @@ if (!string.Equals(bootstrapOptions.Storage.Driver, "memory", StringComparison.O
|
||||
app.Logger.LogWarning("Platform storage driver {Driver} is not implemented; using in-memory stores.", bootstrapOptions.Storage.Driver);
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseStellaOpsTelemetryContext();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.TryUseStellaRouter(routerOptions);
|
||||
|
||||
app.MapEnvironmentSettingsEndpoints();
|
||||
app.MapEnvironmentSettingsAdminEndpoints();
|
||||
app.MapPlatformEndpoints();
|
||||
app.MapSetupEndpoints();
|
||||
app.MapAnalyticsEndpoints();
|
||||
|
||||
@@ -4,9 +4,62 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"PLATFORM_ENVIRONMENTSETTINGS__REDIRECTURI": "https://stella-ops.local:10000/auth/callback",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000",
|
||||
|
||||
"STELLAOPS_PLATFORM_URL": "https://platform.stella-ops.local",
|
||||
"STELLAOPS_ROUTER_URL": "https://router.stella-ops.local",
|
||||
"STELLAOPS_AUTHORITY_URL": "https://authority.stella-ops.local",
|
||||
"STELLAOPS_GATEWAY_URL": "https://gateway.stella-ops.local",
|
||||
"STELLAOPS_ATTESTOR_URL": "https://attestor.stella-ops.local",
|
||||
"STELLAOPS_EVIDENCELOCKER_URL": "https://evidencelocker.stella-ops.local",
|
||||
"STELLAOPS_SCANNER_URL": "https://scanner.stella-ops.local",
|
||||
"STELLAOPS_CONCELIER_URL": "https://concelier.stella-ops.local",
|
||||
"STELLAOPS_EXCITITOR_URL": "https://excititor.stella-ops.local",
|
||||
"STELLAOPS_VEXHUB_URL": "https://vexhub.stella-ops.local",
|
||||
"STELLAOPS_VEXLENS_URL": "https://vexlens.stella-ops.local",
|
||||
"STELLAOPS_VULNEXPLORER_URL": "https://vulnexplorer.stella-ops.local",
|
||||
"STELLAOPS_POLICY_ENGINE_URL": "https://policy-engine.stella-ops.local",
|
||||
"STELLAOPS_POLICY_GATEWAY_URL": "https://policy-gateway.stella-ops.local",
|
||||
"STELLAOPS_RISKENGINE_URL": "https://riskengine.stella-ops.local",
|
||||
"STELLAOPS_ORCHESTRATOR_URL": "https://orchestrator.stella-ops.local",
|
||||
"STELLAOPS_TASKRUNNER_URL": "https://taskrunner.stella-ops.local",
|
||||
"STELLAOPS_SCHEDULER_URL": "https://scheduler.stella-ops.local",
|
||||
"STELLAOPS_GRAPH_URL": "https://graph.stella-ops.local",
|
||||
"STELLAOPS_CARTOGRAPHER_URL": "https://cartographer.stella-ops.local",
|
||||
"STELLAOPS_REACHGRAPH_URL": "https://reachgraph.stella-ops.local",
|
||||
"STELLAOPS_TIMELINEINDEXER_URL": "https://timelineindexer.stella-ops.local",
|
||||
"STELLAOPS_TIMELINE_URL": "https://timeline.stella-ops.local",
|
||||
"STELLAOPS_FINDINGS_LEDGER_URL": "https://findings.stella-ops.local",
|
||||
"STELLAOPS_DOCTOR_URL": "https://doctor.stella-ops.local",
|
||||
"STELLAOPS_OPSMEMORY_URL": "https://opsmemory.stella-ops.local",
|
||||
"STELLAOPS_NOTIFIER_URL": "https://notifier.stella-ops.local",
|
||||
"STELLAOPS_NOTIFY_URL": "https://notify.stella-ops.local",
|
||||
"STELLAOPS_SIGNER_URL": "https://signer.stella-ops.local",
|
||||
"STELLAOPS_SMREMOTE_URL": "https://smremote.stella-ops.local",
|
||||
"STELLAOPS_AIRGAP_CONTROLLER_URL": "https://airgap-controller.stella-ops.local",
|
||||
"STELLAOPS_AIRGAP_TIME_URL": "https://airgap-time.stella-ops.local",
|
||||
"STELLAOPS_PACKSREGISTRY_URL": "https://packsregistry.stella-ops.local",
|
||||
"STELLAOPS_REGISTRY_TOKENSERVICE_URL": "https://registry-token.stella-ops.local",
|
||||
"STELLAOPS_BINARYINDEX_URL": "https://binaryindex.stella-ops.local",
|
||||
"STELLAOPS_ISSUERDIRECTORY_URL": "https://issuerdirectory.stella-ops.local",
|
||||
"STELLAOPS_SYMBOLS_URL": "https://symbols.stella-ops.local",
|
||||
"STELLAOPS_SBOMSERVICE_URL": "https://sbomservice.stella-ops.local",
|
||||
"STELLAOPS_EXPORTCENTER_URL": "https://exportcenter.stella-ops.local",
|
||||
"STELLAOPS_REPLAY_URL": "https://replay.stella-ops.local",
|
||||
"STELLAOPS_INTEGRATIONS_URL": "https://integrations.stella-ops.local",
|
||||
"STELLAOPS_SIGNALS_URL": "https://signals.stella-ops.local",
|
||||
"STELLAOPS_ADVISORYAI_URL": "https://advisoryai.stella-ops.local",
|
||||
"STELLAOPS_UNKNOWNS_URL": "https://unknowns.stella-ops.local",
|
||||
|
||||
"STELLAOPS_POLICY_URL": "https://policy-engine.stella-ops.local",
|
||||
"STELLAOPS_LEDGER_URL": "https://findings.stella-ops.local",
|
||||
"STELLAOPS_VEX_URL": "https://vexhub.stella-ops.local",
|
||||
"STELLAOPS_EXCITOR_URL": "https://excititor.stella-ops.local"
|
||||
},
|
||||
"applicationUrl": "https://localhost:52413;http://localhost:52415"
|
||||
"applicationUrl": "https://localhost:10010;http://localhost:10011"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Platform.WebService.Options;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Composes a final <see cref="PlatformEnvironmentSettingsOptions"/> from three layers:
|
||||
/// <list type="number">
|
||||
/// <item>Environment variables (via <see cref="Configuration.StellaOpsEnvVarPostConfigure"/>)</item>
|
||||
/// <item>YAML/JSON configuration (standard IOptions binding)</item>
|
||||
/// <item>Database (via <see cref="IEnvironmentSettingsStore"/>)</item>
|
||||
/// </list>
|
||||
/// Layer 3 (DB) has highest priority and is overlaid last.
|
||||
/// </summary>
|
||||
public sealed class EnvironmentSettingsComposer
|
||||
{
|
||||
private readonly IOptionsMonitor<PlatformServiceOptions> _optionsMonitor;
|
||||
private readonly IEnvironmentSettingsStore _store;
|
||||
|
||||
public EnvironmentSettingsComposer(
|
||||
IOptionsMonitor<PlatformServiceOptions> optionsMonitor,
|
||||
IEnvironmentSettingsStore store)
|
||||
{
|
||||
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the merged environment settings with all three layers applied.
|
||||
/// </summary>
|
||||
public async Task<PlatformEnvironmentSettingsOptions> ComposeAsync(CancellationToken ct = default)
|
||||
{
|
||||
var source = _optionsMonitor.CurrentValue.EnvironmentSettings;
|
||||
|
||||
// Clone the options so we don't mutate the originals
|
||||
var merged = new PlatformEnvironmentSettingsOptions
|
||||
{
|
||||
ClientId = source.ClientId,
|
||||
AuthorizeEndpoint = source.AuthorizeEndpoint,
|
||||
TokenEndpoint = source.TokenEndpoint,
|
||||
LogoutEndpoint = source.LogoutEndpoint,
|
||||
RedirectUri = source.RedirectUri,
|
||||
PostLogoutRedirectUri = source.PostLogoutRedirectUri,
|
||||
Scope = source.Scope,
|
||||
Audience = source.Audience,
|
||||
DpopAlgorithms = new List<string>(source.DpopAlgorithms),
|
||||
RefreshLeewaySeconds = source.RefreshLeewaySeconds,
|
||||
ApiBaseUrls = new Dictionary<string, string>(source.ApiBaseUrls, StringComparer.OrdinalIgnoreCase),
|
||||
OtlpEndpoint = source.OtlpEndpoint,
|
||||
TelemetrySampleRate = source.TelemetrySampleRate,
|
||||
WelcomeTitle = source.WelcomeTitle,
|
||||
WelcomeMessage = source.WelcomeMessage,
|
||||
WelcomeDocsUrl = source.WelcomeDocsUrl,
|
||||
DoctorFixEnabled = source.DoctorFixEnabled,
|
||||
};
|
||||
|
||||
// Overlay DB values (Layer 3 — highest priority)
|
||||
var dbSettings = await _store.GetAllAsync(ct).ConfigureAwait(false);
|
||||
|
||||
foreach (var (key, value) in dbSettings)
|
||||
{
|
||||
if (key.StartsWith("ApiBaseUrls:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var serviceName = key["ApiBaseUrls:".Length..];
|
||||
merged.ApiBaseUrls[serviceName] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Map scalar settings by key name
|
||||
ApplyScalarSetting(merged, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
private static void ApplyScalarSetting(PlatformEnvironmentSettingsOptions options, string key, string value)
|
||||
{
|
||||
// Case-insensitive key matching for known scalar settings
|
||||
if (string.Equals(key, "ClientId", StringComparison.OrdinalIgnoreCase))
|
||||
options.ClientId = value;
|
||||
else if (string.Equals(key, "AuthorizeEndpoint", StringComparison.OrdinalIgnoreCase))
|
||||
options.AuthorizeEndpoint = value;
|
||||
else if (string.Equals(key, "TokenEndpoint", StringComparison.OrdinalIgnoreCase))
|
||||
options.TokenEndpoint = value;
|
||||
else if (string.Equals(key, "LogoutEndpoint", StringComparison.OrdinalIgnoreCase))
|
||||
options.LogoutEndpoint = value;
|
||||
else if (string.Equals(key, "RedirectUri", StringComparison.OrdinalIgnoreCase))
|
||||
options.RedirectUri = value;
|
||||
else if (string.Equals(key, "PostLogoutRedirectUri", StringComparison.OrdinalIgnoreCase))
|
||||
options.PostLogoutRedirectUri = value;
|
||||
else if (string.Equals(key, "Scope", StringComparison.OrdinalIgnoreCase))
|
||||
options.Scope = value;
|
||||
else if (string.Equals(key, "Audience", StringComparison.OrdinalIgnoreCase))
|
||||
options.Audience = value;
|
||||
else if (string.Equals(key, "OtlpEndpoint", StringComparison.OrdinalIgnoreCase))
|
||||
options.OtlpEndpoint = value;
|
||||
else if (string.Equals(key, "TelemetrySampleRate", StringComparison.OrdinalIgnoreCase)
|
||||
&& double.TryParse(value, out var rate))
|
||||
options.TelemetrySampleRate = rate;
|
||||
else if (string.Equals(key, "WelcomeTitle", StringComparison.OrdinalIgnoreCase))
|
||||
options.WelcomeTitle = value;
|
||||
else if (string.Equals(key, "WelcomeMessage", StringComparison.OrdinalIgnoreCase))
|
||||
options.WelcomeMessage = value;
|
||||
else if (string.Equals(key, "WelcomeDocsUrl", StringComparison.OrdinalIgnoreCase))
|
||||
options.WelcomeDocsUrl = value;
|
||||
else if (string.Equals(key, "DoctorFixEnabled", StringComparison.OrdinalIgnoreCase)
|
||||
&& bool.TryParse(value, out var fix))
|
||||
options.DoctorFixEnabled = fix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Platform.WebService.Options;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Background service that periodically invalidates the <see cref="IEnvironmentSettingsStore"/>
|
||||
/// cache so DB-layer changes are picked up without restart.
|
||||
/// </summary>
|
||||
public sealed class EnvironmentSettingsRefreshService : BackgroundService
|
||||
{
|
||||
private readonly IEnvironmentSettingsStore _store;
|
||||
private readonly IOptionsMonitor<PlatformServiceOptions> _optionsMonitor;
|
||||
private readonly ILogger<EnvironmentSettingsRefreshService> _logger;
|
||||
|
||||
public EnvironmentSettingsRefreshService(
|
||||
IEnvironmentSettingsStore store,
|
||||
IOptionsMonitor<PlatformServiceOptions> optionsMonitor,
|
||||
ILogger<EnvironmentSettingsRefreshService> logger)
|
||||
{
|
||||
_store = store;
|
||||
_optionsMonitor = optionsMonitor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("EnvironmentSettingsRefreshService started");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var seconds = _optionsMonitor.CurrentValue.Cache.EnvironmentSettingsRefreshSeconds;
|
||||
if (seconds <= 0) seconds = 60;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(seconds), stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_store.InvalidateCache();
|
||||
_logger.LogDebug("Environment settings cache invalidated");
|
||||
}
|
||||
|
||||
_logger.LogInformation("EnvironmentSettingsRefreshService stopped");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
namespace StellaOps.Platform.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Persistence interface for DB-layer environment settings (Layer 3).
|
||||
/// Keys follow the pattern <c>ApiBaseUrls:scanner</c>, <c>OtlpEndpoint</c>, etc.
|
||||
/// </summary>
|
||||
public interface IEnvironmentSettingsStore
|
||||
{
|
||||
Task<IReadOnlyDictionary<string, string>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<string?> GetAsync(string key, CancellationToken ct = default);
|
||||
Task SetAsync(string key, string value, string updatedBy = "system", CancellationToken ct = default);
|
||||
Task DeleteAsync(string key, CancellationToken ct = default);
|
||||
void InvalidateCache();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of <see cref="IEnvironmentSettingsStore"/> for development
|
||||
/// and environments without PostgreSQL.
|
||||
/// </summary>
|
||||
public sealed class InMemoryEnvironmentSettingsStore : IEnvironmentSettingsStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, string> _store = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public Task<IReadOnlyDictionary<string, string>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
var snapshot = new Dictionary<string, string>(_store, StringComparer.OrdinalIgnoreCase);
|
||||
return Task.FromResult<IReadOnlyDictionary<string, string>>(snapshot);
|
||||
}
|
||||
|
||||
public Task<string?> GetAsync(string key, CancellationToken ct = default)
|
||||
{
|
||||
_store.TryGetValue(key, out var value);
|
||||
return Task.FromResult(value);
|
||||
}
|
||||
|
||||
public Task SetAsync(string key, string value, string updatedBy = "system", CancellationToken ct = default)
|
||||
{
|
||||
_store[key] = value;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteAsync(string key, CancellationToken ct = default)
|
||||
{
|
||||
_store.TryRemove(key, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void InvalidateCache()
|
||||
{
|
||||
// No-op for in-memory store.
|
||||
}
|
||||
}
|
||||
@@ -23,15 +23,18 @@ namespace StellaOps.Platform.WebService.Services;
|
||||
public sealed class PlatformSetupService
|
||||
{
|
||||
private readonly PlatformSetupStore _store;
|
||||
private readonly IEnvironmentSettingsStore _envSettingsStore;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<PlatformSetupService> _logger;
|
||||
|
||||
public PlatformSetupService(
|
||||
PlatformSetupStore store,
|
||||
IEnvironmentSettingsStore envSettingsStore,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<PlatformSetupService> logger)
|
||||
{
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_envSettingsStore = envSettingsStore ?? throw new ArgumentNullException(nameof(envSettingsStore));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
@@ -118,7 +121,7 @@ public sealed class PlatformSetupService
|
||||
/// <summary>
|
||||
/// Executes a setup step with validation.
|
||||
/// </summary>
|
||||
public Task<ExecuteSetupStepResponse> ExecuteStepAsync(
|
||||
public async Task<ExecuteSetupStepResponse> ExecuteStepAsync(
|
||||
PlatformRequestContext context,
|
||||
ExecuteSetupStepRequest request,
|
||||
CancellationToken ct)
|
||||
@@ -145,11 +148,11 @@ public sealed class PlatformSetupService
|
||||
ErrorMessage = $"Blocked by incomplete dependencies: {string.Join(", ", blockedByDeps)}"
|
||||
};
|
||||
|
||||
return Task.FromResult(new ExecuteSetupStepResponse(
|
||||
return new ExecuteSetupStepResponse(
|
||||
StepState: stepState,
|
||||
Success: false,
|
||||
ErrorMessage: stepState.ErrorMessage,
|
||||
SuggestedFixes: ImmutableArray<SetupSuggestedFix>.Empty));
|
||||
SuggestedFixes: ImmutableArray<SetupSuggestedFix>.Empty);
|
||||
}
|
||||
|
||||
var nowUtc = _timeProvider.GetUtcNow();
|
||||
@@ -189,6 +192,23 @@ public sealed class PlatformSetupService
|
||||
|
||||
_store.Upsert(context.TenantId, updatedSession);
|
||||
|
||||
// Record resume point so the frontend can resume at the next step
|
||||
if (allPassed)
|
||||
{
|
||||
var nextStep = updatedSteps
|
||||
.Where(s => s.Status != SetupStepStatus.Passed && s.Status != SetupStepStatus.Skipped)
|
||||
.OrderBy(s => (int)s.StepId)
|
||||
.FirstOrDefault();
|
||||
|
||||
var resumeValue = nextStep is not null
|
||||
? nextStep.StepId.ToString().ToLowerInvariant()
|
||||
: "true"; // all steps done
|
||||
|
||||
await _envSettingsStore.SetAsync(
|
||||
SetupStateDetector.SetupCompleteKey, resumeValue, "setup-wizard", ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Executed step {StepId} for session {SessionId}: {Status}.",
|
||||
request.StepId, session.SessionId, newStatus);
|
||||
@@ -197,11 +217,11 @@ public sealed class PlatformSetupService
|
||||
? ImmutableArray<SetupSuggestedFix>.Empty
|
||||
: GenerateSuggestedFixes(stepDef, checkResults);
|
||||
|
||||
return Task.FromResult(new ExecuteSetupStepResponse(
|
||||
return new ExecuteSetupStepResponse(
|
||||
StepState: updatedStepState,
|
||||
Success: allPassed,
|
||||
ErrorMessage: errorMessage,
|
||||
SuggestedFixes: suggestedFixes));
|
||||
SuggestedFixes: suggestedFixes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -266,7 +286,7 @@ public sealed class PlatformSetupService
|
||||
/// <summary>
|
||||
/// Finalizes the setup session.
|
||||
/// </summary>
|
||||
public Task<FinalizeSetupSessionResponse> FinalizeSessionAsync(
|
||||
public async Task<FinalizeSetupSessionResponse> FinalizeSessionAsync(
|
||||
PlatformRequestContext context,
|
||||
FinalizeSetupSessionRequest request,
|
||||
CancellationToken ct)
|
||||
@@ -319,16 +339,24 @@ public sealed class PlatformSetupService
|
||||
|
||||
_store.Upsert(context.TenantId, updatedSession);
|
||||
|
||||
// Mark setup as complete in environment settings so the frontend knows
|
||||
if (finalStatus == SetupSessionStatus.Completed || finalStatus == SetupSessionStatus.CompletedPartial)
|
||||
{
|
||||
await _envSettingsStore.SetAsync(
|
||||
SetupStateDetector.SetupCompleteKey, "true", "setup-wizard", ct)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Finalized setup session {SessionId} with status {Status}.",
|
||||
session.SessionId, finalStatus);
|
||||
|
||||
return Task.FromResult(new FinalizeSetupSessionResponse(
|
||||
return new FinalizeSetupSessionResponse(
|
||||
FinalStatus: finalStatus,
|
||||
CompletedSteps: completedSteps,
|
||||
SkippedSteps: skippedSteps,
|
||||
FailedSteps: failedSteps,
|
||||
ReportPath: null));
|
||||
ReportPath: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Copyright (c) 2025 StellaOps
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IEnvironmentSettingsStore"/>.
|
||||
/// Reads from <c>platform.environment_settings</c> with an in-memory cache
|
||||
/// that is invalidated periodically by <see cref="EnvironmentSettingsRefreshService"/>.
|
||||
/// </summary>
|
||||
public sealed class PostgresEnvironmentSettingsStore : IEnvironmentSettingsStore
|
||||
{
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
private readonly ILogger<PostgresEnvironmentSettingsStore> _logger;
|
||||
private volatile IReadOnlyDictionary<string, string>? _cache;
|
||||
private readonly object _cacheLock = new();
|
||||
|
||||
private const string SelectAllSql = """
|
||||
SELECT key, value FROM platform.environment_settings ORDER BY key
|
||||
""";
|
||||
|
||||
private const string SelectOneSql = """
|
||||
SELECT value FROM platform.environment_settings WHERE key = @key
|
||||
""";
|
||||
|
||||
private const string UpsertSql = """
|
||||
INSERT INTO platform.environment_settings (key, value, updated_at, updated_by)
|
||||
VALUES (@key, @value, now(), @updated_by)
|
||||
ON CONFLICT (key) DO UPDATE SET value = @value, updated_at = now(), updated_by = @updated_by
|
||||
""";
|
||||
|
||||
private const string DeleteSql = """
|
||||
DELETE FROM platform.environment_settings WHERE key = @key
|
||||
""";
|
||||
|
||||
public PostgresEnvironmentSettingsStore(
|
||||
NpgsqlDataSource dataSource,
|
||||
ILogger<PostgresEnvironmentSettingsStore>? logger = null)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<PostgresEnvironmentSettingsStore>.Instance;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<string, string>> GetAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
var cached = _cache;
|
||||
if (cached is not null)
|
||||
return cached;
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectAllSql, conn);
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
dict[reader.GetString(0)] = reader.GetString(1);
|
||||
}
|
||||
|
||||
var result = dict;
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_cache ??= result;
|
||||
}
|
||||
|
||||
return _cache;
|
||||
}
|
||||
|
||||
public async Task<string?> GetAsync(string key, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
||||
|
||||
var all = await GetAllAsync(ct).ConfigureAwait(false);
|
||||
return all.TryGetValue(key, out var value) ? value : null;
|
||||
}
|
||||
|
||||
public async Task SetAsync(string key, string value, string updatedBy = "system", CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(UpsertSql, conn);
|
||||
cmd.Parameters.AddWithValue("key", key);
|
||||
cmd.Parameters.AddWithValue("value", value);
|
||||
cmd.Parameters.AddWithValue("updated_by", updatedBy);
|
||||
|
||||
await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
|
||||
InvalidateCache();
|
||||
|
||||
_logger.LogInformation("Environment setting {Key} updated by {UpdatedBy}", key, updatedBy);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string key, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(DeleteSql, conn);
|
||||
cmd.Parameters.AddWithValue("key", key);
|
||||
|
||||
var rows = await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
|
||||
InvalidateCache();
|
||||
|
||||
_logger.LogInformation("Environment setting {Key} deleted ({Rows} rows affected)", key, rows);
|
||||
}
|
||||
|
||||
public void InvalidateCache()
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_cache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under BUSL-1.1. See LICENSE in the project root.
|
||||
|
||||
using StellaOps.Platform.WebService.Options;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Detects whether the platform requires initial setup, has completed setup,
|
||||
/// or is mid-way through the setup wizard.
|
||||
/// </summary>
|
||||
public sealed class SetupStateDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// DB settings key written by the setup wizard to record completion or resume state.
|
||||
/// </summary>
|
||||
public const string SetupCompleteKey = "SetupComplete";
|
||||
|
||||
/// <summary>
|
||||
/// Returns the setup state for inclusion in the <c>envsettings.json</c> response.
|
||||
/// <list type="bullet">
|
||||
/// <item><c>null</c> — setup required (no DB configured, or fresh empty DB)</item>
|
||||
/// <item><c>"complete"</c> — setup done, normal operation</item>
|
||||
/// <item>Any other value — a <c>SetupStepId</c> to resume the wizard at</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public string? Detect(
|
||||
PlatformStorageOptions storage,
|
||||
IReadOnlyDictionary<string, string> dbSettings)
|
||||
{
|
||||
// 1. No DB configured → needs setup
|
||||
if (string.IsNullOrWhiteSpace(storage.PostgresConnectionString))
|
||||
return null;
|
||||
|
||||
// 2. Explicit SetupComplete key in DB
|
||||
if (dbSettings.TryGetValue(SetupCompleteKey, out var value))
|
||||
{
|
||||
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|
||||
? "complete"
|
||||
: value; // step ID to resume at
|
||||
}
|
||||
|
||||
// 3. No SetupComplete key but other settings exist → existing deployment (upgrade scenario)
|
||||
if (dbSettings.Count > 0)
|
||||
return "complete";
|
||||
|
||||
// 4. Empty DB settings + DB configured → fresh DB, needs setup
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Migration: 044_PlatformEnvironmentSettings
|
||||
-- Purpose: Create platform.environment_settings table for DB-layer service URL configuration
|
||||
-- Sprint: SPRINT_20260202_001_Platform_port_registry_env_config
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS platform;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform.environment_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_by TEXT NOT NULL DEFAULT 'system'
|
||||
);
|
||||
|
||||
COMMENT ON TABLE platform.environment_settings IS
|
||||
'Key-value store for environment settings (Layer 3). Keys follow the pattern ApiBaseUrls:{service}, OtlpEndpoint, ClientId, etc.';
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under BUSL-1.1. See LICENSE in the project root.
|
||||
|
||||
using StellaOps.Platform.WebService.Options;
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Tests;
|
||||
|
||||
public sealed class SetupStateDetectorTests
|
||||
{
|
||||
private readonly SetupStateDetector _detector = new();
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Detect_NoPostgresConnectionString_ReturnsNull()
|
||||
{
|
||||
var storage = new PlatformStorageOptions { PostgresConnectionString = null };
|
||||
var dbSettings = new Dictionary<string, string>();
|
||||
|
||||
var result = _detector.Detect(storage, dbSettings);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Detect_EmptyPostgresConnectionString_ReturnsNull()
|
||||
{
|
||||
var storage = new PlatformStorageOptions { PostgresConnectionString = " " };
|
||||
var dbSettings = new Dictionary<string, string>();
|
||||
|
||||
var result = _detector.Detect(storage, dbSettings);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Detect_FreshDbNoKeys_ReturnsNull()
|
||||
{
|
||||
var storage = new PlatformStorageOptions
|
||||
{
|
||||
PostgresConnectionString = "Host=localhost;Database=stella"
|
||||
};
|
||||
var dbSettings = new Dictionary<string, string>();
|
||||
|
||||
var result = _detector.Detect(storage, dbSettings);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Detect_SetupCompleteTrue_ReturnsComplete()
|
||||
{
|
||||
var storage = new PlatformStorageOptions
|
||||
{
|
||||
PostgresConnectionString = "Host=localhost;Database=stella"
|
||||
};
|
||||
var dbSettings = new Dictionary<string, string>
|
||||
{
|
||||
[SetupStateDetector.SetupCompleteKey] = "true"
|
||||
};
|
||||
|
||||
var result = _detector.Detect(storage, dbSettings);
|
||||
|
||||
Assert.Equal("complete", result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Detect_SetupCompleteTrueCaseInsensitive_ReturnsComplete()
|
||||
{
|
||||
var storage = new PlatformStorageOptions
|
||||
{
|
||||
PostgresConnectionString = "Host=localhost;Database=stella"
|
||||
};
|
||||
var dbSettings = new Dictionary<string, string>
|
||||
{
|
||||
[SetupStateDetector.SetupCompleteKey] = "True"
|
||||
};
|
||||
|
||||
var result = _detector.Detect(storage, dbSettings);
|
||||
|
||||
Assert.Equal("complete", result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Detect_SetupCompleteStepId_ReturnsStepId()
|
||||
{
|
||||
var storage = new PlatformStorageOptions
|
||||
{
|
||||
PostgresConnectionString = "Host=localhost;Database=stella"
|
||||
};
|
||||
var dbSettings = new Dictionary<string, string>
|
||||
{
|
||||
[SetupStateDetector.SetupCompleteKey] = "migrations"
|
||||
};
|
||||
|
||||
var result = _detector.Detect(storage, dbSettings);
|
||||
|
||||
Assert.Equal("migrations", result);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Detect_ExistingDeploymentNoSetupCompleteKey_ReturnsComplete()
|
||||
{
|
||||
var storage = new PlatformStorageOptions
|
||||
{
|
||||
PostgresConnectionString = "Host=localhost;Database=stella"
|
||||
};
|
||||
var dbSettings = new Dictionary<string, string>
|
||||
{
|
||||
["ApiBaseUrls:scanner"] = "https://scanner.local",
|
||||
["ClientId"] = "stella-ops-ui"
|
||||
};
|
||||
|
||||
var result = _detector.Detect(storage, dbSettings);
|
||||
|
||||
Assert.Equal("complete", result);
|
||||
}
|
||||
}
|
||||
@@ -65,3 +65,14 @@ The Policy module includes suppression primitives for Smart-Diff:
|
||||
- Note blockers with the specific decision needed.
|
||||
- When policy contracts change, update both module docs and consumer documentation.
|
||||
|
||||
## Service Endpoints
|
||||
|
||||
### Policy Engine (Slot 14)
|
||||
- Development: https://localhost:10140, http://localhost:10141
|
||||
- Local alias: https://policy-engine.stella-ops.local, http://policy-engine.stella-ops.local
|
||||
- Env var: STELLAOPS_POLICY_ENGINE_URL
|
||||
|
||||
### Policy Gateway (Slot 15)
|
||||
- Development: https://localhost:10150, http://localhost:10151
|
||||
- Local alias: https://policy-gateway.stella-ops.local, http://policy-gateway.stella-ops.local
|
||||
- Env var: STELLAOPS_POLICY_GATEWAY_URL
|
||||
|
||||
@@ -235,6 +235,7 @@ builder.Services.AddSingleton<IRuntimeEvaluationExecutor, RuntimeEvaluationExecu
|
||||
builder.Services.AddVexDecisionEmitter(); // POLICY-VEX-401-006
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddHealthChecks();
|
||||
@@ -302,8 +303,11 @@ if (bootstrap.Options.Authority.Enabled)
|
||||
});
|
||||
}
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("policy-engine");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("policy-engine");
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62533;http://localhost:62534"
|
||||
"applicationUrl": "https://localhost:10140;http://localhost:10141"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ builder.Services.AddOptions<ToolLatticeOptions>()
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<PolicyGatewayOptions>>().Value);
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddSystemGuidProvider();
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
builder.Services.AddRouting(options => options.LowercaseUrls = true);
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddHealthChecks();
|
||||
@@ -254,7 +255,9 @@ builder.Services.AddHttpClient<IPolicyEngineClient, PolicyEngineClient>((service
|
||||
})
|
||||
.AddPolicyHandler(static (provider, _) => CreatePolicyEngineRetryPolicy(provider));
|
||||
|
||||
builder.TryAddStellaOpsLocalBinding("policy-gateway");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("policy-gateway");
|
||||
|
||||
app.UseExceptionHandler(static appBuilder => appBuilder.Run(async context =>
|
||||
{
|
||||
@@ -264,6 +267,7 @@ app.UseExceptionHandler(static appBuilder => appBuilder.Run(async context =>
|
||||
|
||||
app.UseStatusCodePages();
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62529;http://localhost:62532"
|
||||
"applicationUrl": "https://localhost:10150;http://localhost:10151"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,3 +27,8 @@
|
||||
- Unit tests for canonicalization, digests, and slice builders.
|
||||
- Integration tests for ingest, slice, and replay endpoints.
|
||||
- Determinism tests: same inputs yield identical digests and slices.
|
||||
|
||||
## Service Endpoints
|
||||
- Development: https://localhost:10220, http://localhost:10221
|
||||
- Local alias: https://reachgraph.stella-ops.local, http://reachgraph.stella-ops.local
|
||||
- Env var: STELLAOPS_REACHGRAPH_URL
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Licensed to StellaOps under the BUSL-1.1 license.
|
||||
|
||||
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Npgsql;
|
||||
using StackExchange.Redis;
|
||||
@@ -101,7 +101,11 @@ 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())
|
||||
@@ -112,6 +116,7 @@ if (app.Environment.IsDevelopment())
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseResponseCompression();
|
||||
app.UseStellaOpsCors();
|
||||
app.UseRateLimiter();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:49951;http://localhost:49952"
|
||||
"applicationUrl": "https://localhost:10220;http://localhost:10221"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user