Commit Graph
134 Commits
Author SHA1 Message Date
Relism 8353033cb1 Merge pull request 'feat: Vite extension and Maven plugin; data pools close on stop' (#19) from feature/ext-vite/replace-web-bundler into master
Publish Maven packages / publish (push) Successful in 2m49s
2026-09-22 16:27:46 +00:00
Zakaria El Orche fd96d67fca Merge branch 'feature/ext-data/close-pool-on-stop' into feature/ext-vite/replace-web-bundler 2026-09-22 16:25:16 +00:00
Zakaria El OrcheandClaude Opus 5 003fd6d1f0 feat(ext-vite): recognise navigations by Sec-Fetch-Mode, read every header in place
A path that is no file falls back to index.html when the request is a navigation:
Sec-Fetch-Mode: navigate, or an Accept naming text/html for older clients. That check,
Accept-Encoding and If-None-Match are all matched on the header bytes through the new
Request.headerView(name), so serving still allocates nothing. navigationOnly(false)
drops the check for an app that wants every GET miss to get the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:16:27 +00:00
Zakaria El OrcheandClaude Opus 5 680bbca8c7 feat(ext-vite): gzip at build time, fall back to the index only for navigations
The Maven plugin now writes a maximally compressed .gz beside every text file of 1 KB
and more, so the server compresses nothing and boot only reads the files: about 50 ms
for Glossa's 500. Hashed files under assets/ skip the ETag, which nothing ever asks for.

A path that is no file gets index.html only when the request's Accept names text/html,
as a browser navigation does. Everything else, an API call to a missing route included,
gets the app's own 404 instead of the index, and a dot in a client route no longer
matters. The base path itself always serves the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:54:19 +00:00
Zakaria El OrcheandClaude Opus 5 580417e952 feat(ext-vite): replace the web bundler with a Vite extension and its Maven plugin
flash-ext-vite runs Vite's dev server in DEV and otherwise serves the build from the
classpath, read straight from the directory or jar with no manifest. The Maven plugin
flash-ext-vite-maven-plugin builds the frontend at prepare-package and packages it
there, so mvn package makes a jar that serves its own frontend and mvn test needs no
Node. Three overrides remain (root, devPort, basePath); the package manager is read
from the nearest lockfile.

Serving fixes what the bundler got wrong: Vite's hashed files under assets/ are
cached as immutable instead of revalidated, HEAD reports the real Content-Length, a
missing asset is a 404 instead of the index, 304s carry ETag and Cache-Control, and
gzip respects q=0 and is prepared at boot. Every response header is pre-encoded, so
serving allocates nothing, which is what Response.type(byte[]) is for. Vite stops
with the app through onClose, and a lockfile change reinstalls before restarting.

The modes, strategies, logging and command-safety options, the asset-source
abstraction, the manifest and the Jackson dependency are gone: 1,535 lines of main
code become 480, plus 84 for the plugin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:47:28 +00:00
Zakaria El OrcheandClaude Opus 5 d6c018242f feat(ext-data): close the connection pool when the app stops
TxManager is AutoCloseable and releases what it was built on: JdbcTxManager its data
source when closeable, HibernateTxManager its session factory and then the data source
Hibernate was handed, which Hibernate itself never closes. DataExtension registers the
close as an onClose callback, so a stopped app no longer leaves its pool connected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:02:35 +00:00
Relism 6d44f9e7b1 Merge pull request 'feat(ext-security): add an OAuth 2.1 authorization server' (#18) from feature/ext-security/oauth-server into master
Publish Maven packages / publish (push) Successful in 2m18s
2026-09-22 11:42:26 +00:00
Zakaria El OrcheandClaude Opus 5 f28fc43150 feat(ext-security): add an OAuth 2.1 authorization server
flash-ext-security-oauth-server issues RFC 9068 access tokens (code + PKCE S256,
CIMD and DCR clients, RFC 8707 resources, rotating refresh tokens) for resources on
the application's own origin. Around it: SecurityExtension resolves a configured
origin instead of X-Forwarded-* headers, mechanisms expose schemes() and a route can
be restricted to some of them, McpConfig.mechanisms(...) uses that, OIDC bearers must
be typed at+jwt, and PublicUrl guards outbound fetches against internal addresses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:39:09 +00:00
Relism 7f225e0faf Merge pull request 'refactor(ext-oidc): replace auth modules with security extensions' (#17) from feature/ext-auth/split-oidc-into-auth-core into master
Publish Maven packages / publish (push) Successful in 2m20s
Reviewed-on: #17
2026-09-16 16:00:15 +00:00
Zakaria El Orche 86fafdb3d8 Merge remote-tracking branch 'origin/master' into feature/ext-auth/split-oidc-into-auth-core
# Conflicts:
#	AGENTS.md
#	flash-extensions/pom.xml
#	pom.xml
2026-09-16 15:54:54 +00:00
Zakaria El Orche e0795299fc refactor(ext-oidc): replace auth modules with security extensions 2026-09-16 15:54:19 +00:00
Zakaria El Orche 017c2443f4 feat(testing): support reusable request customizers 2026-09-16 15:54:10 +00:00
Zakaria El Orche b30a4af1d6 feat(core): add request helpers for security flows 2026-09-16 15:54:10 +00:00
Zakaria El Orche 096098b33c fix(ext-openapi): allow contributors to enrich default responses 2026-09-16 15:54:10 +00:00
Zakaria El Orche 829b9bf348 feat(ext-mcp): let applications put middleware on the MCP route, and document the auth split
McpExtension built its middleware chain entirely internally, so a consumer had no
way to add rate limiting, audit logging or tracing to /mcp — routine on every other
Flash route. McpConfig.middleware(...) appends to the chain after the transport
guards and after whatever McpSecurity resolved to, so it composes with OAuth2
protection instead of replacing it, and never satisfies REQUIRED.

Docs: flash-ext-auth-core and flash-ext-auth-oidc both get a docs/ directory —
oidc had none at all, and its module README documented types that no longer exist.
Includes a migration table from flash-ext-oidc.
2026-09-10 19:12:44 +00:00
Zakaria El Orche 9d39e24ccb refactor(ext-auth): generic sessions, shared annotation wiring, rename to flash-ext-auth-oidc
AuthMiddleware.install(ctx, config, source) now owns the annotation processor and
the flash.auth.policy key, so a second credential source gets annotation-driven
authorization without copying the wiring. The key is public: an extension that
contributes middleware can order itself around authentication.

OidcSession becomes Session in auth-core, carrying claims, an expiry and an opaque
attribute map. OpenID Connect keeps its access, id and refresh tokens in that map
under its own keys, so renewal stays its business and core has no OAuth2 vocabulary
in it. isAccessTokenExpired() becomes isExpired(), with the 30s eager-renewal
window it always had and now a test for it.

flash-ext-oidc is renamed flash-ext-auth-oidc, matching cache-core/cache-caffeine
and data-core/data-hibernate.
2026-09-10 19:06:15 +00:00
Zakaria El Orche c5be6ac7b8 refactor(ext-auth): extract flash-ext-auth-core out of flash-ext-oidc
flash-ext-oidc has always held two things: the OpenID Connect protocol, and a
session/claims/authorization layer that is generic and was only ever fed by one
source. This splits them along Flash's own <domain>-core convention, the same
shape cache-core, data-core and view-core already use.

flash-ext-auth-core gets what never referenced the protocol — @Authenticated,
@RolesAllowed, @ScopesAllowed, ClaimsHolder, the claim matching, and the policy
compiled from annotations — under generic names: OidcUser is Claims, since it
never was more than a typed view over a claims map, and OidcAuthPolicy is
AuthPolicy. It was package-private while being the parameter type of a public
method, so the move also fixes that.

The new seam is CredentialSource: it resolves a request's claims, or rejects the
request the way its protocol says to. AuthMiddleware publishes the result and
matches roles and scopes against it. ClaimsHolder's writers stay package-private
— an implementation produces claims and core publishes them, so nothing outside
this module can put claims on a request that did not carry them.

flash-ext-oidc keeps discovery, JWKS, PKCE, the token endpoint, the login and
callback routes and the OpenAPI oauth2 contributor, and now registers
OidcCredentialSource. flash-ext-mcp still keys McpSecurity on finding that type
and not on AuthMiddleware: REQUIRED has to keep meaning "a real authorization
server is protecting this endpoint", not "something authenticates here".

The middleware key moves with the mechanism: flash.oidc.policy -> flash.auth.policy.

Breaking for consumers: imports move to dev.relism.flash.ext.auth, OidcMiddleware
becomes AuthMiddleware, ClaimsHolder.user()/get() become current()/map().
2026-09-10 19:00:39 +00:00
Zakaria El Orche ea00182c7c test(ext-oidc): characterise claim matching before the auth-core split
Pins the current behaviour of the role/scope matching that is about to move out
of OidcMiddleware: delimiter set for string claims, whole-entry comparison for
list claims, trimming, empty-requirement semantics under ALL vs ANY, and how a
claim path that walks into a non-map resolves. None of it is OIDC-specific and
none of it was covered directly.
2026-09-10 18:48:10 +00:00
Relism 9892ad44b7 Merge pull request 'feat(ext-scheduler): add interval and cron background jobs' (#16) from feature/ext-scheduler/cron-and-interval-jobs into master
Publish Maven packages / publish (push) Successful in 2m10s
2026-09-09 14:36:36 +00:00
Zakaria El OrcheandClaude Sonnet 5 4866624e73 Merge branch 'master' into feature/ext-scheduler/cron-and-interval-jobs
Resolve conflicts in AGENTS.md, pom.xml and flash-extensions/pom.xml — master
had already picked up ext-validation and ext-cache-core/caffeine (merged after
this branch was cut); union kept alongside this branch's ext-scheduler entries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 14:35:18 +00:00
Relism d3fe740e05 Merge pull request 'Feature/ext cache/core and caffeine' (#14) from feature/ext-cache/core-and-caffeine into master
Publish Maven packages / publish (push) Successful in 2m16s
2026-09-09 14:29:29 +00:00
Zakaria El OrcheandClaude Sonnet 5 000cc79cca Merge branch 'master' into feature/ext-cache/core-and-caffeine
Resolve conflicts in AGENTS.md, pom.xml and flash-extensions/pom.xml — both
sides added new module/scope/dependency entries in the same spot (ext-validation
from master, ext-cache-core/ext-cache-caffeine from this branch); union kept.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 14:28:12 +00:00
Relism bd899d52bb Merge pull request 'Feature/ext validation/request validation' (#15) from feature/ext-validation/request-validation into master
Publish Maven packages / publish (push) Successful in 2m16s
Reviewed-on: #15
2026-09-09 14:21:17 +00:00
Zakaria El OrcheandClaude Sonnet 5 4feabc45d5 fix(ext-view): clear cross-engine error, propagate globals across JteExtension builder chain
- BaseViewExtension: wrap the ViewRuntimeBridge provider collision with a message
  naming the real constraint (one view engine per FlashApp) instead of the generic
  "duplicate provider" error.
- BaseViewExtension/JteExtension: carry registered globals across JteExtension's
  immutable settings builders (templateRoot/serveStatics/staticPrefix/withStaticCors/
  staticCors). addGlobal() called before any of those used to be silently dropped,
  since each builder method returned a fresh instance with an empty globals list.
- Correct flash-ext-view-jte docs (README, architecture.md, model-and-globals.md):
  globals merge flat with one typed @param per key, not under a global.* namespace
  like Thymeleaf — the docs previously claimed the same reserved namespace for both
  engines, which doesn't match JteRuntime's actual merge behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 13:31:21 +00:00
Zakaria El Orche 9de2800a83 Merge branch 'feature/ext-cache/core-and-caffeine' into feature/extensions/validation-scheduler-cache
# Conflicts:
#	AGENTS.md
#	flash-extensions/pom.xml
#	pom.xml
2026-09-09 12:37:52 +00:00
Zakaria El Orche dbf057f493 Merge branch 'feature/ext-scheduler/cron-and-interval-jobs' into feature/extensions/validation-scheduler-cache
# Conflicts:
#	AGENTS.md
#	flash-extensions/pom.xml
#	pom.xml
2026-09-09 12:37:26 +00:00
Zakaria El OrcheandClaude Opus 5 24bb10175d feat(ext-cache-core): add the caching contract and a Caffeine backend
Split the way flash-ext-data and flash-ext-view are: cache-core defines
Cache, CacheManager, CacheSpec and CacheStats and talks to nothing;
cache-caffeine implements them in process.

    users = require(CacheManager.class).build("users", spec -> spec
            .maxSize(10_000).ttl(Duration.ofMinutes(10)));

    return users.get(id, repo::findById);

get(key, loader) is the only shape most code needs and the only one that is
hard to get right: the loader runs once per key across concurrent callers
rather than each racing its own. A null result stores nothing, because caching
absence is a decision rather than a default.

build(name, spec) is idempotent per name, so two handlers wanting one cache get
one cache without coordinating who creates it. Disagreeing about the spec
throws rather than resolving to whichever handler initialised first, which is a
bug that only surfaces under load.

recordStats() is opt-in — counting is two atomic increments per lookup, and a
cache nobody measures should not pay for numbers nobody reads. Unmeasured
caches return CacheStats.DISABLED rather than zeroes that look like a cold
cache.

Caffeine rather than a hand-rolled LRU: for genuinely low traffic
ConcurrentHashMap::computeIfAbsent is one line and needs no module at all, and
this exists for when that stops being true. W-TinyLFU admission, striped
counters and amortised eviction are not a weekend's work, and getting them
wrong yields a cache slower than no cache. The adapter is deliberately thin —
every method delegates, adding no wrapper, copy or locking of its own.

Caches are dropped through FlashContext.onClose, so values do not outlive the
app holding them. Invisible with one app per process; immediate under test.

flash-ext-cache-redis is designed but not built, and has docs only — no module,
no pom, no source. An empty module that builds an empty jar is dead weight in
the reactor. The docs record what changes once the cache can fail: get() must
decide whether to fall through to the loader, values need a codec,
invalidateAll needs a key prefix that becomes wire contract, and eviction stats
stop meaning anything. Those are decisions that want a real second replica to
check them against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 12:30:03 +00:00
Zakaria El OrcheandClaude Opus 5 5c163b7f8d feat(ext-scheduler): add interval and cron background jobs
jobs.every(Duration.ofMinutes(5), reports::refresh);
    jobs.cron("0 0 3 * * *", archive::sweep);

One daemon platform thread keeps time; every job body runs on a virtual thread,
so a slow job delays nothing but its own next run and cannot occupy the timer.

Cron expressions compile once into a bitmask per field — a long for seconds and
minutes, an int for the rest — so matching an instant is a shift and a mask
rather than a parse or a set lookup. Next-fire advances by the largest unit
that cannot match instead of ticking second by second, so a yearly expression
resolves in a few dozen iterations rather than thirty million. Five or six
fields, ranges, steps, lists, named months and weekdays, both Sunday encodings,
and the standard union semantics when both day fields are restricted. A
malformed expression throws when the job is registered, not when it would have
fired.

Overlapping runs are skipped, and deliberately not configurable: two copies of
one job at once is a bug in every case anyone has needed, and a flag would only
let it be set wrongly. A skipped run logs how long the previous one has been
going.

A throwing job is logged and keeps its schedule. scheduleAtFixedRate cancels
the task on first exception, silently — a job that dies at 3am and is never
heard from again is the failure this avoids.

Shutdown goes through FlashContext.onClose, so app.stop() drains HTTP first,
then gives running jobs a grace period before forcing them down. Nothing runs
after the app stops. The grace period is the only knob.

Known ceiling, marked in the source: schedules are per-instance, so two
replicas run every job twice. A distributed lock should not exist until there
is a second replica.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 12:23:34 +00:00
Zakaria El OrcheandClaude Opus 5 f68e661296 feat(ext-validation): add request validation with compiled constraints
Standard jakarta.validation annotations, compiled once per type into a flat
check table. No configuration: constraints come from the annotations already on
your types, and ValidationException extends HttpException with status 422 so
the default handler renders it without this extension registering anything.

    record CreateUser(@NotBlank @Size(max = 80) String name,
                      @Email String email,
                      @Min(18) int age) {}

    CreateUser dto = validation.body(req, CreateUser.class);

Annotations only — Hibernate Validator's engine is deliberately absent. It
resolves constraints reflectively per call and pulls ~2 MB plus EL, which is
the per-request cost this module exists to avoid. jakarta.validation-api is
~90 KB of annotations.

The passing path allocates nothing. Constraints resolve at first use into an
opcode plus operands cached in a ClassValue, so there is no map lookup and no
lock. Fields are read through MethodHandles adapted to an exact signature —
(Object)Object for references, (Object)long for primitive integrals — so
invokeExact neither boxes nor builds the argument array Field.get and
Method.invoke allocate. Checks are a flat array walked by a tableswitch rather
than a class hierarchy behind a virtual call. @Size reads a length the object
already knows and @Email scans with indexOf, because Pattern.matcher allocates
a matcher and two int arrays per call. Messages are pre-rendered at compile
time. The violation list and the exception exist only once something fails.

@Pattern is the marked exception: its regex compiles once but matcher()
allocates per call.

Constraints are read from declared fields, so records and plain classes take
one code path — a constraint on a record component propagates to its backing
field.

Jakarta null semantics are exact: only @NotNull rejects null.

flash-ext-openapi now mirrors the same annotations into the generated schema —
minLength, maxLength, minItems, minimum, maximum, pattern, format: email and
required — via an optional jakarta.validation dependency detected at boot. A
type declares its rules once and both the validator and the published contract
read them. An explicit @Schema still wins; the bridge only fills keys nobody
set, and without the annotations on the classpath the bridge class is never
loaded.

flash-ext-jackson is optional too: validate(value) works without it, only
body(req, type) needs a codec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 12:20:33 +00:00
Zakaria El OrcheandClaude Opus 5 fe8c6ed162 fix(core): honour HttpException status in the default exception handler
HttpException carries the status the caller meant, and its own javadoc says
extensions map it to a structured response — but nothing did. Every one reached
the catch-all and came back as 500, including the 400s that RequestHelper
raises for a malformed query param and that flash-ext-jackson raises for an
unparseable body. A handler doing the documented thing produced the wrong
status.

The default handler now renders HttpException at its own status, in both dev
and prod modes, with the message JSON-escaped. Not pre-encoded like JSON_404
and JSON_500: the message is per-exception, and a path that already unwound a
stack does not need the allocation shaved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 12:20:33 +00:00
Zakaria El OrcheandClaude Opus 5 58bae41f7a docs(testing): document flash-testing and the limits it deliberately keeps
README covers the application handle, requests and assertions, service
replacement, multi-server wiring, scope, WebSockets, configuration and the
teardown ordering.

limits.md records the seven things the harness cannot do and what to use for
each: TLS, HTTP/2, WebSocket over HTTP/2, malformed requests, response framing,
the flash core module's dependency cycle, and scoped services. Each is a
consequence of a real constraint rather than an unfinished feature, so writing
them down stops the next person rediscovering them one at a time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 12:07:55 +00:00
Zakaria El OrcheandClaude Opus 5 7785712efe test(ext-web-bundler): migrate integration test to flash-testing
Two frontend layouts, each laid out on disk inside its own application's
configure(). The harness runs that lazily at first access, so @TempDir is
populated by then and the server for whichever test is not running never boots.

Replaces three blocks of HttpRequest.newBuilder(URI.create(...)) per test with
single-line assertions. 98 to 60 code lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:05:06 +00:00
Zakaria El OrcheandClaude Opus 5 c02459dd7c test(ext-mcp): migrate integration and security suites to flash-testing
McpExtensionIntegrationTest: 10 stateless JSON-RPC calls against one server
config, so one class-scoped server replaces a boot per test. 117 to 85 code
lines.

McpExtensionSecurityTest: the four server configurations it exercises — AUTO
without oidc, REQUIRED with a derived resource identifier, REQUIRED with an
explicit one, and REQUIRED with advertised scopes — become four named servers
sharing one FakeOidcProvider, replacing eight boots and a freePort() helper.
151 to 128 code lines. The boot-rejection test still builds its app directly:
a harness whose job is to boot an app is the wrong tool for asserting that
booting fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:05:06 +00:00
Zakaria El OrcheandClaude Opus 5 e0ad83eb2f test(core): read bound ports back from the app instead of guessing free ones
Every integration test picked a port by opening ServerSocket(0), closing it and
reusing the number, which races anything else on the machine between the close
and the rebind. FlashApp.port() reports the port the listener actually bound,
so the guess is gone: 18 freePort() helpers deleted, 40 call sites now pass
port(0) and read the result back.

Two ServerSocket(0) uses remain and are correct. ConnectionRunnerTest accepts
on its socket rather than using it to pick a number. H2LoadMeasurementTest
hands its port to an external nghttpd process, which has no equivalent of
port() to read back; that one is now commented to say so.

HttpServerTlsTest's two-listener case reads both back through ports().

HttpServerTest and HttpServerConcurrencyTest also move from @BeforeEach to
@BeforeAll — every test in them is read-only against the same routes, so 11 and
3 boots respectively become 1. HttpServerConcurrencyTest's lazy-compile test
keeps building its own app, since a freshly compiled router is the thing it
tests. HttpServerTest now runs 11 tests in 0.06s.

The http2 interop suites (curl, nghttp, grpcurl, h2spec), the load measurement
and the soak test are skipped without their external binaries or system
property, so those edits are compile-verified here and exercised in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:05:06 +00:00
Zakaria El OrcheandClaude Opus 5 1c207cf94c feat(testing): boot lazily instead of eagerly in beforeEach
beforeEach called ensureStarted(), so every FlashTest field in a class booted
for every test whether or not that test touched it — a class holding four
servers paid for four boots per test. Booting is already lazy on first access,
so the hook was only ever forcing work forward.

Neither hook starts anything now. beforeAll still records that a static field
owns the class-scoped lifecycle, which is what keeps afterEach from tearing a
class-scoped server down after the first test.

This also lets an application read @TempDir inside configure(): JUnit populates
those during instance post-processing, before the first test body but after
extension beforeEach callbacks would have fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:05:06 +00:00
Zakaria El OrcheandClaude Opus 5 fa5a025c93 test(ext-mcp): migrate McpAuthPolicyTest to flash-testing
First migration, chosen because it is the case that drove the harness design:
a Flash app plus a FakeOidcProvider, with tokens audience-bound to the app's
own port, so the port has to be readable after boot.

Drops the racy free-port dance, the hand-rolled HttpClient plumbing and the
per-test teardown; failures now report the response body. 120 to 106 code
lines, and what remains is tool-policy assertions rather than fixture code.

The two boot-rejection tests keep building their app directly — a harness whose
job is to boot an app is the wrong tool for asserting that booting fails — but
port(0) removes freePort() from those too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:22:40 +00:00
Zakaria El OrcheandClaude Opus 5 424ca31b7a feat(testing): add flash-testing, a JUnit 5 harness for Flash applications
Boots a real app on an OS-assigned port for a test class or a single test and
hands back a client pointed at it:

    @RegisterExtension
    static FlashTest app = FlashTest.of(new BlogApp())
            .mock(UserService.class, new InMemoryUserService());

    app.get("/api/users").expectStatus(200).expectBodyContains("alice");

A field rather than an annotation, because annotation values are compile-time
constants and so could never express a second server wired from the first —
FlashTest.of(new BlogApp(auth.baseUri())). Startup is lazy, so reading
baseUri() boots that server on the spot and declaration order does the wiring,
with no dependence on JUnit's extension ordering. A static field boots once per
class, a non-static field once per test; that is stock JUnit field semantics
rather than an option to configure.

Runs against a real loopback port instead of dispatching in-process. An
in-process dispatcher would be a third copy of the routing/handler/exception
sequence that Http1Connection and Http2StreamDispatcher already duplicate, kept
in sync by hand, and it would let a test pass while the status line,
content-length or HPACK encoding was broken.

mock() installs overrides as the last extension, after everything the
application and its extensions declare, so a fake always wins. Any object is
accepted, so a hand-written fake and a Mockito mock are equally welcome and
this module depends on no mocking library — only flash and junit-jupiter-api.

Teardown cancels the client before stopping the server: HttpClient holds
keep-alive sockets open and ServerLifecycle.stop() spins until the last one
closes, so the default 15s drain would otherwise be paid on every test class.
shutdownNow rather than close(), which blocks until every operation completes
and would hang on a leaked WebSocket.

Lives at the top level, not under flash-extensions/, which holds things you
install() onto an app; this carries junit-jupiter-api at compile scope and
nothing installable should.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:22:40 +00:00
Zakaria El OrcheandClaude Opus 5 4a85a27648 feat(core): expose bound ports, service override, FlashApplication and close hooks
Four small seams, each useful on its own, that together make a Flash app
testable without hand-rolled scaffolding.

- ServerHandle/ServerLifecycle/FlashApp gain port()/ports(). Listeners already
  bind in the FlashApp constructor, so port(0) resolved to a real port that
  nothing could read back; every integration test worked around this by
  opening a ServerSocket(0), closing it and reusing the number, which races
  anything else on the machine.

- FlashContext.override() replaces a binding instead of rejecting it. The
  duplicate-is-an-error rule stays everywhere else; this is the single
  deliberate exception, for swapping a service out in tests. A replacement is
  logged at INFO so misuse in production is visible.

- FlashApplication + FlashApp.apply() name an application independently of the
  port it runs on, so the same one can be booted twice. It takes FlashApp
  rather than FlashRegistrar because ws() and mount() live there. Being a
  functional interface, a lambda and a named class are the same thing.

- FlashContext.onClose() runs cleanup at stop(), children first and then in
  reverse registration order. stop() previously closed sockets and the
  executor and never touched the service graph, so a pooled DataSource was
  only ever released by JVM exit — invisible with one app per process, a leak
  per test class once a suite boots and stops many.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:22:23 +00:00
Zakaria El OrcheandClaude Sonnet 5 74169e40f4 feat(core): add any() route registration, warn on undocumented OpenAPI routes, log unhandled 500s
Publish Maven packages / publish (push) Failing after 3m35s
FlashRegistrar#any() registers a handler under every HTTP method for
verb-indifferent handlers (e.g. a reverse proxy). OpenApiExtension now warns
when a class-based handler has no @ApiOperation instead of silently omitting
it from the spec. AbstractRouter's default production exception handler now
logs unhandled exceptions server-side instead of only returning a JSON 500.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 11:56:51 +00:00
Zakaria El OrcheandClaude Sonnet 5 3d743b34ce fix(build): resolve fpr-core from Gitea, not the dead maven.relism.dev
Publish Maven packages / publish (push) Successful in 2m3s
maven.relism.dev is down. fpr-core's source (FastPathRouter) now publishes
to Gitea's own Maven registry via its own publish-maven.yml, same pattern
this project already uses for itself — point <repositories> there and bump
the pinned fpr-core version to the first build actually published there
(1.1.0-36216cd). Full reactor build + test suite green against the new
resolution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 11:10:50 +00:00
Relism f8e0a1d3fa Merge pull request 'fix(http2): close a streaming response body on every exit path' (#13) from hotfix/http2-response-writer-stream-leak into master
Publish Maven packages / publish (push) Successful in 2m9s
Reviewed-on: #13
2026-08-14 23:02:30 +00:00
Relism 5954cac66c Merge pull request 'fix(websocket): mask outgoing CLOSE frames in client mode' (#12) from hotfix/ws-close-frame-masking into master
Publish Maven packages / publish (push) Canceled after 7s
Reviewed-on: #12
2026-08-14 23:02:12 +00:00
Zakaria El OrcheandClaude Sonnet 5 e5bec59410 fix(http2): close a streaming response body on every exit path
Http2ResponseWriter.streamBody (a streaming response's InputStream, driven
across startFlowControlled/resume as flow-control windows allow) was never
closed anywhere -- not on clean EOF, not on a write failure, not when the
stream is abandoned (RST_STREAM from the peer, connection teardown). Same
bug Http1ResponseWriter had before cf16be0, just never given the same fix: a
handler stream that releases a held resource (a pooled backend connection,
for a reverse proxy) from close() leaks it under any real amount of stream
resets or aborted connections.

- appendData closes streamBody once `end` is reached (clean completion) and
  on any IOException from the read itself, mirroring Http1ResponseWriter's
  relayAndClose/writeChunkedAndClose reasoning.
- New Http2ResponseWriter#abort(), called from Http2Stream#cancel() --
  symmetric with that method's existing http2Body.cancel() for the inbound
  leg, now covering the outbound one too. cancel() is the single hook every
  abandoned-stream path (RST_STREAM handling and connection teardown in
  Http2Connection, plus Http2StreamDispatcher) already goes through, so this
  covers every abort case without adding a new one.

closeStreamBodyQuietly() is idempotent (nulls streamBody after closing), so
the appendData and abort() close paths can't double-close or race.

Four new Http2ResponseWriterTest cases: normal completion in one call,
completion across a resume() (multiple flow-control windows), abort() while
still streaming, and abort() as a no-op on a non-streaming response. 697/697
flash-module tests green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LLwcyHUnbApCrY33gvgoa
2026-08-14 22:51:16 +00:00
Zakaria El OrcheandClaude Sonnet 5 901da954a9 fix(websocket): mask outgoing CLOSE frames in client mode
WebSocketSession.close(int) hand-wrote a raw, always-unmasked 4-byte CLOSE
frame, bypassing writeFrame's maskOutgoing handling that sendText/send/
sendPong already go through correctly. A client-mode session (maskOutgoing
true — WS-client usage, e.g. Pathway's UpstreamWebSocketConnector relaying a
proxied client's close to a backend) therefore sent an RFC-6455-invalid
unmasked frame.

This was latent until this same HTTP/2 branch's readFrame rewrite added the
receive-side masking check RFC 6455 §5.1 requires: a strict peer now rejects
the malformed frame with WebSocketProtocolException("client frame must be
masked") before ever exposing it as a CLOSE, silently dropping the close
instead of relaying it — reproduced end to end via Pathway's
ProxyWebSocketIntegrationTest.clientCloseIsForwardedToTheBackend.

close(int) now builds its 2-byte payload and calls the same writeFrame path
every other outgoing frame uses, so masking (or not) follows maskOutgoing
automatically. Existing server-mode close_setsClosedAndWritesFrame is
unchanged (byte-for-byte identical output — no mask bit, no key). Added
close_masksWhenActingAsClient (mirrors the existing sendText coverage) and a
round-trip regression, readFrame_acceptsCloseFrameWrittenByAClientSession,
that reproduces the actual bug: a client session's close() output fed
straight into a server session's readFrame().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LLwcyHUnbApCrY33gvgoa
2026-08-14 22:46:59 +00:00
Relism 787ae610d4 Merge pull request 'fix(ext): update stale HeaderMap references to Http1HeaderMap' (#11) from hotfix/header-map-rename into master
Publish Maven packages / publish (push) Successful in 2m36s
Reviewed-on: #11
2026-08-14 18:27:13 +00:00
Zakaria El OrcheandClaude Sonnet 5 b5ff5a4c1f fix(ext): update stale HeaderMap references to Http1HeaderMap
HeaderMap was split into the HeaderView interface and Http1HeaderMap
impl in the HTTP/2 model refactor, but two extension-module tests
(flash-ext-jackson, flash-ext-view-jte) still referenced the old
class name, breaking the CI build on master after the HTTP/2 PR merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JCmqb7XGb3JQvCWf589yma
2026-08-14 18:25:42 +00:00
Relism 2078dea54f Merge pull request 'feat(core): HTTP/2 support, correctness fixes, and doc reorganization' (#10) from feature/core/http2 into master
Publish Maven packages / publish (push) Failing after 53s
Reviewed-on: #10
2026-08-14 18:20:30 +00:00
Zakaria El Orche a0dda8e47a refactor(core): remove out-of-scope HTTP/2 client/proxy, reorganize docs, refresh README
HttpProxy and Http2Client (719 LOC) shipped a reverse-proxy adapter and outbound HTTP/2
client from flash core with zero callers anywhere in the server itself — only each
other and their own tests. An HTTP/1.1+2 server framework has no business bundling an
outbound client; that capability belongs in its own flash-extensions/flash-ext-*
module if/when it's needed. Removed, along with the now-dead src/bench load driver
that depended on Http2Client (no replacement client written here — flagged as
follow-up work, not silently dropped).

docs/http2/ had accumulated core, cross-protocol documentation alongside genuine
HTTP/2-protocol internals: HTTP1-HARDENING, TRANSPORT, MESSAGE-MODEL,
TRAILERS-AND-STREAMING and BYTES all describe machinery HTTP/1.1 and HTTP/2 share, not
HTTP/2 specifically. Moved to a new docs/core/, leaving docs/http2/ to the protocol
layers, wire internals and operational docs that are actually HTTP/2-specific.
CLEARTEXT-AND-PROXY.md renamed to CLEARTEXT.md and its now-removed upstream-client
section cut, matching the source removal above.

README.md: removed the "HTTP/2 upstream proxy" section (documented the deleted
HttpProxy/Http2Client), the flash-bench module row and build command (not a module
that exists in this repo), and fixed every doc link to the new docs/core/ paths.
Added the new FlashConfiguration.maxConnections field to the configuration reference.

src/bench/ (a load-test harness distinct from the JMH suite, not wired into any Maven
profile or CI) is committed here for the first time.
2026-08-14 18:13:03 +00:00
Zakaria El Orche cf16be08c0 fix(core): fix HTTP/2 rate-limiter false positives, connection-flood OOM, and a streaming-body leak
Targeted stress testing under this branch's HTTP/2 work surfaced four independent
production bugs, each verified with a before/after load test and a regression test:

- Http2Limits.MAX_STREAMS_CREATED_PER_INTERVAL (400/10s) rejected legitimate
  high-concurrency HTTP/2 clients as if they were CVE-2023-44487 rapid-reset abuse —
  h2load's default pattern alone triggered 40-92% request failure. Raised to 100,000,
  matching MAX_STREAMS_PER_CONNECTION's existing lifetime budget; the RST_STREAM-rate
  counter remains the precise defence against the actual attack signature.

- Flash had no connection-admission control anywhere: AcceptLoop accepted every TCP
  connection unconditionally, so a connection flood (h2load -c 400) ran the JVM out of
  heap and crashed with OutOfMemoryError, killing even unrelated daemon threads.
  TransportLimits.defaultMaxConnections() auto-scales a cap from Runtime.maxMemory();
  ConnectionRunner.accept() enforces it before any per-connection state (TLS handshake
  included) is created. Verified surviving 42x the admission limit under both cleartext
  and TLS load with bounded RSS.

- Http1ResponseWriter never closed a handler's streaming response body on a write
  failure (e.g. the client disconnecting mid-transfer) — only on a clean EOF. A handler
  whose stream releases a held resource (a pooled backend connection, for a reverse
  proxy) from close() leaks it under any real amount of client disconnects. Now closed
  on every exit path, matching InputStream#close()'s own idempotency contract.

- Http2StreamState.transition() called the enum's values() every state transition;
  values() clones a fresh array on every call. Cached once, removing ~10.76% of
  allocations measured live under load.

695 -> 698 tests (three new regression tests), all passing.
2026-08-14 18:12:46 +00:00
Zakaria El Orche 825bdfc942 docs(core): document HTTP/2 operation and architecture 2026-08-13 21:39:37 +00:00