HTTP entity endpoints — top-level commissioning story
Table of Contents
This page is a capture in the next bucket of the product backlog — a pre-sprint idea, not yet pulled into a sprint as a story.
What
Make HTTP a top-level story (with one task per component / entity group),
rather than a "file Wt and HTTP gap captures" task buried in each
commission_* story. Every commission story currently carries such a task
(e.g. country, and the equivalents
for currency, book_status, party_status, party_type, monetary_nature,
rounding_type, currency_market_tier, …) — all of which only defer HTTP to a
capture. Consolidate them: those per-commission tasks are superseded by this
top-level story and should be abandoned/closed in favour of it when picked up.
Pick up after all entities are commissioned — HTTP is a horizontal layer best done once across entities, not interleaved entity-by-entity.
Current state (from investigation of projects/ores.http)
Three libs + an exe: api (Boost.Beast server, router=/=route_builder,
request/response value types, OpenAPI endpoint_registry), core (the route
modules iam_routes, risk_routes [currencies], variability_routes,
assets_routes, storage_routes), server (the application=/=host, config
parser, main, and a NATS discovery-only registrar). Entry:
server/src/main.cpp:60 → host::execute → application::run.
- Routes are lumped by domain, not per entity.
risk_routesowns currencies;iam_routesowns auth+accounts+roles. Each module is a class withregister_routes(router, registry)+ handler methods, instantiated and wired by hand inapplication.cpp:153-167. That hand-wiring is the blob. - Handlers call domain services IN-PROCESS, not over NATS. e.g.
refdata::service::currency_service service(ctx_); service.list_currencies(...)(core/src/routes/risk_routes.cpp:114-115), serialised with rfl::json. So country routes will callrefdata::service::country_servicedirectly (it already exposes list/get/save/delete/history). No NATS round-trip. - Route declaration (
risk_routes.cpp:44-57):router->get("/api/v1/currencies").summary(...).auth_required().query_param(...).response<get_currencies_response>().handler(...), then BOTHrouter->add_route(built)andregistry->register_route(built)(OpenAPI is a separate registry — forget it and the route is invisible in/openapi.json). - Auth is a single session-level choke point:
api/src/net/http_session.cpp:138-191validates the Bearer token whenroute.requires_auth, stores claims inauthenticated_user, then checksrequired_roles.
Auth / JWT is stale and half-migrated (must be fixed as part of this)
- CRITICAL — algorithm mismatch. The HTTP server signs HS256 with a local
secret (
api/src/net/http_server.cpp:41-45; RS256-from-file is a// TODOstub at:48-49), while the entire rest of the fleet verifies RS256 against the signing service's public key (request_context.cpp:46-48, the domain/Wt runners). HTTP-minted tokens fail verification everywhere else; only masked today because HTTP handlers call services in-process and never present the token over NATS. - Tenant identity not propagated.
handle_logincomputes tenant id/name but never setsclaims.tenant_id(iam_routes.cpp:537-543), thoughcreate_token=/=validatesupport it and downstream reads it. Authenticated requests lose tenant scoping → entity routes operate onctx_'s tenant, not the caller's. - Fails open if the authenticator is unconfigured (
http_session.cpp:150: validation only runs insideif (is_configured())). - 100-year leeway validator exists (
jwt_authenticator.cpp:220-318); no refresh endpoint (24h fixed expiry, re-login only).
Target architecture (codegen-first)
The goal is: adding an entity's HTTP API = a codegen run, touching no shared file — mirroring SQL/C++/Qt.
- Per-entity generated unit
<entity>_routes: thin class, parse → in-process<entity>_servicecall → rfl::json serialise, declarative.auth_required()=/.roles()=. Exactly therisk_routesshape, so directly templatable. - Generated aggregator = the delegation (the registrar idea, done right — see
generate messaging registrars via codegen):
a generated
register_refdata_routes(router, registry, ctx, sessions)that instantiates+registers each entity's routes;application.cppcalls the one aggregator. Adding an entity regenerates the aggregator — no hand-edit, and it is not a single growing function (it iterates the entity model list). - Shared, modernized auth in the session/authenticator + login; generated
routes only declare
auth_requiredso the fix lands once and all inherit. - OpenAPI generated per entity (the
registry->register_routecalls), so the spec stays in sync.
Proposed task breakdown (top-level HTTP story)
- Modernize HTTP auth: implement RS256-from-file to align with the fleet
(or consciously decide HS256 + propagate), set
tenant_id(and party) in login claims, fail closed when unconfigured, drop the 100-year leeway, add a refresh endpoint. Foundation for everything below. - Unpick
countryinto a per-entitycountry_routesunit + a delegation aggregator wired intoapplication.cpp(hand-written, modelled on =risk_routes=/currency, structured to be codegen-ready). Reference impl. - Templatize into a codegen HTTP facet (generate
<entity>_routes+ the aggregator + OpenAPI); regenerate country and prove zero-diff against the hand-written unit. - Per-component/per-entity rollout tasks: generate HTTP for each commissioned entity (country, currency, party_status, party_type, …). One task per component keeps it reviewable.
Why
HTTP is the one entity-facing layer with neither codegen nor a coherent home: it's lumped by domain, hand-wired, and its auth is architecturally inconsistent with the rest of the fleet. Threading it as a deferred task in every commission story produced a pile of identical "file captures" tasks and no implementation. A single top-level story — modernize auth, establish the per-entity pattern on country, then templatize — gives every entity a consistent, generated REST API and fixes the auth split in one place.
References
projects/ores.http/(api/core/server). Route exemplar:core/src/routes/risk_routes.cpp:44-205(currency). Router:api/src/net/router.cpp. Auth choke point:api/src/net/http_session.cpp:138-191. JWT:security/src/jwt/jwt_authenticator.cpp; HTTP HS256 wiring:api/src/net/http_server.cpp:41-49; login claims:iam_routes.cpp:537-543.- Backend ready:
refdata/core/.../service/country_service.hpp;refdata/api/.../messaging/country_protocol.hpp:30-78. - Wiring point:
server/src/app/application.cpp:153-167. OpenAPI:api/src/openapi/endpoint_registry.cpp, served at/openapi.json//swagger. - Recipe:
doc/recipes/http/how_do_i_create_entity_endpoints.org.
See also
- Wt entity UI — top-level commissioning story (the sibling layer; same per-entity + generated-aggregator approach).
- Generate messaging registrars via codegen (same generated-aggregator idea on the NATS side).
- Superseded per-commission task exemplar: File Wt and HTTP gap captures for country (and equivalents in every other commission_* story).