@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 +309 -0
- package/README.md +143 -58
- package/dist/access-state.translator.d.ts +7 -0
- package/dist/access-state.translator.js +20 -0
- package/dist/aventara.module.d.ts +30 -8
- package/dist/aventara.module.js +19 -6
- package/dist/express-protocol.mounter.d.ts +14 -4
- package/dist/express-protocol.mounter.js +35 -6
- package/dist/fastify-protocol.mounter.d.ts +13 -4
- package/dist/fastify-protocol.mounter.js +29 -5
- package/dist/framework.token.d.ts +1 -1
- package/dist/host-bootstrap.validator.js +16 -2
- package/dist/internal-failure.logger.d.ts +3 -3
- package/dist/internal-failure.logger.js +8 -2
- package/dist/mcp-binding.loader.d.ts +30 -0
- package/dist/mcp-binding.loader.js +39 -0
- package/dist/node-exchange.translator.d.ts +6 -0
- package/dist/node-exchange.translator.js +9 -0
- package/dist/protocol-host.factory.d.ts +7 -18
- package/dist/protocol-host.factory.js +22 -11
- package/dist/protocol-route.table.d.ts +12 -1
- package/dist/protocol-route.table.js +7 -2
- package/package.json +32 -11
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 `
|
|
4
|
-
`AvProtocol.bind(framework)` from `@aventara/core/protocol`
|
|
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
|
-
|
|
9
|
-
|
|
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 `
|
|
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:
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
76
|
-
`framework`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
unsupported platform before the application listens. A testing module initialized with no
|
|
84
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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()
|
|
120
|
-
`protocol.surface()` entry
|
|
121
|
-
<
|
|
122
|
-
rest of `<
|
|
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
|
|
131
|
-
|
|
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`
|
|
139
|
-
operation, the offending field and its V-code
|
|
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) |
|
|
150
|
-
| `MiddlewareConsumer` middleware |
|
|
151
|
-
| Your controller at `<
|
|
152
|
-
| Your controller at `<
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
196
|
+
// In the configuration: a guard reads it.
|
|
197
|
+
const signedIn: AvGuard = (ctx) => ctx.$state.user !== undefined;
|
|
198
|
+
```
|
|
157
199
|
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
170
|
-
`req.headers` cannot change it, and `X-Request-Id` is never copied into it
|
|
171
|
-
|
|
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
|
-
-
|
|
176
|
-
reserved character (`: + # $ & , / ; = ? @`) in its route path, or an
|
|
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
|
-
-
|
|
179
|
-
Fastify's is not. That is Express's own `case sensitive routing` setting, off by default, and yours to change.
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
187
|
-
use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
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
|
+
}
|