using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace ZB.MOM.WW.Secrets.Replicator.Grpc.DependencyInjection;
///
/// Dependency-injection and endpoint wiring for hosting the central secrets hub.
///
///
///
/// Register this on the central node only. A site node registers the client half instead —
/// the two are separate registrations on purpose, so "central hosts, sites follow" is visible in
/// each role's composition root rather than inferred from configuration.
///
///
/// Every node must be configured with the same KEK. The hub serves ciphertext, so a follower
/// whose master key differs cannot decrypt what it pulls; it fails closed on resolve with a
/// kek_id mismatch, which reads like data corruption but is a deployment error.
///
///
public static class SecretsGrpcHubServerExtensions
{
///
/// Binds and validates , registers the fail-closed
/// bearer interceptor, and attaches it to .
///
///
/// Validation runs here, eagerly, as well as through the options pipeline: a hub with no bearer
/// token should refuse to start rather than start and refuse every follower. The interceptor's
/// deny-all is the second layer, for the case where a host constructs the options some other way.
/// The service requires an ISecretStore in the container — register the store first.
///
/// The service collection to add to.
/// The application configuration.
/// Configuration section holding the hub options.
/// The same instance, for chaining.
///
/// No bearer token is configured, or the hub has already been registered.
///
public static IServiceCollection AddZbSecretsGrpcHub(
this IServiceCollection services,
IConfiguration configuration,
string sectionPath = "Secrets:GrpcHub")
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
ArgumentException.ThrowIfNullOrWhiteSpace(sectionPath);
// Registering twice is refused rather than tolerated, because the two calls do NOT compose:
// options binding MERGES (a key absent from the second section leaves the first section's
// value standing) while the SectionPath stamp below is last-one-wins. A hub registered from
// two sections could therefore serve a token bound from one of them while every failure
// message names the other — a misdirection that outlives the mistake that caused it.
if (services.Any(descriptor => descriptor.ServiceType == typeof(HubRegistrationMarker)))
{
throw new InvalidOperationException(
"The gRPC secrets hub may only be registered once; AddZbSecretsGrpcHub was called " +
"more than once. Remove the duplicate call — a second registration would merge its " +
"configuration into the first and leave the reported section path pointing at " +
"whichever call ran last.");
}
// Registered as an instance so nothing ever has to activate it: the marker exists to be
// looked for, not resolved.
services.AddSingleton(new HubRegistrationMarker());
IConfigurationSection section = configuration.GetSection(sectionPath);
services.Configure(section);
// SectionPath is internal, so binding cannot set it; stamping it here is what lets the
// startup failure and the runtime warning both name the key this host actually reads.
services.PostConfigure(options => options.SectionPath = sectionPath);
var eager = new SecretsGrpcHubServerOptions { SectionPath = sectionPath };
section.Bind(eager);
eager.SectionPath = sectionPath;
eager.Validate();
// Singleton so the "unconfigured" warning is emitted once per process rather than once per
// interceptor instance; the gRPC interceptor activator prefers a registered instance.
services.TryAddSingleton();
// AddGrpc is safe to call again if the host already called it — it registers with TryAdd.
services.AddGrpc()
.AddServiceOptions(
options => options.Interceptors.Add());
return services;
}
///
/// Maps the secrets hub gRPC endpoint. The host must have set up routing;
/// supplies the gRPC services and the auth interceptor.
///
/// The endpoint route builder.
/// The same instance, for chaining.
public static IEndpointRouteBuilder MapZbSecretsHub(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
endpoints.MapGrpcService();
return endpoints;
}
///
/// Presence of this in the container means the hub is already registered. It carries no state
/// and is never resolved — a dedicated type is used rather than probing for one of the real
/// registrations because every one of those is added with TryAdd-style semantics that a
/// host may legitimately have satisfied for its own reasons.
///
private sealed class HubRegistrationMarker;
}