namespace ZB.MOM.WW.Secrets.Replicator.Grpc;
///
/// Configuration for the central-hosted secrets hub. Bound from a configuration section by
/// AddZbSecretsGrpcHub.
///
///
///
/// The hub serves ciphertext to followers that authenticate with one shared bearer token. That
/// token is the whole of the hub's authorization: there is no per-follower identity, because a
/// follower's authority is uniform (read the manifest, read rows) and the pull-only contract gives
/// it nothing else to be authorized for.
///
///
/// It cannot itself be supplied as a ${secret:} reference — resolving that reference is what
/// the hub exists to make possible — so it comes from appsettings or the environment, the same way
/// the family's mesh pre-shared keys do.
///
///
public sealed class SecretsGrpcHubServerOptions
{
/// The configuration section the hub is bound from unless a host overrides it.
internal const string DefaultSectionPath = "Secrets:GrpcHub";
///
/// The default ceiling on names in one GetSecrets call. Set comfortably above the
/// follower's own fetch chunk so a compliant sweep never approaches it.
///
internal const int DefaultMaxNamesPerRequest = 1000;
///
/// The shared bearer token a follower must present. Required — an unset token is not "open
/// access", it is a hub that refuses every call (see ).
///
public string BearerToken { get; set; } = string.Empty;
///
/// The most names a single GetSecrets call may carry. A larger request is refused with
/// ResourceExhausted rather than served.
///
///
/// The well-behaved follower already chunks its fetches well below this, so the cap is not a
/// throughput knob — it is the server declining to let one authenticated caller turn a single
/// request into an unbounded sequential store sweep. The cap sits behind authentication, so it
/// is defence in depth against a misbehaving token holder, not an anonymous-DoS control.
///
public int MaxNamesPerRequest { get; set; } = DefaultMaxNamesPerRequest;
///
/// The configuration section this instance was bound from, used so failure messages name the
/// key an operator actually has to set.
///
///
/// Deliberately : configuration binding only writes public settable
/// properties, so a stray SectionPath key in an appsettings file cannot make the hub
/// misreport which key it wants. The registration extension sets it.
///
internal string SectionPath { get; set; } = DefaultSectionPath;
///
/// Throws if the options are unusable. Called by the registration extension so a hub with no
/// token fails at startup, rather than starting and rejecting every follower at runtime.
///
///
/// The bearer token is missing or blank, or the request cap is not positive.
///
public void Validate()
{
if (string.IsNullOrWhiteSpace(BearerToken))
{
// Names the key, never the value — there is no value here, and there never will be one
// in an error message from this type.
throw new InvalidOperationException(
$"The gRPC secrets hub requires a shared bearer token ({SectionPath}:BearerToken). " +
"Until it is set the hub refuses every call.");
}
if (MaxNamesPerRequest <= 0)
{
// A zero or negative cap would refuse every GetSecrets call, which reads at runtime like
// a broken follower rather than a typo in configuration — so it is refused at startup.
throw new InvalidOperationException(
$"The gRPC secrets hub request cap ({SectionPath}:MaxNamesPerRequest) must be " +
$"greater than zero; it is {MaxNamesPerRequest}.");
}
}
}