# revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
# reachability is measured by qualified name, not by bare word:
#   import dev.caskeleton.messaging.security.<Type>;  OR  dev.caskeleton.messaging.security.<Type>

## A. twelve public types, files referencing them outside the owning leaf
DestinationAccessPolicy        7
DestinationAccessValidator     0
MessageSecurityValidator       1
BrokerTlsPolicy                6
BrokerSecurityProfile          8
BrokerCredentialProfile        5
BrokerAclManifest              0
CredentialProvider             4
CredentialRuntime              2
CredentialRuntimeRegistry      6
CredentialRotationPlan         0
CredentialIds                  0

## B. the four with no consumer
DestinationAccessValidator     NONE
BrokerAclManifest              NONE
CredentialRotationPlan         NONE
CredentialIds                  NONE
(CredentialIds is package-private: internal by construction, not a finding.)

## C. duplicate 1 — the access check the publisher performs is not the validator
--- the validator nobody calls
32:  public void requirePublish(DestinationName destination) {
33-    if (!policy.mayPublish(destination)) {
34-      throw new MessageAuthorizationException(
35-          "DESTINATION_PUBLISH_DENIED",
36-          "the producer credential may not publish to " + destination.value());
37-    }
--- what the publish path actually does
170:      if (!access.mayPublish(destination.name())) {
171-        // Before encoding: an unauthorized publish must not serialise the payload, because the
172-        // encoded bytes are what a claim-check or a log would then be holding.
173-        return rejected(
174-            "PUBLISH_FORBIDDEN",
175-            "this application may not publish to '" + destination.name().value() + '\'',
176-            startedAt);

## D. duplicate 2 — the same TLS posture is checked by two classes with different strictness
--- MessageSecurityValidator: hostname verification only in production, IllegalArgumentException
    if (profile.production() && !profile.tlsEnabled()) {
      throw new IllegalArgumentException(
          "a production broker connection requires TLS: " + profile.broker());
    }
    if (profile.production() && !profile.hostnameVerification()) {
      throw new IllegalArgumentException(
          "a production broker connection requires TLS hostname verification: " + profile.broker());
    }

--- BrokerTlsPolicy: hostname verification WHENEVER TLS is on, MessagingConfigurationException
    if (!profile.tlsEnabled()) {
      if (profile.production() || !allowPlaintextOutsideProduction) {
        throw new MessagingConfigurationException(
            "TLS_REQUIRED",
            "broker %s runs in production and must not use a plaintext connection"
                .formatted(profile.broker()));
      }
      return;
    }

    if (!profile.hostnameVerification()) {
      throw new MessagingConfigurationException(
          "HOSTNAME_VERIFICATION_REQUIRED",
          "broker %s enables TLS without hostname verification, which accepts any certificate"
              .formatted(profile.broker()));
    }

--- both are created as beans
156:  public MessageSecurityValidator messageSecurityValidator() {
328:  public BrokerTlsPolicy brokerTlsPolicy() {

## E. duplicate 3 — the rotation predicate exists twice, verbatim
--- CredentialRotationPlan (no consumer)
  public boolean isDue(Instant now) {
    Objects.requireNonNull(now, "now must not be null");
    return expiresAt.map(expiry -> !now.isBefore(expiry.minus(rotateBefore))).orElse(false);
  }

  /**
   * Reports whether the credential has already expired.
   *
   * @param now the current instant
   * @return true once expired
   */
  public boolean isExpired(Instant now) {
    return expiresAt.map(expiry -> !now.isBefore(expiry)).orElse(false);
  }
--- CredentialRuntime (used)
  public boolean isExpired(Instant now) {
    Objects.requireNonNull(now, "now must not be null");
    return expiresAt.map(expiry -> !now.isBefore(expiry)).orElse(false);
  }

  /**
   * Reports whether rotation should begin.
   *
   * @param now the current instant
   * @return true when the credential is inside its rotation lead or already expired
   */
  public boolean isDueForRotation(Instant now) {
    Objects.requireNonNull(now, "now must not be null");
    return expiresAt.map(expiry -> !now.isBefore(expiry.minus(rotationLead))).orElse(false);
  }

## F. who does use this leaf
--- BrokerTlsPolicy
messaging-kafka/main/java/dev/caskeleton/messaging/kafka/KafkaSecurityConfigurer.java
messaging-kafka/test/java/dev/caskeleton/messaging/kafka/KafkaSecurityConfigurerTest.java
messaging-rabbit/main/java/dev/caskeleton/messaging/rabbit/RabbitSecurityConfigurer.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/KafkaMessagingAutoConfiguration.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/MessagingCoreAutoConfiguration.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/RabbitMessagingAutoConfiguration.java
--- CredentialRuntimeRegistry
messaging-kafka/main/java/dev/caskeleton/messaging/kafka/KafkaSecurityConfigurer.java
messaging-kafka/test/java/dev/caskeleton/messaging/kafka/KafkaSecurityConfigurerTest.java
messaging-rabbit/main/java/dev/caskeleton/messaging/rabbit/RabbitSecurityConfigurer.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/KafkaMessagingAutoConfiguration.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/MessagingCoreAutoConfiguration.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/RabbitMessagingAutoConfiguration.java
--- CredentialProvider
messaging-kafka/test/java/dev/caskeleton/messaging/kafka/KafkaSecurityConfigurerTest.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/MessagingCoreAutoConfiguration.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/MessagingCredentialRequirementValidator.java
messaging-spring-boot-starter/test/java/dev/caskeleton/messaging/autoconfigure/MessagingAutoConfigurationTest.java
--- DestinationAccessPolicy
messaging-kafka/test/java/dev/caskeleton/messaging/kafka/KafkaSecurityConfigurerTest.java
messaging-runtime-core/main/java/dev/caskeleton/messaging/runtime/DeclaredDestinationAccess.java
messaging-runtime-core/main/java/dev/caskeleton/messaging/runtime/DefaultMessagePublisher.java
messaging-runtime-core/test/java/dev/caskeleton/messaging/runtime/DefaultMessagePublisherTest.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/MessagingConfigurationCompiler.java
messaging-spring-boot-starter/main/java/dev/caskeleton/messaging/autoconfigure/MessagingCoreAutoConfiguration.java
messaging-spring-boot-starter/test/java/dev/caskeleton/messaging/autoconfigure/MessagingAutoConfigurationTest.java
