@aventara/nest 1.0.0-rc.0 → 1.0.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,314 @@
1
1
  # @aventara/nest
2
2
 
3
+ ## 1.0.0-rc.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 04bb39a: `engines.node` is now `^22.23.4 || >=24.22.1`, the Node security releases of 2026-10-20 (CVE-2026-105481,
8
+ CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106072, CVE-2026-106073, CVE-2026-106074,
9
+ CVE-2026-111497). The bins refuse an older Node in one sentence naming that range.
10
+ - 587dd01: `AventaraModule` mounts `POST {api}/_access/can` on Express and Fastify when the Framework is built with `access`,
11
+ answered by `AvProtocol.bind(framework).handleCan` with the request's state seeded as on every other protocol route. The internal-failure log line names a `can`
12
+ request's failure by its question (`A3000 on access question 2 (orders update) …`) or as `the access questions`.
13
+ - Updated dependencies [587dd01]
14
+ - Updated dependencies [d7e7369]
15
+ - Updated dependencies [7ee547b]
16
+ - Updated dependencies [f52c499]
17
+ - Updated dependencies [2907484]
18
+ - Updated dependencies [a6bbdf0]
19
+ - Updated dependencies [3e26549]
20
+ - Updated dependencies [a6bbdf0]
21
+ - Updated dependencies [6f3f6f6]
22
+ - Updated dependencies [04bb39a]
23
+ - Updated dependencies [d7e7369]
24
+ - Updated dependencies [3fb3333]
25
+ - Updated dependencies [827ccb6]
26
+ - Updated dependencies [7f5abc3]
27
+ - Updated dependencies [88263a1]
28
+ - Updated dependencies [10341d5]
29
+ - Updated dependencies [8129844]
30
+ - Updated dependencies [88263a1]
31
+ - Updated dependencies [3020b5c]
32
+ - Updated dependencies [88263a1]
33
+ - Updated dependencies [88263a1]
34
+ - @aventara/core@1.0.0-rc.10
35
+ - @aventara/mcp@1.0.0-rc.10
36
+
37
+ ## 1.0.0-rc.9
38
+
39
+ ### Patch Changes
40
+
41
+ - 8c40b81: `@aventara/nest` declares a NestJS project's layout in its manifest (`"aventara": { "host": … }`).
42
+ `aventara new` and `aventara init`, which now also scaffold Next.js apps, and `aventara-build` read a NestJS project's
43
+ paths from it, so the three always agree. A NestJS project is scaffolded exactly as before.
44
+ - 205a379: `AventaraModule` hosts a Framework built with `access` (an access provider, from `@aventara/core`): each protocol
45
+ request's `ctx.$state` is seeded by the provider's `state`, called with `{ headers, request }`, the platform
46
+ request's headers as `Headers` and Express's `req` or Fastify's `request`. It works the same through `config`
47
+ (`forRoot({ config: { ...config, access } })`) and `framework`. The module's own `state` option beside `access` is
48
+ refused while Nest builds the application, and is a type error on a `config` that states `access`.
49
+ - Updated dependencies [205a379]
50
+ - Updated dependencies [a0bb05e]
51
+ - Updated dependencies [86ab319]
52
+ - Updated dependencies [fdf52e6]
53
+ - @aventara/core@1.0.0-rc.9
54
+ - @aventara/mcp@1.0.0-rc.9
55
+
56
+ ## 1.0.0-rc.8
57
+
58
+ 1.0.0-rc.5, rc.6 and rc.7 were tagged but never published; 1.0.0-rc.8 contains everything listed under them and the changes below.
59
+
60
+ ### Patch Changes
61
+
62
+ - Updated dependencies [1c8e4ee]
63
+ - @aventara/core@1.0.0-rc.8
64
+ - @aventara/mcp@1.0.0-rc.8
65
+
66
+ ## 1.0.0-rc.7
67
+
68
+ 1.0.0-rc.5 and 1.0.0-rc.6 were tagged but never published; 1.0.0-rc.7 contains everything listed under them and the changes below.
69
+
70
+ ### Patch Changes
71
+
72
+ - Updated dependencies [f917c1c]
73
+ - @aventara/core@1.0.0-rc.7
74
+ - @aventara/mcp@1.0.0-rc.7
75
+
76
+ ## 1.0.0-rc.6
77
+
78
+ 1.0.0-rc.5 was tagged but never published. 1.0.0-rc.6 contains everything listed under 1.0.0-rc.5 and the fix below.
79
+
80
+ ### Patch Changes
81
+
82
+ - Updated dependencies [1cf72e0]
83
+ - @aventara/core@1.0.0-rc.6
84
+ - @aventara/mcp@1.0.0-rc.6
85
+
86
+ ## 1.0.0-rc.5
87
+
88
+ ### Patch Changes
89
+
90
+ - Updated dependencies [4795ff1]
91
+ - Updated dependencies [9e178ca]
92
+ - @aventara/core@1.0.0-rc.5
93
+ - @aventara/mcp@1.0.0-rc.5
94
+
95
+ ## 1.0.0-rc.4
96
+
97
+ ### Patch Changes
98
+
99
+ - Updated dependencies [e3d03a7]
100
+ - Updated dependencies [d10639a]
101
+ - Updated dependencies [f67e68c]
102
+ - Updated dependencies [708556c]
103
+ - Updated dependencies [3a0dfca]
104
+ - Updated dependencies [99169d2]
105
+ - Updated dependencies [0fd2608]
106
+ - Updated dependencies [154e440]
107
+ - Updated dependencies [2fcffdb]
108
+ - @aventara/core@1.0.0-rc.4
109
+ - @aventara/mcp@1.0.0-rc.4
110
+
111
+ ## 1.0.0-rc.3
112
+
113
+ ### Patch Changes
114
+
115
+ - Updated dependencies [b38034d]
116
+ - Updated dependencies [b38034d]
117
+ - Updated dependencies [b38034d]
118
+ - Updated dependencies [b38034d]
119
+ - Updated dependencies [b38034d]
120
+ - Updated dependencies [b38034d]
121
+ - @aventara/core@1.0.0-rc.3
122
+ - @aventara/mcp@1.0.0-rc.3
123
+
124
+ ## 1.0.0-rc.2
125
+
126
+ ### Patch Changes
127
+
128
+ - `AventaraModule.forRoot({ config })` reports a Pick layer that names what its parent hid with the sentence first, as
129
+ `createFramework` does: an inline `client: { exposed: false, exemptions: { resources: { Comment: true } } }` whose
130
+ root hid `Comment` fails at the key with
131
+ `Type 'true' is not assignable to type 'true & (readonly ["Error: client cannot expose 'Comment': hidden by root"] & …)'`.
132
+ - 9366e16: MCP endpoint polish:
133
+
134
+ - A request with no bearer credentials (no `Authorization` header, or another scheme such as `Basic`) is answered
135
+ `401` with `WWW-Authenticate: Bearer resource_metadata="…"` and no error code, as RFC 6750 §3.1 asks; its body no
136
+ longer says "Missing Authorization header". A bad bearer token keeps `error="invalid_token"`.
137
+ - A `mcp.authorization.verify` that throws is still answered `401`, and is now reported to the `diagnostics` sink as
138
+ an `AvMcpDiagnostic` holding the error.
139
+ - With `mcp` configured and `@aventara/mcp` missing, the startup error names the command that installs it at the
140
+ installed `@aventara/core`'s exact version: `npm install --save-exact @aventara/mcp@<version>`.
141
+
142
+ - Updated dependencies [9366e16]
143
+ - Updated dependencies [9366e16]
144
+ - Updated dependencies [9366e16]
145
+ - Updated dependencies [9366e16]
146
+ - @aventara/core@1.0.0-rc.2
147
+ - @aventara/mcp@1.0.0-rc.2
148
+
149
+ ## 1.0.0-rc.1
150
+
151
+ ### Patch Changes
152
+
153
+ - 7e3d06f: Breaking: the configuration has a new shape. Every layer (the root, `application` and `client`) now holds the
154
+ same six keys: `exposed`, `transactionsStyle`, `limits`, `pipelines`, `resources` and `exemptions`. Everything about
155
+ one Resource lives under `resources.<Resource>`: its fields, and each field's `definition` and `pipelines`. The old
156
+ keys are refused when the framework starts, with a message that names where each one moved:
157
+
158
+ | Before (rc.0) | Now (rc.1) |
159
+ | ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
160
+ | `fields: { Post: { url: { type, nullable, … } } }` | `resources: { Post: { fields: { url: { definition: { type, nullable, … } } } } }` |
161
+ | `behaviors: { Post: { fields: { slug: { create } } } }` | `resources: { Post: { fields: { slug: { pipelines: { compute: { create } } } } } }` |
162
+ | `transactions: "none"` | `transactionsStyle: "none"` |
163
+ | `restrictions` | `exposed` + `exemptions` (see below) |
164
+ | `@aventara/core/json` | `@aventara/core/avz` |
165
+
166
+ Before:
167
+
168
+ ```ts
169
+ createFramework({
170
+ api: "/api",
171
+ adapter,
172
+ transactions: "interactive",
173
+ fields: {
174
+ Post: {
175
+ url: {
176
+ type: { scalar: "string" },
177
+ nullable: false,
178
+ list: false,
179
+ lifecycle: ["VIRTUAL"],
180
+ },
181
+ },
182
+ },
183
+ behaviors: {
184
+ Post: {
185
+ fields: {
186
+ url: {
187
+ read: {
188
+ dependsOn: ["slug"],
189
+ compute: ({ record }) => `/p/${record.slug}`,
190
+ },
191
+ },
192
+ },
193
+ },
194
+ },
195
+ });
196
+ ```
197
+
198
+ After:
199
+
200
+ ```ts
201
+ createFramework({
202
+ api: "/api",
203
+ adapter,
204
+ transactionsStyle: "interactive",
205
+ resources: {
206
+ Post: {
207
+ fields: {
208
+ url: {
209
+ definition: {
210
+ type: { scalar: "string" },
211
+ nullable: false,
212
+ list: false,
213
+ lifecycle: ["VIRTUAL"],
214
+ },
215
+ pipelines: {
216
+ compute: {
217
+ read: {
218
+ dependsOn: ["slug"],
219
+ compute: ({ record }) => `/p/${record.slug}`,
220
+ },
221
+ },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ },
227
+ });
228
+ ```
229
+
230
+ What else changed:
231
+
232
+ - A field is defined at the root only. A `definition` in `application` or `client` is refused
233
+ (`CONFIG_FORBIDDEN_IN_OVERLAY`). Define the field
234
+ at the root; `application` and `client` can still add compute stages to it.
235
+ - Computed values chain. When the root and an overlay both compute a field, the root's stage runs first and the
236
+ overlay's receives its result as `value`. A stage can no longer be turned off with `false`. Returning `undefined`
237
+ keeps the current value. An upsert computes its create branch with `compute.create` and its update branch with
238
+ `compute.update`.
239
+ - A discovered field keeps the adapter's facts. On a field the adapter discovered you may restate its own `type`,
240
+ `nullable` and `list`, and give a Json field its shape (`definition.json`). Setting a different type, any `format`
241
+ or any `capabilities` on it is refused. `format` is for fields you add, and it must be one of zod's named string
242
+ formats (`"email"`, `"url"`, `"uuid"`, …); any other name is a type error and refused at startup.
243
+ - A misplaced key is a type error. In a configuration written inline in `createFramework({ ... })` or
244
+ `AventaraModule.forRoot`, a key that has no place there (a typo, an old key, `compute` on a relation field, a field
245
+ name neither your schema nor the root has) is now a type error at that key, and still refused at startup.
246
+ - A Resource whose name starts with `$` is refused at startup (`$` names are the framework's).
247
+ - The Json shape builder moved from `@aventara/core/json` to `@aventara/core/avz`. Change the import; `AvZ` is
248
+ the same.
249
+
250
+ `exposed` and `exemptions`, which replace `restrictions`, have their own entry.
251
+
252
+ - 4907afc: New: `AventaraModule.forRoot` and `forRootAsync` take a `state` option that seeds each protocol request's
253
+ `ctx.$state` (see `@aventara/core`'s changelog). It is called once per request with the platform's own request, so
254
+ what your middleware set there reaches every guard, pipe, hook and interceptor of the request:
255
+
256
+ ```ts
257
+ AventaraModule.forRoot({
258
+ config,
259
+ state: (req: Request) => ({ user: req.user }), // Passport's req.user, on Express
260
+ });
261
+ ```
262
+
263
+ On Express the request is Express's `req`. On Fastify it is Fastify's `request`; middleware added with `app.use`
264
+ writes to its Node request, `request.raw`, so read `request.raw.user` there. Fastify answers a protocol route in its
265
+ `onRequest` hook, so only what runs before that (`app.use` middleware, an earlier `onRequest` hook) can be read. A
266
+ `state` function that throws an `AvError` answers that error (`AvError("A4001", …)` is a `403`); any other throw is
267
+ `500 A3000`. A request the protocol refuses before running (a stale client, an unknown route) never calls it. A
268
+ `state` that is not a function is refused while Nest builds the application.
269
+
270
+ - Breaking: the configuration's mount path is `api` (it was `entrypoint`), and the module reads `framework.api`.
271
+ `forRoot({ config: { api: "/api", adapter } })`. The Fastify refusal of a path with `*` now says "Change `api`, or
272
+ host on Express."
273
+
274
+ - New: with `mcp` in the configuration, the module mounts the MCP endpoint of `@aventara/mcp` at that path (and, with
275
+ `mcp.authorization`, its OAuth metadata at `/.well-known/oauth-protected-resource` plus the path), on Express and on
276
+ Fastify, as it mounts the protocol's routes: the body read raw, the request's URL and peer address handed over, the
277
+ answer written as it is. The `state` option seeds each tool call's `ctx.$state`. `@aventara/mcp` is an optional peer
278
+ dependency, loaded only when `mcp` is configured; with `mcp` set and the package missing, the application refuses to
279
+ start and names it.
280
+
281
+ - Fixed on Express: a request whose path matched a mounted route only because Express ignores case reached the route
282
+ without its body, and was answered as if it had sent none. Express matches routes case-insensitively by default,
283
+ while the body reader compared the path exactly. The MCP routes now answer their own path only, exactly as written,
284
+ so `/MCP` or `/mcp/` goes on to Nest's `404`, as on Fastify. The protocol's routes keep Express's matching (it also
285
+ answers the protocol under `/API` for `api: "/api"`), and the body reader now matches the `api` path the same way, so
286
+ `POST /API/_resources/…` gets its body.
287
+
288
+ - The host's log line for an internal failure names the MCP endpoint when the endpoint itself failed
289
+ (`A3000 on the MCP endpoint (agent origin, request …)`).
290
+
291
+ - Updated dependencies [800123b]
292
+ - Updated dependencies [8bb1d6a]
293
+ - Updated dependencies [68d4395]
294
+ - Updated dependencies [7e3d06f]
295
+ - Updated dependencies [1b5d3c0]
296
+ - Updated dependencies [6317811]
297
+ - Updated dependencies [6d0e5d6]
298
+ - Updated dependencies [7a0cab9]
299
+ - Updated dependencies [eb2a718]
300
+ - Updated dependencies [03312c5]
301
+ - Updated dependencies [123f6dc]
302
+ - Updated dependencies [b65df2e]
303
+ - Updated dependencies [c6e5010]
304
+ - Updated dependencies [c1f9389]
305
+ - Updated dependencies [9c52482]
306
+ - Updated dependencies [8396148]
307
+ - Updated dependencies [e8fbb02]
308
+ - Updated dependencies [916e5f6]
309
+ - Updated dependencies [6cbd158]
310
+ - @aventara/core@1.0.0-rc.1
311
+
3
312
  ## 1.0.0-rc.0
4
313
 
5
314
  ### Major Changes
package/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # `@aventara/nest`
2
2
 
3
- Hosts an Aventara `Framework` in a NestJS application. It does one thing: it mounts the framework's HTTP protocol —
4
- `AvProtocol.bind(framework)` from `@aventara/core/protocol` — on Nest's HTTP platform, and puts the `Framework` into
3
+ Hosts an Aventara `AvFramework` in a NestJS application. It mounts the framework's HTTP protocol
4
+ (`AvProtocol.bind(framework)` from `@aventara/core/protocol`) on Nest's HTTP platform, and puts the `AvFramework` into
5
5
  Nest's dependency injection. Every rule of the protocol (routes, decoding, statuses, envelopes, headers, absence) is
6
6
  core's; this package only translates a Nest request into the protocol's request and writes the protocol's answer back.
7
7
 
8
- - **Nest 12** (`@nestjs/common` and `@nestjs/core` `^12.0.0`), on **Express** (Nest's default) or **Fastify**.
9
- - `@aventara/core` is its one dependency.
8
+ It runs on Nest 12 (`@nestjs/common` and `@nestjs/core` `^12.0.0`), on Express (Nest's default) or Fastify.
9
+ `@aventara/core` is its one dependency.
10
10
 
11
11
  ## Mounting it
12
12
 
13
- `AventaraModule` is a global module, imported once in your root module. It takes the `Framework` in one of four forms.
13
+ `AventaraModule` is a global module, imported once in your root module. It takes the `AvFramework` in one of four forms.
14
14
 
15
15
  ```ts
16
16
  // src/app.module.ts — the shape `aventara init` writes (`@aventara/cli`)
@@ -33,14 +33,27 @@ import { PrismaModule, PrismaService } from "./prisma.service.js";
33
33
  })
34
34
  export class AppModule {}
35
35
 
36
- // src/aventara.config.ts — the framework's configuration: entrypoint, restrictions, pipelines
36
+ // src/aventara.config.ts — the framework's configuration: api, exposure, pipelines, resources
37
+ import type { AvDiagnostic } from "@aventara/core";
37
38
  import { createPrismaAdapter } from "@aventara/prisma7-adapter";
39
+ import { Logger } from "@nestjs/common";
38
40
  import { discovery } from "./generated/aventara/discovery.artifact.js";
39
41
  import type { PrismaService } from "./prisma.service.js";
40
42
 
43
+ const logger = new Logger("Aventara");
44
+
41
45
  export async function aventaraConfig(prisma: PrismaService) {
42
46
  return {
43
- entrypoint: "/api",
47
+ api: "/api",
48
+ exposed: true,
49
+ // The raw error of a server failure, with its stack, in Nest's log.
50
+ diagnostics: (diagnostic: AvDiagnostic) => {
51
+ const { error } = diagnostic;
52
+ logger.error(
53
+ `${diagnostic.code} (request ${diagnostic.requestId}): raw error`,
54
+ (error instanceof Error && error.stack) || String(error),
55
+ );
56
+ },
44
57
  adapter: await createPrismaAdapter({
45
58
  client: prisma,
46
59
  discovery,
@@ -53,15 +66,18 @@ export async function aventaraConfig(prisma: PrismaService) {
53
66
 
54
67
  | Form | When |
55
68
  |---|---|
56
- | `AventaraModule.forRoot({ framework })` | You called `createFramework(config)` yourself and want your own typed `Framework<M, Cfg>` in code. |
69
+ | `AventaraModule.forRoot({ framework })` | You called `createFramework(config)` yourself and want your own typed `AvFramework<M, Cfg>` in code. |
57
70
  | `AventaraModule.forRoot({ config })` | The module calls `createFramework(config)` for you. |
58
71
  | `AventaraModule.forRootAsync({ imports, inject, useFactory })` resolving `{ framework }` or `{ config }` | What builds the Framework (a Prisma client, a configuration service) is itself a provider. |
59
72
 
60
- The `config` form is typed exactly like `createFramework`'s parameter: an annotated `FrameworkConfig<M>`, an inline
73
+ Each form also takes `state`, which seeds every protocol request's `ctx.$state` from the platform request (see
74
+ *Request state*).
75
+
76
+ The `config` form is typed exactly like `createFramework`'s parameter: an annotated `AvConfig<M>`, an inline
61
77
  literal, an inferred constant and a `satisfies` check are all accepted, and checked against your Adapter's model.
62
78
 
63
- **Injecting it.** `@InjectFramework()` (or `@Inject(AVENTARA_FRAMEWORK)`) injects the `Framework`. DI carries no type
64
- argument, so type the parameter with your own alias:
79
+ `@InjectFramework()` (or `@Inject(AVENTARA_FRAMEWORK)`) injects the `AvFramework`. DI carries no type argument, so type
80
+ the parameter with your own alias:
65
81
 
66
82
  ```ts
67
83
  export type AppFramework = Awaited<ReturnType<typeof buildFramework>>;
@@ -72,23 +88,23 @@ export class ReportsService {
72
88
  }
73
89
  ```
74
90
 
75
- **Fails before listen.** These reject while Nest builds the application, before it listens: both or neither of
76
- `framework` and `config`; a `framework` value that is not a Framework; the module mounted twice in one application; a
77
- platform other than Express or Fastify (or none, as in `createApplicationContext`); a `FrameworkConstructionError`,
78
- yours or the module's own, which propagates unchanged. Nest's default `abortOnError: true` **exits the process** on a
79
- bootstrap error instead of rejecting; pass `abortOnError: false` to `NestFactory.create` to catch it.
91
+ These reject while Nest builds the application, before it listens: both or neither of `framework` and `config`; a
92
+ `framework` value that is not a Framework; a `state` option that is not a function; the module mounted twice in one application; a platform other than Express
93
+ or Fastify (or none, as in `createApplicationContext`); an `AvConstructionError`, yours or the module's own, which
94
+ propagates unchanged. Nest's default `abortOnError: true` exits the process on a bootstrap error instead of rejecting;
95
+ pass `abortOnError: false` to `NestFactory.create` to catch it.
80
96
 
81
- **Nest's testing module works.** `Test.createTestingModule({ imports: [AppModule] }).compile()` builds the providers
82
- before any HTTP platform exists; the module waits for the one `createNestApplication()` supplies, and still refuses an
83
- unsupported platform before the application listens. A testing module initialized with no
84
- application at all is refused at `init()`, as a platform of `none`.
97
+ Nest's testing module works. `Test.createTestingModule({ imports: [AppModule] }).compile()` builds the providers before
98
+ any HTTP platform exists; the module waits for the one `createNestApplication()` supplies, and still refuses an
99
+ unsupported platform before the application listens. A testing module initialized with no application at all is
100
+ refused at `init()`, as a platform of `none`.
85
101
 
86
- **Nothing to shut down.** The module owns no resource. Your Prisma provider closes its own client — in its
102
+ The module owns no resource, so there is nothing to shut down. Your Prisma provider closes its own client in its
87
103
  `onModuleDestroy`, with `app.enableShutdownHooks()` for signals.
88
104
 
89
105
  ## Calling it by hand
90
106
 
91
- Against the shape `aventara init` writes — the entrypoint `/api`, a Prisma `User` model:
107
+ Against the shape `aventara init` writes (the `api` mount path `/api`, a Prisma `User` model):
92
108
 
93
109
  <!-- pilot-gate:curl -->
94
110
  ```bash
@@ -108,18 +124,19 @@ curl -s -X POST http://localhost:3000/api/_resources/User/find/many \
108
124
  Leave out an identity header and the answer is `400 A2000`, naming the header that is missing. The hash changes
109
125
  whenever the schema or the configuration does: fetch it again, and regenerate the client.
110
126
 
111
- - **Resource keys are the model names, as written.** `model User` is `User` everywhere: on the wire
112
- (`/_resources/User/find/many`) and in the generated client (`avClient.User.find.many({})`). Nothing is renamed,
113
- lower-cased or pluralized.
114
- - **Results are read-only.** A list result is a `readonly` array: type it `readonly User[]`, not `User[]` —
115
- `const users: readonly User[] = await avClient.User.find.many({});`.
127
+ Resource keys are the model names, as written. `model User` is `User` everywhere: on the wire
128
+ (`/_resources/User/find/many`) and in the generated client (`avClient.User.find.many({})`). Nothing is renamed,
129
+ lower-cased or pluralized.
130
+
131
+ Results are read-only. A list result is a `readonly` array, so type it `readonly User[]`:
132
+ `const users: readonly User[] = await avClient.User.find.many({});`.
116
133
 
117
134
  ## What runs on a protocol route
118
135
 
119
- The protocol's routes are platform routes, registered from the module's `configure()`, one per
120
- `protocol.surface()` entry — every advertised operation, `GET <entrypoint>/_contract`, and `POST
121
- <entrypoint>/_transactions` only when the ClientContract advertises `interactive` — plus the absence handler for the
122
- rest of `<entrypoint>/_*`. Nest's `RouterExplorer` does not list platform routes, so the module logs one line of its
136
+ The protocol's routes are platform routes, registered from the module's `configure()`. There is one per
137
+ `protocol.surface()` entry (every advertised operation, `GET <api>/_contract`, and `POST
138
+ <api>/_transactions` only when the ClientContract advertises `interactive`), plus the absence handler for the
139
+ rest of `<api>/_*`. Nest's `RouterExplorer` does not list platform routes, so the module logs one line of its
123
140
  own when it mounts them, through Nest's `Logger` (context `AventaraModule`; `logger: false` silences it like any other):
124
141
 
125
142
  ```text
@@ -127,16 +144,19 @@ own when it mounts them, through Nest's `Logger` (context `AventaraModule`; `log
127
144
  ```
128
145
 
129
146
  An internal failure (`A3000`–`A3004`, answered as a 500) is logged as one error line through Nest's `Logger`
130
- (context `Aventara`): the code, the operation, the origin and the request id, and the thrown value's class name — never
131
- its message, which can hold SQL or a connection string. The raw error still goes to your `diagnostics` sink, when you
132
- configure one:
147
+ (context `Aventara`): the code, the operation, the origin and the request id, and the thrown value's class name. It
148
+ never logs the message, which can hold SQL or a connection string. The raw error still goes to your `diagnostics` sink, when you
149
+ configure one. The `aventara.config.ts` that `aventara new` and `aventara init` write has one: it logs the raw error with its
150
+ stack through the same `Logger`, at error level, right after this line.
133
151
 
134
152
  ```text
135
153
  [Nest] ERROR [Aventara] A3001 on Post.create.one (client origin, request 3f1c…): PrismaClientValidationError. The raw error goes to the framework's diagnostics sink.
154
+ [Nest] ERROR [Aventara] A3001 (request 3f1c…): raw error
155
+ PrismaClientValidationError: …
136
156
  ```
137
157
 
138
- For `A3004` — one of your pipes returned arguments the server contract refuses — the line also names the pipe, the
139
- operation, the offending field and its V-code; the caller only ever sees the generic message.
158
+ For `A3004` (one of your pipes returned arguments the server contract refuses), the line also names the pipe, the
159
+ operation, the offending field and its V-code. The caller only ever sees the generic message.
140
160
 
141
161
  The line is added when the module builds the framework from `config`; a framework you build yourself and pass as
142
162
  `framework` keeps exactly the `diagnostics` sink you gave it.
@@ -146,42 +166,107 @@ The framework's validation is outermost:
146
166
  | | Express | Fastify |
147
167
  |---|---|---|
148
168
  | Your `app.use(...)` middleware, `app.enableCors()`, helmet | runs | runs |
149
- | Nest guards, interceptors, pipes (global or not) | **never** | **never** |
150
- | `MiddlewareConsumer` middleware | **never** | **runs** — Fastify runs Nest middleware before routing; add `.exclude("api/_*path")` (your entrypoint) to keep it off |
151
- | Your controller at `<entrypoint>/_contract` (or any `_` path) | never reached | the application does not start |
152
- | Your controller at `<entrypoint>/health` (outside `_`) | reached | reached |
169
+ | Nest guards, interceptors, pipes (global or not) | never | never |
170
+ | `MiddlewareConsumer` middleware | never | runs, because Fastify runs Nest middleware before routing; add `.exclude("api/_*path")` (your `api` path) to keep it off |
171
+ | Your controller at `<api>/_contract` (or any `_` path) | never reached | the application does not start |
172
+ | Your controller at `<api>/health` (outside `_`) | reached | reached |
173
+
174
+ Authorization therefore belongs in Aventara pipelines (guards). A guard reads the request's headers as
175
+ `ctx.$transport.headers` (lower-cased names, repeats joined), and whatever your middleware found as `ctx.$state`,
176
+ through the module's `state` option (next section).
177
+
178
+ ## Request state
179
+
180
+ `state` seeds each protocol request's `ctx.$state`, the one object every stage of that request reads and writes. It is
181
+ called once per request, with the platform's own request, after the protocol has accepted it (a stale client or an
182
+ unknown route never calls it):
183
+
184
+ ```ts
185
+ declare module "@aventara/core" {
186
+ interface AvState {
187
+ user?: { id: string; roles: string[] };
188
+ }
189
+ }
190
+
191
+ AventaraModule.forRoot({
192
+ config,
193
+ state: (req: Request) => ({ user: req.user }), // what your auth middleware set
194
+ });
153
195
 
154
- Authentication and authorization therefore belong in Aventara **pipelines** (guards), which read
155
- `transport.headers` — lower-cased names, repeats joined. A Passport strategy run as Nest middleware sets `req.user`,
156
- which a pipeline cannot see: verify the bearer token or cookie in the Aventara guard instead.
196
+ // In the configuration: a guard reads it.
197
+ const signedIn: AvGuard = (ctx) => ctx.$state.user !== undefined;
198
+ ```
157
199
 
158
- **Where it is mounted.** `FrameworkConfig.entrypoint` alone decides it. `app.setGlobalPrefix(...)` does not move the
159
- protocol; set the entrypoint to where you want it, and point the generated client at the same URL.
200
+ - `forRootAsync`'s `useFactory` returns `state` beside `framework` or `config`.
201
+ - On Express, `req` is Express's request. On Fastify it is Fastify's `request`; `app.use` middleware writes to its
202
+ Node request, so read `request.raw.user`. Fastify answers a protocol route from its `onRequest` hook, so only what
203
+ runs before it (`app.use` middleware, an earlier `onRequest` hook) has run.
204
+ - Use `app.use(...)` for the middleware that finds the user (a Passport `authenticate` call, a session or JWT check).
205
+ Nest guards never run on protocol routes, and `MiddlewareConsumer` middleware does not on Express (the table above).
206
+ - A Framework built with `access` (an access provider, see `@aventara/core`) seeds the state itself: the module calls
207
+ the provider's `state` with `{ headers, request }`, the platform request's headers and the request. Pass no `state`
208
+ beside it; the module refuses the pair while Nest builds the application, and as a type error on a `config` that
209
+ states `access`. It works the same through `config` and `framework`:
210
+ `AventaraModule.forRoot({ config: { ...config, access } })`.
211
+ - Throw `new AvError("A4001", "…")` from `state` to refuse the request (`403`); any other throw is `500 A3000`, with the
212
+ error in your `diagnostics` sink.
213
+ - A plan sent to `/_transactions` gets one state for all its steps and transaction stages. A ctx call a stage makes
214
+ inherits it. Compute reads it but cannot write it.
215
+ - Server code that calls the framework in process seeds it per call:
216
+ `framework.application.Post.create.one(args, { state: { user } })`.
217
+
218
+ `AvConfig.api` alone decides where the protocol is mounted. `app.setGlobalPrefix(...)` does not move it; set
219
+ `api` to where you want it, and point the generated client at the same URL.
160
220
 
161
221
  ## Requests
162
222
 
163
- - **Bodies** on protocol routes are read raw — never through Nest's body parser — up to `limits.maxRequestBytes`; the
223
+ - Bodies on protocol routes are read raw, never through Nest's body parser, up to `limits.maxRequestBytes`. The
164
224
  protocol's decoder answers an oversized body (`413 A2010`), another content type (`415 A2011`) and malformed JSON
165
225
  (`400 A2000`). Your own routes keep Nest's parsed `req.body`. No `NestFactory.create` option is needed.
166
- - **`Content-Encoding`:** `gzip`, `deflate` and `br` are inflated, and the limit counts decoded bytes. Any other coding
226
+ - For `Content-Encoding`, `gzip`, `deflate` and `br` are inflated, and the limit counts decoded bytes. Any other coding
167
227
  answers `415 A2011`; a body that does not decode in the coding it names answers `400 A2000`. These two answers carry a
168
228
  newly minted `Aventara-Request-Id`, not the client's.
169
- - **The request id** is the client's `Aventara-Request-Id` as it arrived, or one core mints. A middleware that rewrites
170
- `req.headers` cannot change it, and `X-Request-Id` is never copied into it — set `Aventara-Request-Id` at your proxy.
171
- - **A query string** is not part of the protocol path; it is ignored.
229
+ - The request id is the client's `Aventara-Request-Id` as it arrived, or one core mints. A middleware that rewrites
230
+ `req.headers` cannot change it, and `X-Request-Id` is never copied into it, so set `Aventara-Request-Id` at your
231
+ proxy.
232
+ - A query string is not part of the protocol path; it is ignored.
233
+
234
+ ## The MCP endpoint
235
+
236
+ With `mcp` in the configuration, the module also mounts the MCP server of `@aventara/mcp` at that path, on Express and
237
+ Fastify alike, and logs `Aventara mounted the MCP endpoint at /mcp (…)`. Install it beside this package, at
238
+ `@aventara/core`'s exact version:
239
+
240
+ ```bash
241
+ npm install --save-exact @aventara/mcp@<your @aventara/core version>
242
+ ```
243
+
244
+ It is an optional peer dependency: an application without `mcp` does not need it, and one with `mcp` and without the
245
+ package refuses to start, naming it and the command, with the version filled in:
246
+
247
+ ```text
248
+ AventaraModule: the configuration has `mcp`, and the package that serves it, @aventara/mcp, is not installed. Install it at @aventara/core's exact version (npm install --save-exact @aventara/mcp@1.0.0-rc.10), or remove `mcp` from the configuration.
249
+ ```
250
+ As for the protocol's routes, the module only mounts: the endpoint's body is read
251
+ raw, the request's URL comes from the platform's `protocol` and `host` (set `trust proxy` on Express or `trustProxy` on
252
+ Fastify behind a proxy), and every answer is `@aventara/mcp`'s. The `state` option seeds each tool call's `ctx.$state`.
253
+ See `@aventara/mcp`'s README for the tools, the contract resource and authorization.
172
254
 
173
255
  ## Platform differences
174
256
 
175
- - **Fastify cannot mount some Resource keys or entrypoints as literal routes.** A Resource key that needs `*` or a
176
- reserved character (`: + # $ & , / ; = ? @`) in its route path, or an entrypoint containing `*`, is refused at startup
257
+ - Fastify cannot mount some Resource keys or `api` paths as literal routes. A Resource key that needs `*` or a
258
+ reserved character (`: + # $ & , / ; = ? @`) in its route path, or an `api` path containing `*`, is refused at startup
177
259
  with a message naming it. Rename it, or host on Express, which mounts every key.
178
- - **Express routing is case-insensitive** by default, so it also answers the protocol under, say, `/API/_contract`;
179
- Fastify's is not. That is Express's own `case sensitive routing` setting, off by default, and yours to change.
180
- - **Express answers `500 A3000`** to a protocol route requested with a percent-encoding a generated client never sends
181
- (for example `%6Eotes` for `notes`); Fastify and the in-process protocol execute it. A known divergence.
260
+ - Express routing is case-insensitive by default, so it also answers the protocol under, say, `/API/_contract`;
261
+ Fastify's is not. That is Express's own `case sensitive routing` setting, off by default, and yours to change. The
262
+ MCP endpoint is the exception: on both platforms it answers its own path only, exactly as written (`/mcp`, not
263
+ `/MCP` or `/mcp/`).
264
+ - Express answers `500 A3000` to a protocol route requested with a percent-encoding a generated client never sends
265
+ (for example `%6Eotes` for `notes`); Fastify and the in-process protocol execute it. This is a known divergence.
182
266
  - On Express the response carries Express's `X-Powered-By` header; `app.disable("x-powered-by")` removes it.
183
267
 
184
268
  ## License
185
269
 
186
- PolyForm Shield 1.0.0 with an additional permission — free to use, including in commercial applications; you may not
187
- use it to build a product that competes with Aventara itself. See `LICENSE` and `LICENSE-ADDITIONAL-PERMISSION.md`.
270
+ PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
271
+ may not use it to build a product that competes with Aventara itself. See `LICENSE` and
272
+ `LICENSE-ADDITIONAL-PERMISSION.md`.
@@ -0,0 +1,7 @@
1
+ import type { AvFramework } from "@aventara/core";
2
+ import type { HostState } from "./protocol-host.factory.js";
3
+ /**
4
+ * The state seed of `framework`'s access provider, over a platform request;
5
+ * `undefined` for a Framework built without `access`.
6
+ */
7
+ export declare function accessStateSeed(framework: AvFramework): HostState | undefined;
@@ -0,0 +1,20 @@
1
+ import { accessProviderOf } from "@aventara/core/internal";
2
+ function headersOf(request) {
3
+ const headers = new Headers();
4
+ const given = request?.headers;
5
+ for (const [name, value] of Object.entries(given ?? {})) {
6
+ if (value === undefined || name.startsWith(":")) {
7
+ continue;
8
+ }
9
+ for (const one of typeof value === "string" ? [value] : value) {
10
+ headers.append(name, one);
11
+ }
12
+ }
13
+ return headers;
14
+ }
15
+ export function accessStateSeed(framework) {
16
+ const state = accessProviderOf(framework)?.state;
17
+ return state === undefined
18
+ ? undefined
19
+ : (request) => state({ headers: headersOf(request), request });
20
+ }