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>
466 lines
19 KiB
Plaintext
466 lines
19 KiB
Plaintext
adapter-inbound-web module inventory / denominator
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T07:06:20+00:00
|
|
|
|
=== TRACKED FILE COUNTS ===
|
|
$ git ls-files adapter/inbound/web | wc -l
|
|
638
|
|
exit=0
|
|
|
|
$ git ls-files adapter/inbound/web/src/main | wc -l
|
|
400
|
|
exit=0
|
|
|
|
$ git ls-files adapter/inbound/web/src/test | wc -l
|
|
150
|
|
exit=0
|
|
|
|
$ git ls-files adapter/inbound/web | grep -v '^adapter/inbound/web/src/'
|
|
adapter/inbound/web/CLAUDE.md
|
|
adapter/inbound/web/README.md
|
|
adapter/inbound/web/build.gradle
|
|
adapter/inbound/web/gradle.lockfile
|
|
exit=0
|
|
|
|
=== MAIN JAVA LOC ===
|
|
$ git ls-files 'adapter/inbound/web/src/main/**/*.java' | xargs wc -l | tail -1
|
|
27473 total
|
|
exit=0
|
|
|
|
$ git ls-files 'adapter/inbound/web/src/test/**/*.java' | xargs wc -l | tail -1
|
|
18319 total
|
|
exit=0
|
|
|
|
$ git ls-files adapter/inbound/web/src/main | grep -v '\.java$'
|
|
adapter/inbound/web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
|
|
adapter/inbound/web/src/main/resources/META-INF/web/problem-catalog.yaml
|
|
adapter/inbound/web/src/main/resources/META-INF/web/wire-type-manifest.yaml
|
|
exit=0
|
|
|
|
$ git ls-files adapter/inbound/web/src/test | grep -v '\.java$'
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/contract/.gitkeep
|
|
adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/mvc/.gitkeep
|
|
adapter/inbound/web/src/test/resources/application-web-contract.yaml
|
|
exit=0
|
|
|
|
=== MAIN PACKAGE TREE (file counts) ===
|
|
$ git ls-files 'adapter/inbound/web/src/main/**/*.java' | sed 's|.*/web/||;s|/[^/]*$||' | sort | uniq -c | sort -rn
|
|
19 pagination
|
|
18 ratelimit
|
|
16 error
|
|
13 advanced/stream
|
|
13 advanced/patch
|
|
12 observability
|
|
12 http
|
|
11 security
|
|
11 operation
|
|
11 auth
|
|
10 fileserver/reactive
|
|
10 conditional
|
|
9 operationasync
|
|
9 idempotency
|
|
9 core
|
|
7 versioning
|
|
7 openapi
|
|
7 notification/platform/submission
|
|
7 budget
|
|
7 advanced/webflux
|
|
7 advanced/stream/encoding
|
|
6 evidence
|
|
6 advanced/codec
|
|
5 notification/platform/callback
|
|
5 fileserver/tus
|
|
5 fileserver/problem
|
|
5 fileserver/dto
|
|
5 fileserver/draft12
|
|
5 authz
|
|
5 advanced/stream/replay
|
|
5 advanced/openapi
|
|
5 advanced/mvc
|
|
5 admission
|
|
4 validation
|
|
4 proxy
|
|
4 notification/platform/callback/reactive
|
|
4 mvc/budget
|
|
4 json
|
|
4 fileserver/nginx
|
|
4 fileserver/config
|
|
4 contract
|
|
4 cache
|
|
4 advanced/functional
|
|
4 admin/route
|
|
3 settings
|
|
3 moduleboundary
|
|
3 fileserver/mapper
|
|
3 fileserver/http
|
|
3 fileserver/admin
|
|
3 config
|
|
3 advanced/virtualthread
|
|
3 advanced/ratelimit
|
|
3 advanced/blockingbridge
|
|
2 webflux/guard
|
|
2 webflux/context
|
|
2 webflux/budget
|
|
2 webflux/autoconfigure
|
|
2 mvc/idempotency
|
|
2 mvc/filter
|
|
2 mvc/context
|
|
2 mvc/autoconfigure
|
|
2 filter
|
|
2 fileserver/controller
|
|
2 cursor
|
|
2 advanced/release
|
|
2 advanced
|
|
2 admin/platform
|
|
1 webflux/throttle
|
|
1 webflux/operation
|
|
1 webflux/idempotency
|
|
1 webflux/error
|
|
1 mvc/throttle
|
|
1 mvc/operation
|
|
1 mvc/error
|
|
1 fileserver/security
|
|
1 fileserver/lifecycle
|
|
1 envelope
|
|
1 controller
|
|
exit=0
|
|
|
|
=== TEST PACKAGE TREE ===
|
|
$ git ls-files 'adapter/inbound/web/src/test/**/*.java' | sed 's|.*/web/||;s|/[^/]*$||' | sort | uniq -c | sort -rn
|
|
11 auth
|
|
8 fileserver/testkit
|
|
7 ratelimit
|
|
7 error
|
|
5 pagination
|
|
5 observability
|
|
4 idempotency
|
|
4 authz
|
|
3 settings
|
|
3 moduleboundary
|
|
3 http
|
|
3 fileserver/reactive
|
|
3 conditional
|
|
3 advanced/release
|
|
3 advanced/codec
|
|
2 versioning
|
|
2 testkit/contract
|
|
2 security
|
|
2 operationasync
|
|
2 openapi
|
|
2 notification/platform/callback
|
|
2 mvc/filter
|
|
2 fixtures/badcontroller
|
|
2 filter
|
|
2 fileserver/nginx
|
|
2 fileserver/controller
|
|
2 envelope
|
|
2 core
|
|
2 advanced/stream/replay
|
|
2 advanced/stream
|
|
2 advanced/patch
|
|
2 advanced/openapi
|
|
2 advanced/mvc
|
|
1 webflux/idempotency
|
|
1 webflux/guard
|
|
1 webflux/context
|
|
1 webflux/autoconfigure
|
|
1 validation
|
|
1 testkit/throttle
|
|
1 testkit/performance
|
|
1 testkit/order
|
|
1 testkit/operation
|
|
1 testkit/mvc
|
|
1 testkit/fault
|
|
1 testkit/budget
|
|
1 testkit/arch
|
|
1 proxy
|
|
1 operation
|
|
1 mvc/idempotency
|
|
1 mvc/autoconfigure
|
|
1 json
|
|
1 fileserver/tus
|
|
1 fileserver/security
|
|
1 fileserver/problem
|
|
1 fileserver/http
|
|
1 fileserver/draft12
|
|
1 fileserver/config
|
|
1 fileserver/admin
|
|
1 evidence
|
|
1 cursor
|
|
1 contract
|
|
1 config
|
|
1 cache
|
|
1 budget
|
|
1 advanced/webflux
|
|
1 advanced/virtualthread
|
|
1 advanced/stream/encoding
|
|
1 advanced/ratelimit
|
|
1 advanced/functional
|
|
1 advanced/blockingbridge
|
|
1 admin/route
|
|
1 admin/platform
|
|
exit=0
|
|
|
|
=== BUILD ===
|
|
$ cat adapter/inbound/web/build.gradle
|
|
// The inbound HTTP API execution platform design models itself as 23 Stable Gradle modules under
|
|
// modules/web. This repository's fail-closed module registry outranks that layout, so those modules
|
|
// are packages here and WebModuleBoundaryTest enforces the design's module dependency table. The
|
|
// full mapping, and the three other places the design's assumptions were adapted, are in
|
|
// docs/web/repository-adaptation.md.
|
|
|
|
// HTTP / web adapters. Depends on application and shared operational contracts.
|
|
dependencies {
|
|
implementation project(':application-core')
|
|
implementation project(':shared-contract')
|
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
|
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
|
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
|
implementation 'org.springframework.session:spring-session-core'
|
|
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
|
implementation('org.openapitools:jackson-databind-nullable:0.2.6') {
|
|
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind'
|
|
}
|
|
// feature-api-contract-baseline D10: OpenAPI producer. springdoc exposes the
|
|
// running app's machine-readable contract at /v3/api-docs (OAS 3.1, generated —
|
|
// never a hand-maintained stale schema). The release-blocking drift gate is
|
|
// owned by feature-contract-verification-test-suite (planned).
|
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0'
|
|
// The two Advanced representations, compile-only on purpose. They were `implementation` first,
|
|
// on the reasoning that a missing backend would surface as a NoClassDefFoundError at the first
|
|
// request that negotiated one. That reasoning was wrong about what the jars do: Spring Boot's
|
|
// Jackson auto-configuration registers an `xmlMapper` and a `cborMapper` the moment each
|
|
// backend is on the runtime classpath, and Spring registers a message converter for XML with
|
|
// it. So every deployment got three ObjectMapper beans — which broke `@Autowired ObjectMapper`
|
|
// in the composition root with an ambiguity — and, worse, silently began parsing
|
|
// `application/xml` request bodies. An Advanced capability that is off by default had turned
|
|
// XML deserialization on for everybody, which is the opposite of what the flag promises and an
|
|
// XXE surface nobody chose.
|
|
//
|
|
// Compile-only keeps the factories compiling and their tests running while leaving the runtime
|
|
// classpath to the deployment that enables the capability. `WebRepresentation.available()` is
|
|
// what turns the absent backend into a sentence instead of a NoClassDefFoundError.
|
|
compileOnly 'tools.jackson.dataformat:jackson-dataformat-cbor'
|
|
compileOnly 'tools.jackson.dataformat:jackson-dataformat-xml'
|
|
testImplementation 'tools.jackson.dataformat:jackson-dataformat-cbor'
|
|
testImplementation 'tools.jackson.dataformat:jackson-dataformat-xml'
|
|
// Fileserver reactive transport. Only the WebFlux framework and Reactor core are declared —
|
|
// deliberately not spring-boot-starter-webflux, which would put a second embedded server
|
|
// (reactor-netty) on the runtime classpath. DispatcherServlet stays present, so Spring Boot's
|
|
// WebApplicationType deduction keeps resolving SERVLET; the reactive handlers are wired only
|
|
// when the fileserver reactive profile is selected.
|
|
implementation 'org.springframework:spring-webflux'
|
|
implementation 'io.projectreactor:reactor-core'
|
|
testImplementation 'org.springframework.security:spring-security-test'
|
|
testImplementation 'io.projectreactor:reactor-test'
|
|
}
|
|
|
|
tasks.named('test') {
|
|
useJUnitPlatform {
|
|
excludeTags 'security-boundary'
|
|
// The parity gate compares recordings written by three lanes. In `test` alone only one of
|
|
// them exists, and a gate that fails because the others have not run yet is a gate people
|
|
// learn to ignore. It runs from `webCrossStackParityTest`, which depends on all three.
|
|
excludeTags 'web-parity'
|
|
}
|
|
}
|
|
|
|
// The web platform's reusable ArchUnit rules ship in their own source set, consumed by this leaf's
|
|
// tests and by the composition root. A rule pack that only its own fixture tests import is verified
|
|
// as library code and applied to nothing — the shape the JPA testkit had to be corrected out of.
|
|
strictTestLanes {
|
|
sourceSet('testkit') { compilesAgainst 'main' }
|
|
// The Jetty compatibility lane is its own source set because it needs a different embedded
|
|
// server on the classpath. Two servers in one source set means Spring Boot picks one and the
|
|
// "Jetty" lane silently runs on Tomcat — a compatibility matrix that certifies the same
|
|
// container twice.
|
|
sourceSet('jettyCompatTest') {
|
|
compilesAgainst 'main', 'testkit'
|
|
inherits 'implementation'
|
|
}
|
|
// Reactor Netty is the Stable WebFlux server baseline and it cannot share a source set with
|
|
// Tomcat: Spring Boot deduces one application type from the classpath, so with both servers
|
|
// present the reactive gate would start a servlet container and certify nothing reactive.
|
|
// Nothing is inherited: the leaf's own `implementation` carries spring-boot-starter-web, and
|
|
// inheriting it would put Tomcat back on this lane's classpath. Boot then deduces a servlet
|
|
// application, starts a servlet container, and the reactive gate certifies the servlet stack.
|
|
// The Nginx lane runs the platform behind a real reverse proxy in a container. Its own source
|
|
// set because it is the only lane that needs Docker: folding it into `test` would make every
|
|
// developer's `check` depend on a container runtime, and the usual outcome of that is an
|
|
// @Disabled that nobody notices has been there for months.
|
|
sourceSet('nginxProxyTest') {
|
|
compilesAgainst 'main', 'testkit'
|
|
inherits 'implementation'
|
|
}
|
|
sourceSet('webfluxContractTest') {
|
|
compilesAgainst 'main', 'testkit'
|
|
// Inherits nothing. The default is to extend `testImplementation`, which extends the leaf's
|
|
// own `implementation` and therefore carries spring-boot-starter-web — and with Tomcat on
|
|
// the classpath Boot deduces a servlet application, starts a servlet container, and the
|
|
// reactive gate certifies the servlet stack while reporting itself green.
|
|
inherits()
|
|
}
|
|
}
|
|
|
|
testkitPublisher {
|
|
consumedBy 'test'
|
|
publishAs 'webTestkit'
|
|
}
|
|
|
|
// ArchUnit is declared after the testkit source set exists, because `testkitImplementation` is
|
|
// created by that declaration.
|
|
dependencies {
|
|
testkitImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
|
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
|
|
|
// Jetty replaces Tomcat for this lane only. The exclusion is what makes the lane mean
|
|
// something: with both on the classpath Boot starts Tomcat and the lane certifies nothing.
|
|
jettyCompatTestImplementation('org.springframework.boot:spring-boot-starter-jetty')
|
|
jettyCompatTestImplementation('org.springframework.boot:spring-boot-starter-test') {
|
|
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
|
}
|
|
jettyCompatTestImplementation('org.springframework.boot:spring-boot-starter-web') {
|
|
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
|
}
|
|
jettyCompatTestImplementation 'org.springframework.boot:spring-boot-starter-security'
|
|
jettyCompatTestImplementation 'org.springframework.boot:spring-boot-starter-validation'
|
|
// The root build adds the launcher to `test` only; a custom lane has to say so itself, or the
|
|
// executor starts and finds no JUnit Platform.
|
|
jettyCompatTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
|
|
|
// The reactive lane: reactor-netty in, Tomcat out. The exclusion is what makes the lane mean
|
|
// something — Boot picks the servlet stack when both are present.
|
|
webfluxContractTestImplementation('org.springframework.boot:spring-boot-starter-webflux') {
|
|
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
|
}
|
|
webfluxContractTestImplementation('org.springframework.boot:spring-boot-starter-test') {
|
|
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
|
}
|
|
webfluxContractTestImplementation 'org.springframework.boot:spring-boot-starter-validation'
|
|
webfluxContractTestImplementation 'io.projectreactor:reactor-test'
|
|
// Explicit because this lane inherits nothing: the platform's auto-configuration references
|
|
// ObjectMapper, and without Jackson the condition evaluation fails before a server starts.
|
|
webfluxContractTestImplementation 'org.springframework.boot:spring-boot-starter-jackson'
|
|
webfluxContractTestImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
|
webfluxContractTestImplementation project(':application-core')
|
|
webfluxContractTestImplementation project(':shared-contract')
|
|
webfluxContractTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
|
|
|
// The Nginx lane. Testcontainers starts the proxy; the application runs in this JVM on a
|
|
// random port and the container reaches it through the host gateway.
|
|
nginxProxyTestImplementation 'org.springframework.boot:spring-boot-starter-test'
|
|
nginxProxyTestImplementation 'org.springframework.boot:spring-boot-starter-web'
|
|
nginxProxyTestImplementation 'org.springframework.boot:spring-boot-starter-security'
|
|
nginxProxyTestImplementation 'org.springframework.boot:spring-boot-starter-validation'
|
|
nginxProxyTestImplementation 'org.testcontainers:testcontainers'
|
|
nginxProxyTestImplementation 'org.testcontainers:testcontainers-junit-jupiter'
|
|
nginxProxyTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
|
}
|
|
|
|
// The lane task. A release compatibility gate that is not wired to a task is a document.
|
|
tasks.register('webFluxContractTest', Test) {
|
|
group = 'verification'
|
|
description = 'Runs the Stable HTTP contract against a real Reactor Netty.'
|
|
testClassesDirs = sourceSets.webfluxContractTest.output.classesDirs
|
|
classpath = sourceSets.webfluxContractTest.runtimeClasspath
|
|
useJUnitPlatform()
|
|
failOnNoDiscoveredTests = true
|
|
outputs.upToDateWhen { false }
|
|
jvmArgs '-Duser.timezone=UTC'
|
|
}
|
|
|
|
// Docker-gated, and it says so rather than skipping. A lane that quietly passes when the container
|
|
// runtime is missing is a lane that has been certifying nothing since whenever Docker last broke.
|
|
tasks.register('webNginxProxyTest', Test) {
|
|
group = 'verification'
|
|
description = 'Runs the proxy, prefix and spoofing contract behind a real Nginx.'
|
|
testClassesDirs = sourceSets.nginxProxyTest.output.classesDirs
|
|
classpath = sourceSets.nginxProxyTest.runtimeClasspath
|
|
useJUnitPlatform()
|
|
failOnNoDiscoveredTests = true
|
|
outputs.upToDateWhen { false }
|
|
jvmArgs '-Duser.timezone=UTC'
|
|
}
|
|
|
|
// The cross-stack gate. It depends on every recording lane rather than tolerating a missing one:
|
|
// a parity check that compares whatever happens to be present would report agreement across a
|
|
// matrix with a hole in it.
|
|
tasks.register('webCrossStackParityTest', Test) {
|
|
group = 'verification'
|
|
description = 'Compares the wire contract recorded by Tomcat, Jetty and Reactor Netty.'
|
|
testClassesDirs = sourceSets.test.output.classesDirs
|
|
classpath = sourceSets.test.runtimeClasspath
|
|
useJUnitPlatform {
|
|
includeTags 'web-parity'
|
|
}
|
|
failOnNoDiscoveredTests = true
|
|
outputs.upToDateWhen { false }
|
|
jvmArgs '-Duser.timezone=UTC'
|
|
dependsOn 'test', 'webJettyCompatTest', 'webFluxContractTest'
|
|
}
|
|
|
|
// The Advanced lane. Every capability is off unless a deployment names it, so none of them is
|
|
// exercised by anything a production deployment runs — which makes a lane that runs them all the
|
|
// only place a break is noticed before whoever enables it notices.
|
|
//
|
|
// They also run inside `test`, deliberately. They are ordinary unit tests, and excluding them from
|
|
// the PR gate to make this lane look meaningful would mean the PR gate stopped covering a fifth of
|
|
// the leaf.
|
|
tasks.register('webAdvancedTest', Test) {
|
|
group = 'verification'
|
|
description = 'Runs every web Advanced capability contract.'
|
|
testClassesDirs = sourceSets.test.output.classesDirs
|
|
classpath = sourceSets.test.runtimeClasspath
|
|
useJUnitPlatform {
|
|
includeTags 'web-advanced'
|
|
}
|
|
failOnNoDiscoveredTests = true
|
|
outputs.upToDateWhen { false }
|
|
jvmArgs '-Duser.timezone=UTC'
|
|
}
|
|
|
|
tasks.register('webJettyCompatTest', Test) {
|
|
group = 'verification'
|
|
description = 'Runs the Stable HTTP contract against a real Jetty instead of Tomcat.'
|
|
testClassesDirs = sourceSets.jettyCompatTest.output.classesDirs
|
|
classpath = sourceSets.jettyCompatTest.runtimeClasspath
|
|
useJUnitPlatform()
|
|
failOnNoDiscoveredTests = true
|
|
outputs.upToDateWhen { false }
|
|
jvmArgs '-Duser.timezone=UTC'
|
|
}
|
|
|
|
strictTestLanes {
|
|
lane('jpaPersistenceRedactionContractTest') {
|
|
description = 'Runs the exact persistence error log/trace redaction contract used by JPA evidence.'
|
|
// Named rather than tagged. This lane is JPA evidence's proof that a database failure never
|
|
// reaches a log line or a span, and it must stay exactly these two contracts — a tag would
|
|
// let a later test opt itself in and change what the evidence covers.
|
|
requires(
|
|
'dev.caskeleton.adapter.inbound.web.error.GlobalExceptionHandlerTest.persistenceFailureObservabilityDoesNotCarryRawDatabaseDetails',
|
|
'dev.caskeleton.adapter.inbound.web.error.SpanErrorRecorderHookTest.persistenceFailureHandlerRecordsSanitizedExceptionWithClassifiedCode')
|
|
}
|
|
|
|
lane('webSecurityBoundaryTest') {
|
|
tag = 'security-boundary'
|
|
description = 'Runs hermetic JWT/JWKS and CORS filter-boundary contracts with no skips.'
|
|
customize = { test ->
|
|
test.shouldRunAfter test.project.tasks.named('test')
|
|
test.jvmArgs '-Duser.timezone=UTC'
|
|
test.afterSuite { descriptor, result ->
|
|
if (descriptor.parent == null && result.skippedTestCount > 0) {
|
|
throw new GradleException(
|
|
"webSecurityBoundaryTest forbids skipped tests: ${result.skippedTestCount}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tasks.named('check') {
|
|
dependsOn tasks.named('webSecurityBoundaryTest')
|
|
}
|
|
exit=0
|
|
|