using Grpc.Net.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Replicator.Grpc.V1;
namespace ZB.MOM.WW.Secrets.Replicator.Grpc.DependencyInjection;
///
/// Dependency-injection entry point for a site node that follows the central-hosted secrets hub.
///
///
///
/// The hub and every follower need the same KEK. Only ciphertext crosses the wire, so a node
/// whose master key differs cannot decrypt rows central wrote — it fails closed on resolve with a
/// kek_id mismatch, which reads like data corruption but is a deployment error.
///
///
/// This is the follower half only. Central hosts the hub service and must not register this
/// client; a site node registers this client and must not host the service. Nothing here can push a
/// row hub-ward — the contract has no write RPC — so the direction of replication is a property of
/// the deployment topology rather than a setting that could be got wrong.
///
///
public static class SecretsGrpcHubClientExtensions
{
///
/// Service key under which this package's is registered, so it can
/// never be confused with a channel the application registered for something else.
///
public const string ChannelServiceKey = "zb-secrets-grpc-hub";
///
/// Registers the pull-only hub client and its background sweep: a channel to the configured
/// endpoint, the generated gRPC client, the reader seam over it, and a hosted service that keeps
/// the node's local store converged with central.
///
///
/// The local is not registered or decorated here — call
/// AddZbSecrets as usual and this sits alongside it. That is the whole difference from the
/// SQL-Server hub-replication mode, which wraps the store in a publishing decorator: pull-only
/// means local writes never leave the node, so there is nothing to publish and the store stays
/// exactly as the application configured it. A host that forgets AddZbSecrets fails loudly
/// at startup when the sweep cannot resolve the store, rather than silently replicating nothing.
///
/// The service collection to add to.
/// The application configuration.
/// Configuration section holding the hub-client options.
/// The same instance, for chaining.
///
/// The bound options are unusable — see .
///
public static IServiceCollection AddZbSecretsGrpcHubClient(
this IServiceCollection services,
IConfiguration configuration,
string sectionPath = SecretsGrpcHubClientOptions.DefaultSectionPath)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
ArgumentException.ThrowIfNullOrWhiteSpace(sectionPath);
IConfigurationSection section = configuration.GetSection(sectionPath);
services.Configure(section);
// Bind and validate NOW as well as through the options pipeline: a missing endpoint or an
// unset bearer token should fail at startup with a message naming the key, not surface as a
// sweep that warns once every interval and never converges. The eagerly-bound instance is
// discarded immediately — it is a credential holder, and everything downstream reads the
// options from IOptions rather than closing over it.
var eager = new SecretsGrpcHubClientOptions();
section.Bind(eager);
eager.Validate(sectionPath);
// Singleton, so the container owns its disposal — a channel per sweep would leak an HTTP/2
// connection every interval. KEYED, because GrpcChannel is a framework type an application
// very plausibly registers for one of its own gRPC dependencies: an unkeyed TryAddSingleton
// would silently lose to that registration and this client would then dial someone else's
// endpoint with the hub credential attached.
services.TryAddKeyedSingleton(ChannelServiceKey, (sp, _) => GrpcChannel.ForAddress(
sp.GetRequiredService>().Value.Endpoint));
services.TryAddSingleton(sp => new GrpcSecretsHubClient(
new SecretsHub.SecretsHubClient(
sp.GetRequiredKeyedService(ChannelServiceKey)),
sp.GetRequiredService>(),
sp.GetRequiredService>(),
sp.GetService()));
services.AddHostedService();
return services;
}
}