The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
12 KiB
Plaintext
132 lines
12 KiB
Plaintext
Sub-scope 01 (notification) governance + 루트 + platform/autoconfigure — manifest and probes
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T06:37:36+00:00
|
|
|
|
=== OWNED FILES ===
|
|
660f77ed47c90d8da6c66022c2b5d66e5a706942 CLAUDE.md 47
|
|
4fbe71c26464d9e2a3dfa420876338c9f5cdf89f README.md 78
|
|
517a950162f15d2bf8c4f55bb0323859ab4db99e build.gradle 54
|
|
b30c50f91495749ea97560903b4662d887a5e543 gradle.lockfile 167
|
|
23a2136015f44e177bdab5a477f7f498ec5d8ebb src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java 40
|
|
416c2cb2d052d3a834e03dfdd8fb996224aa9c37 src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationRoutesSettings.java 28
|
|
928df24c90847cd9db99a489e3b7149656ec207a src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/AssembledProvider.java 63
|
|
c114dfcd96177d602589eff07dc731a88eec3fa2 src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationPlatformAutoConfiguration.java 118
|
|
182c790f192069fc98caeb269e460b659ff5aca1 src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationPlatformMode.java 24
|
|
fb23f3b75a076d06de387da7ee9f2c16f977d219 src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationPlatformSettings.java 190
|
|
eba7ccdc3d555b0b7a2ad72c05797ca638d2fd95 src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationProviderAssembly.java 239
|
|
4ce10c47f9c74c5e426d92093b010b69ced464ed src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java 98
|
|
8870a02465769a3a8c4987c8e0e935ed0e614984 src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSmtpProviderConfig.java 211
|
|
c6393dabcd7bd61abadedc6b2c9387d19554ed36 src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSmtpSettings.java 60
|
|
aac01be3780329c32f76e3b857f7e8de3c3b5501 src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/ProviderRuntimeAssembler.java 35
|
|
c75b240b6b23bff058b58df5ddf13fa4fed95d17 src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/ProviderType.java 87
|
|
d73384a60e4797e54127f12e5a018d58bf90350d src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/SmtpProviderRuntimeAssembler.java 164
|
|
6f630e9813d2ed3db38bc429931d1b0d74cb3bf9 src/test/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationPlatformSettingsTest.java 166
|
|
f9fac342d9de203c1777315cc05b1cd55890a6c0 src/test/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationProviderAssemblyTest.java 267
|
|
5f8df14bba9f88525e41d483ce8444ccaf8df4fc src/test/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationReleaseGateTest.java 63
|
|
92052e37ceccc1af9f5f72b3f9d69e94cbff3928 src/test/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirementsTest.java 173
|
|
1831898eba674f57a4be0f318ba667421fbcc4a8 src/test/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/SmtpProviderRuntimeAssemblerTest.java 133
|
|
|
|
=== 8.1 활성화 게이트 전수 ===
|
|
$ grep -rn 'ConditionalOn' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/*.java adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/*.java | sed 's|.*/notification/||'
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:19:import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:20:import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:34:@ConditionalOnProperty(
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:42: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:49: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:56: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:70: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:88: * <p>{@code @ConditionalOnBean(SecretMaterialProvider.class)} used to guard this, to express
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:102: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationPlatformAutoConfiguration.java:114: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:17:import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:18:import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:58:@ConditionalOnProperty(name = "spring.mail.host")
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:64: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:78: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:88: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:102: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:113: @ConditionalOnMissingBean
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:128: @ConditionalOnMissingBean(name = "notificationSmtpExecutor")
|
|
platform/autoconfigure/NotificationSmtpProviderConfig.java:162: @ConditionalOnMissingBean(name = "smtpProviderRuntimeAssembler")
|
|
exit=0
|
|
|
|
$ git ls-files 'adapter/outbound/notification/src/main/resources' 2>/dev/null | head; echo 'main resources: none tracked'
|
|
main resources: none tracked
|
|
exit=0
|
|
|
|
$ git grep -rn 'NotificationPlatformAutoConfiguration\|NotificationConfig' -- 'app-bootstrap/src/main' | head
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationRootAutoConfiguration.java:3:import dev.caskeleton.adapter.outbound.notification.NotificationConfig;
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationRootAutoConfiguration.java:6:import dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformAutoConfiguration;
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationRootAutoConfiguration.java:27: * <p>{@link NotificationPlatformAutoConfiguration} is named here for a blunter reason: nothing
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationRootAutoConfiguration.java:47: NotificationPlatformAutoConfiguration.class,
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationRootAutoConfiguration.java:51: NotificationConfig.class,
|
|
app-bootstrap/src/main/resources/application.yml:959: # Add a provider = new files only (NotificationConfig); select it here.
|
|
exit=0
|
|
|
|
=== 8.2 INGEST_ONLY 가 실제로 readiness 를 내리는가 ===
|
|
$ git grep -rn 'INGEST_ONLY' -- 'adapter/outbound/notification/src/main' 'app-bootstrap/src/main' | head -20
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationPlatformMode.java:23: INGEST_ONLY
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationProviderAssembly.java:35: * <li>zero providers without {@link NotificationPlatformMode#INGEST_ONLY}, because a platform
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationProviderAssembly.java:141: routes.isEmpty() ? NotificationPlatformMode.INGEST_ONLY : NotificationPlatformMode.SERVING);
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationProviderAssembly.java:183: if (routes.isEmpty() && mode != NotificationPlatformMode.INGEST_ONLY) {
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationProviderAssembly.java:187: + " declare ca-skeleton.notification.platform.mode=INGEST_ONLY so readiness reports"
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java:83: * The purposes every mode needs, including {@code INGEST_ONLY}.
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:128: * Everything that starts a thread, and therefore everything {@code INGEST_ONLY} must not have.
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java:132: * method — so a deployment in {@code INGEST_ONLY}, the mode whose whole purpose is to accept and
|
|
app-bootstrap/src/main/resources/application.yml:487: # SERVING | INGEST_ONLY. SERVING refuses to start without a working provider; INGEST_ONLY
|
|
exit=0
|
|
|
|
=== 8.3 실행 probe: dependencyPolicy 와 API 표면 ===
|
|
$ ./gradlew :adapter:outbound:notification:verifyDependencyPolicy → BUILD SUCCESSFUL (EXIT=0)
|
|
$ ./gradlew verifyNotificationApiSurface → OK — 586 public types, unchanged (EXIT=0)
|
|
$ wc -l ../docs/notification/api-surface-snapshot.txt
|
|
589 ../docs/notification/api-surface-snapshot.txt
|
|
exit=0
|
|
|
|
=== 8.4 비밀 요구가 능력에 따라 좁아지는가 ===
|
|
$ grep -n 'ALWAYS\|required.add' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java
|
|
36: Set<SecretPurpose> required = EnumSet.copyOf(ALWAYS);
|
|
42: required.add(SecretPurpose.CALLBACK_SIGNING);
|
|
43: required.add(SecretPurpose.CALLBACK_FINGERPRINT_HMAC);
|
|
54: required.add(SecretPurpose.PROVIDER_CREDENTIAL);
|
|
57: required.add(SecretPurpose.VAPID_SIGNING);
|
|
63: required.add(SecretPurpose.CALLBACK_SIGNING);
|
|
92: private static final Set<SecretPurpose> ALWAYS =
|
|
exit=0
|
|
|
|
$ git grep -h ' void [a-z]' -- 'adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure' | sed 's/^ *//'
|
|
void productionWebPushWithoutVapidKeyFailsStartup() {
|
|
void apnsWithoutTopicFailsStartup() {
|
|
void aCallbackCapableProviderRequiresASigningSecret() {
|
|
void aDisabledProfileIsNotValidated() {
|
|
void ambiguousFallbackCannotBeEnabled() {
|
|
void anUnboundedClaimBatchIsRefused() {
|
|
void aLeaseShorterThanThePollIntervalIsRefused() {
|
|
void defaultsAreUsableAndConservative() {
|
|
void aConfiguredProfileBecomesARuntimeAndARoute() {
|
|
void anUnknownProviderTypeFailsTheBoot() {
|
|
void aFamilyWithNoAssemblerIsRefused() {
|
|
void twoProfilesOnOneChannelNeedAPrimary() {
|
|
void aMarkedPrimaryResolvesTheAmbiguity() {
|
|
void twoPrimariesAreRefused() {
|
|
void aDisabledProfileAssemblesNothing() {
|
|
void anEnabledPlatformWithNoProviderRefusesToStart() {
|
|
void ingestOnlyIsADeclaration() {
|
|
void twoAssemblersForOneFamilyAreRefused() {
|
|
void requiredDocumentationAndAdrsExist() {
|
|
void supportMatrixDoesNotClaimGuaranteedDelivery() throws IOException {
|
|
void theEvidenceDocumentStatesTheForbiddenPromotions() throws IOException {
|
|
void theAcceptPathKeysAreAlwaysRequired() {
|
|
void anSmtpOnlyPlatformIsNotAskedForAProviderCredential() {
|
|
void aDisabledProfileDoesNotDemandItsKeys() {
|
|
void anEnabledWebPushProfileDemandsItsKeys() {
|
|
void callbacksOffDoNotDemandTheCallbackKeys() {
|
|
void callbacksOnDemandBothCallbackKeys() {
|
|
void aProfileNamingASigningRefDemandsTheSigningPurpose() {
|
|
void theAssemblerServesSmtpOnEmail() {
|
|
void aConfiguredProfileAssemblesIntoARuntime() {
|
|
void smtpClaimsNothingItCannotDo() {
|
|
void thePerProfileBoundsComeFromTheProfile() {
|
|
exit=0
|
|
|