You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: query-capability discovery via meta.schema + entity record response envelope (#3332)
* feat: wrap entity record responses in a data/meta envelope
Every single-record entity endpoint (getOne/create/update/put/delete/
confirm across all 24 entity controllers) now returns the { data, meta }
envelope (EntityRecordWrappedResponse) that the MFA enroll response
pioneered, and the typed client exposes the envelope to callers.
Services keep returning bare domain entities - the controller owns the
wire shape. Groundwork for #1649: query-capability metadata (meta.schema)
rides the record envelope and the collection meta in a follow-up.
The OIDC userinfo endpoint splits onto a dedicated flat GET /userinfo
route (advertised via discovery, client userinfoEndpoint updated), so
GET /users/@me could adopt the envelope without touching the protocol
surface. Protocol/bespoke shapes stay flat: token/introspect/revoke,
authorize, jwks + openid-configuration, logout, register/activate/
password workflows, status, authenticator-challenge, permission/policy
check, session deleteMany.
BREAKING CHANGE: API consumers reading bare record bodies must unwrap
data; old clients against a new server break (lockstep beta release).
* feat(server-core): expose the queryable vocabulary under meta.schema
Closes#1649. Every query-capable GET now carries the endpoint's rapiq
schema description in the response meta: collections advertise the full
vocabulary (fields/filters/sort/relations/pagination), record reads the
fields+relations subset a single read processes. The description is the
static allow-list upper bound, serialized once per schema via rapiq's
new schema.describe() (tada5hi/rapiq#851, needs @rapiq/core
2.0.0-beta.11) and memoized in core/query/describe.ts. Relation
capabilities are referenced by target schema name (relations.schemas)
instead of being expanded inline - dotted vocabulary is discovered on
the target entity's own endpoints. Actor-dependent gates stay
deliberately unreflected.
* docs: record envelope, meta.schema discovery and userinfo split
Rewrites the core-http-kit client/testing SDK docs onto the { data, meta }
record contract, adds the query-capability discovery section (meta.schema
reading rules incl. the referenced-not-expanded relation vocabulary), and
updates the agent guides (thin-controller conventions, core-http-kit row,
mTLS userinfo example).
* test: align meta.schema expectations and docs with normalized descriptions
rapiq's describe() output is now shape-normalized (tada5hi/rapiq#851
follow-up): every described parameter carries every constraint key, with
null = never declared and [] = explicitly nothing (realm's pinned empty
relations allow-list now reads { allowed: [], schemas: {} }).
* chore: bump @rapiq/* to ^2.0.0-beta.11
The released schema.describe() (tada5hi/rapiq#851) is what meta.schema
serializes through - core-http-kit and server-core now declare the
minimum that actually carries it.
* test: adopt the converged include=client secret gating of rapiq beta.11
rapiq#847 (shipped in 2.0.0-beta.11 alongside the describe API) narrows
an explicit include to its requested per-relation fieldset, so the
select:false client secret is now genuinely selected under
include=client + fields[client] and the schema gate governs it — the
same self-visible / foreign-redacted contract as the auto-joined bare
projection, which is exactly the convergence the #3322 gate design
anticipated. The spec previously pinned the historical #831
fully-selected-join divergence (secret unshippable for anyone).
* refactor: make EntityRecordResponse the envelope type directly
EntityRecordResponse<R, M> IS the { data, meta } record envelope now -
the transitional EntityRecordWrappedResponse name and the deprecated
alias are gone; every signature, controller annotation and doc uses
the one name.
* fix: harden schema description sharing and audit fallout
Review-audit follow-ups: the memoized meta.schema description is now
deep-frozen (the shared-by-reference object's do-not-mutate contract is
self-enforcing), the RECORD_QUERY_PARAMETERS doc no longer overclaims
(only the user/client single reads decode fields/relations today - the
record advertisement is the target vocabulary, convergence tracked in
plan 076), and the two kit form specs stub their record routes with the
real { data, meta } wire shape instead of bare entities.
Copy file name to clipboardExpand all lines: .agents/architecture.md
+10-7Lines changed: 10 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -648,12 +648,13 @@ not by client-web:
648
648
649
649
### Thin Controller Pattern (HTTP Adapter)
650
650
651
-
Controllers are thin HTTP adapters. They extract input from the routup `IAppEvent`, build an `ActorContext`, delegate to the service, and format the HTTP response. Request body payload types come from `@authup/core-http-kit` (shared between the typed Client, the controller, and `@trapi/swagger` schema generation); response types are the domain entity from `@authup/core-kit`directly:
651
+
Controllers are thin HTTP adapters. They extract input from the routup `IAppEvent`, build an `ActorContext`, delegate to the service, and format the HTTP response. Request body payload types come from `@authup/core-http-kit` (shared between the typed Client, the controller, and `@trapi/swagger` schema generation); every entity **record**response is the `{ data, meta }` envelope (`EntityRecordResponse<T>` — the shape the MFA enroll response pioneered, uniform since issue #1649), with the domain entity from `@authup/core-kit`under `data`:
652
652
653
653
```typescript
654
654
importtype { Role } from'@authup/core-kit';
655
655
importtype {
656
656
EntityCollectionResponse,
657
+
EntityRecordResponse,
657
658
RoleCreatePayload,
658
659
} from'@authup/core-http-kit';
659
660
@@ -677,25 +678,25 @@ export class RoleController {
- Return type is the domain entity directly (`Promise<Role>`, `Promise<EntityCollectionResponse<Role>>`). This lets `@trapi/swagger` extract the response schema from the method signature.
699
+
- Return type is a literal annotation (`Promise<EntityRecordResponse<Role>>`, `Promise<EntityCollectionResponse<Role>>`). This lets `@trapi/swagger` extract the response schema from the method signature. Services still return bare domain entities — the controller owns the envelope. Excluded from the envelope (protocol/bespoke shapes, stay flat): the OAuth2/OIDC surface (`/token*`, `/authorize`, jwks + openid-configuration, `/userinfo`, `/logout`), the register/activate/password workflows, `/`, the authenticator-challenge surface, permission/policy `check`, and session `deleteMany` (`{ count }`).
699
700
- Body parameter type is the concrete payload type (`@DBody() data: RoleCreatePayload`) — sourced from `@authup/core-http-kit`. Naming convention: `<Entity>CreatePayload` for POST, `<Entity>UpdatePayload` for POST `/:id`, `<Entity>SavePayload` for PUT `/:id`. Response shapes that genuinely diverge from the domain entity (e.g. `PolicyResponse`, `RegisterResponse`, `PasswordForgotResponse`) keep a named alias; trivial passthrough aliases are not introduced.
700
701
-**No business logic** — no permission checks, no validation, no entity manipulation
701
702
- Read the routup event via `@DContext() event: IAppEvent`
@@ -710,6 +711,8 @@ Controller conventions:
710
711
Exceptions where controllers retain some logic:
711
712
-**Self-access resolution** (client, user): Resolve `@me`/`@self` tokens to actual IDs before delegating
712
713
714
+
The OIDC userinfo endpoint is a dedicated flat route `GET /userinfo` (`adapters/http/controllers/workflows/userinfo/`, advertised via discovery, `userinfoEndpoint` in the core-http-kit `Client` config) — it serves the authenticated user's record as a FLAT claims document and must never adopt the record envelope, which is why it is not an alias of `GET /users/@me` (that route carries the envelope like every other record read).
715
+
713
716
No controller (or service) reaches for global singletons — cross-cutting services (logger, domain-event publisher) are constructor-injected from the DI container by the factories in `app/modules/http/modules/controller.ts`.
714
717
715
718
### Wiring (Module Layer)
@@ -2380,7 +2383,7 @@ realtime), port `IUserAuthenticatorRepository` + `UserAuthenticatorService` in
2380
2383
entity) nulls both — the raw seed/URI/QR/codes appear exactly once, in the
Copy file name to clipboardExpand all lines: .agents/structure.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -21,7 +21,7 @@ It follows hexagonal architecture principles, separating core business logic, ad
21
21
|[client-web-nuxt](../packages/client-web-nuxt)| Library | A package for the integration in a nuxt web application. |
22
22
|[client-web-theme](../packages/client-web-theme)| Library | Authup app theme for vuecs components, built on `@vuecs/theme-tailwind` (extends `@authup/client-web-kit-theme`). CSS lives under `assets/css/` (`index.css` + `styles/**` partials) and ships raw (`files: ["assets", "dist"]`, `exports.style` + `./index.css` → `./assets/css/index.css`). Its entry imports the kit theme via the bare `@import "@authup/client-web-kit-theme";`, and the apps import this package via `@import "@authup/client-web-theme";` — Tailwind v4 resolves CSS imports through node resolution honoring the `style` exports condition (the apps' `@authup/* → src` Vite/Nuxt aliases do NOT apply to Tailwind's CSS import resolution — verified empirically; don't switch these to relative paths). |
23
23
|[core-kit](../packages/core-kit)| Library | A package providing functions, interfaces and utilities for the core service. |
24
-
|[core-http-kit](../packages/core-http-kit)| Library | A package providing a http client with different sub api clients for resources and workflows. Entity-type-string dispatch goes through the derived registry (`pickEntityAPI(client, type)`; `ClientEntityAPIKey = keyof IClient & keyof EntityTypeMap`, so a sub-API named after an `EntityTypeMap` key joins automatically) — the cast-free `ClientEntityAPIRegistry` assignment inside the helper is the compile-time proof that each entity-keyed sub-API serves its `EntityTypeMap` record type (record-type drift fails this package's build). The dispatch surface `EntityAPIDispatch<T>` is deliberately per-verb optional (sessions/events are read(+delete)-only, junctions carry no update), so dispatch callers — the kit's entity record/collection managers, `AEntityDelete`, `APermissionPolicyBindingButton` — guard per method instead of `as any`-indexing the client (#3087). |
24
+
| [core-http-kit](../packages/core-http-kit) | Library | A package providing a http client with different sub api clients for resources and workflows. Entity **record** responses are the `{ data, meta }` envelope (`EntityRecordResponse`; `EntityRecordResponse` is a deprecated alias of it) and query-capable GETs carry the endpoint's queryable vocabulary under `meta.schema` (rapiq `SchemaDescription`, issue #1649). The OIDC userinfo endpoint is the dedicated flat `GET /userinfo` (`userinfoEndpoint` in the `Client` config). Entity-type-string dispatch goes through the derived registry (`pickEntityAPI(client, type)`; `ClientEntityAPIKey = keyof IClient & keyof EntityTypeMap`, so a sub-API named after an `EntityTypeMap` key joins automatically) — the cast-free `ClientEntityAPIRegistry` assignment inside the helper is the compile-time proof that each entity-keyed sub-API serves its `EntityTypeMap` record type (record-type drift fails this package's build). The dispatch surface `EntityAPIDispatch<T>` is deliberately per-verb optional (sessions/events are read(+delete)-only, junctions carry no update), so dispatch callers — the kit's entity record/collection managers, `AEntityDelete`, `APermissionPolicyBindingButton` — guard per method instead of `as any`-indexing the client (#3087). |
25
25
|[core-realtime-kit](../packages/core-realtime-kit)| Library | A package for the core socket service. |
26
26
|[errors](../packages/errors)| Library |`AuthupError` (extends `BaseError` from `@ebec/core`), error-code constants, built-in subclasses (`BadRequestError`, `EntityNotFoundError`, ...), code→HTTP-status mapping, and `Symbol.for(...)`-keyed duck guards. Error JSON carries the `@instanceof` marker chain as a string list (`BaseError.toJSON()` since `@ebec/core` 1.2.0, tada5hi/ebec#448; `AuthupError.toJSON()` re-stamps it after the `data` spread so a data key can't displace it), and every duck guard's fast path is `matchesInstanceof` (symbol **or** serialized-string chain, re-exported from `@ebec/core`) — so guards keep the inheritance match for JSON-rehydrated errors (#3042); new guard modules must use `matchesInstanceof`, never raw `hasInstanceof`. |
27
27
| [i18n](../packages/i18n) | Library | Framework-agnostic translation catalogs + locale registry. `CATALOGS` is an ilingo `CatalogNode` (built via ilingo's `defineCatalog`/`defineLocale`/`defineNamespace`/`defineTranslations` helpers, locale → namespace → translations) consumed directly by `MemoryStore({ data: CATALOGS })`. Also exports namespace/key enums (`TranslatorTranslation*`), `LOCALES`/`LocaleCode`/`DEFAULT_LOCALE`/`isLocale`, the `NamespaceTranslations<K>` mapped type for compile-time key completeness, and the `authupError` namespace mapping `@authup/errors` `ErrorCode`s to localized messages (B1: validup-issue-shaped, `IssueDataByCode`-augmented). **Generic UI vocabulary is split across four namespaces** — `ENTITY` (`authupEntity`), `FIELD` (`authupField`), `ACTION` (`authupAction`), `COMMON` (`authupCommon`); every `TranslatorTranslationNamespace` value carries an `authup` prefix (e.g. `ENTITY = 'authupEntity'`, matching the pre-existing `ERROR = 'authupError'`) so a host app embedding `client-web-kit` can't collide with its own catalogs. Per-locale catalog modules mirror the split one-file-per-namespace (`catalogs/{en,de,fr,es}/{entity,field,action,common,client,app,error,vuecs}.ts`; the old `default.ts` is gone). All four locales in `LOCALES` (`en`, `de`, `fr`, `es`) are fully authored in `CATALOGS`; the locale-parity test enforces exact per-namespace key parity across every authored locale, and the `LanguageSwitcherDropdown` UI iterates `LOCALES` (rendering each `nativeName`) so adding a locale to the registry surfaces it in the switcher automatically. **Entity nouns are ilingo plural nodes** (`definePlural({ one, other })` under `authupEntity`): the call site selects the form via `count` (`count: 1` → singular, any other → plural) instead of a separate `*S` key. The `authupMail` namespace (`TranslatorTranslationMailKey`, `catalogs/{en,de,fr,es}/mail.ts`) holds transactional mail copy (subjects, intros, CTA labels, hints; ilingo `{{var}}` placeholders) consumed **server-side** by `apps/server-core`'s mail template renderer. Pure data, zero Vue; consumed by `client-web-kit`'s ilingo install and `server-core`'s mail renderer. |
0 commit comments