stabilizaiton work - projects rework for maintenanceability and ui livening

This commit is contained in:
master
2026-02-03 23:40:04 +02:00
parent 074ce117ba
commit 557feefdc3
3305 changed files with 186813 additions and 107843 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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");

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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" />

View File

@@ -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

View File

@@ -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();

View File

@@ -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"
}
}
}

View File

@@ -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). |

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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"
}
}
}
}

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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) =>

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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()
{
}
}

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -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();
}

View File

@@ -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.
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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.';

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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();

View File

@@ -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"
}
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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