119 lines
6.5 KiB
Markdown
119 lines
6.5 KiB
Markdown
# Conditional Inbound Transport Boundary Design
|
|
|
|
**Date:** 2026-08-02
|
|
**Status:** approved by the user's instruction to apply the detailed P1/P2 review sequentially
|
|
**Scope:** the opt-in GraphQL, gRPC, and WebSocket leaf modules and their release evidence
|
|
|
|
## Context
|
|
|
|
The three leaves are registered and tested independently, but neither `app-bootstrap` nor
|
|
`sample-portfolio` has a production dependency on them. That omission is intentional: adding a
|
|
classpath edge today would activate GraphQL, start a plaintext/reflection-enabled gRPC server by
|
|
default, and unconditionally expose a wildcard-origin STOMP broker that serializes arbitrary domain
|
|
events. The leaf documentation nevertheless describes sample contributions that do not exist, and
|
|
the ordinary root `check` can become `NO-SOURCE` without a transport-specific positive-count and
|
|
zero-skip qualification gate.
|
|
|
|
P1 therefore makes opt-in status executable and makes accidental activation fail closed. It does
|
|
not add these leaves to the default runtime or claim the P2 production baselines.
|
|
|
|
## Runtime Membership SSOT
|
|
|
|
Every entry in `config/architecture/modules.json` gains an exact `runtime_memberships` array whose
|
|
values are limited to the two composition roots: `app-bootstrap` and `sample-portfolio`.
|
|
|
|
- A composition root includes itself in its membership.
|
|
- Direct production `api`/`implementation`/`compileOnly`/`runtimeOnly` project dependencies must
|
|
equal the registry members for that root, excluding the root itself.
|
|
- An empty array means the leaf is built and architecture-checked but absent from both shipped
|
|
runtime graphs. GraphQL, gRPC, WebSocket, and Mongo remain in this state.
|
|
- Test fixtures and custom qualification configurations do not change production membership.
|
|
|
|
Settings validation is fail-closed for missing, duplicate, or unknown membership names. A Gradle
|
|
verification task compares the registry to both composition roots and is part of `check`.
|
|
|
|
## Explicit Qualification Composition
|
|
|
|
`app-bootstrap` owns a `conditionalTransportTest` source set whose classpath explicitly includes
|
|
the three opt-in leaves. It proves that the opt-in artifacts resolve together while the registry
|
|
still declares them absent from both default runtime graphs. It is evidence composition, not a new
|
|
production dependency edge.
|
|
|
|
The root registers exact qualification `Test` tasks for GraphQL, gRPC, and WebSocket. Each task:
|
|
|
|
- names required test classes rather than broad discovery;
|
|
- fails on no match or no discovery;
|
|
- always reruns in UTC;
|
|
- fails if the root suite reports any skipped test.
|
|
|
|
An aggregate `conditionalTransportQualification` task depends on the composition contract and all
|
|
three exact lanes. CI invokes it explicitly from the existing release-blocking quality job, and the
|
|
gate matrix records the task.
|
|
|
|
## gRPC P1 Boundary
|
|
|
|
gRPC activation becomes explicit and local-only until a later TLS/mTLS design exists:
|
|
|
|
- `enabled=false` and `reflectionEnabled=false` are defaults; missing properties create no runner,
|
|
health manager, reflection service, or listener.
|
|
- The current insecure credential mode requires an explicit local-development override and a
|
|
loopback bind address. Non-loopback insecure bind fails startup.
|
|
- Feature services require a caller-supplied authentication policy/interceptor. Missing or invalid
|
|
metadata returns stable `UNAUTHENTICATED`; valid metadata reaches the service.
|
|
- Health remains a local lifecycle probe; reflection is a separate explicit flag.
|
|
- The error interceptor wraps `ServerCall.close`, so handler throws, listener throws, ordinary
|
|
`responseObserver.onError`, and raw `StatusRuntimeException` all pass the same sanitizer.
|
|
Recognized `ApiErrorCarrier` causes produce stable code/category trailers; unrecognized status
|
|
descriptions become fixed `INTERNAL_ERROR` with no raw diagnostic.
|
|
|
|
A real ephemeral Netty unary service verifies authentication, reflection-off, all error paths, and
|
|
sentinel redaction. TLS/mTLS, external bind, deadlines, streaming, and protobuf compatibility are
|
|
P2 and remain unclaimed.
|
|
|
|
## GraphQL P1 Boundary
|
|
|
|
GraphQL remains classpath-selected: its absence from the default runtime is the disable mechanism,
|
|
and the qualification classpath is the explicit opt-in mechanism. The wire lane starts a real
|
|
random-port MVC server and crosses HTTP JSON, Spring Security, and CORS.
|
|
|
|
It verifies unauthenticated rejection, authenticated health success, allowed/disallowed origins,
|
|
GraphiQL disabled, production-style introspection disabled, stable carrier errors, unknown errors,
|
|
and absence of distinct secret sentinels from the complete response body. The existing resolver is
|
|
changed only if a failing wire contract proves unsafe behavior.
|
|
|
|
Feature schema/resolvers, field authorization, depth/cost, persisted queries, DataLoader, schema
|
|
compatibility, and subscriptions remain P2.
|
|
|
|
## WebSocket P1 Boundary
|
|
|
|
WebSocket gains `ca-skeleton.websocket.enabled=false`; both configuration and broadcaster are
|
|
conditional. Enabled settings reject wildcard/blank origins and invalid endpoint/destination
|
|
shapes.
|
|
|
|
The inbound channel requires an authenticated handshake principal, permits subscription only to
|
|
the configured server topic, permits authenticated application sends under `/app/**`, and rejects
|
|
client sends to `/topic/**`. A custom STOMP error handler emits only a fixed client-safe code.
|
|
|
|
The broadcaster no longer serializes arbitrary `@DomainEvent` objects. It consults an explicit
|
|
projection allowlist; an event without exactly one projection is not sent. Projection output is a
|
|
bounded primitive map, not the domain object graph.
|
|
|
|
A real random-port WebSocket/STOMP lane verifies disabled absence, origin/auth/connect/subscribe,
|
|
server push, broker-send rejection, error redaction, and no projection/no broadcast. The simple
|
|
broker remains local/R1 only; broker relay, cross-node durability, replay, backpressure, and a
|
|
domain-specific versioned projection catalog remain P2.
|
|
|
|
## Documentation Truthfulness
|
|
|
|
Leaf READMEs and CLAUDE files describe only code that exists. Sample GraphQL schemas, gRPC services,
|
|
and WebSocket publishers are future adoption examples, not current runtime features. Each document
|
|
states the activation switch, exact P1 evidence, and unimplemented P2 limits.
|
|
|
|
## Non-Goals
|
|
|
|
- adding any of the three leaves to a shipped default runtime;
|
|
- adding a production project dependency edge outside the registry;
|
|
- claiming production readiness from local loopback/simple-broker tests;
|
|
- implementing sample feature APIs or domain payloads;
|
|
- staging, committing, amending, or pushing changes.
|