@aventara/nest 1.0.0-rc.1 → 1.0.0-rc.11
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 +173 -1
- package/README.md +16 -6
- package/dist/access-state.translator.d.ts +7 -0
- package/dist/access-state.translator.js +20 -0
- package/dist/aventara.module.d.ts +15 -4
- package/dist/aventara.module.js +2 -1
- package/dist/express-protocol.mounter.d.ts +1 -1
- package/dist/express-protocol.mounter.js +6 -5
- package/dist/fastify-protocol.mounter.js +7 -6
- package/dist/host-bootstrap.validator.js +9 -0
- package/dist/internal-failure.logger.js +5 -1
- package/dist/mcp-binding.loader.js +16 -2
- package/dist/node-exchange.translator.d.ts +29 -5
- package/dist/node-exchange.translator.js +82 -3
- package/dist/protocol-host.factory.d.ts +3 -24
- package/dist/protocol-host.factory.js +3 -3
- package/dist/protocol-route.table.d.ts +8 -3
- package/dist/protocol-route.table.js +4 -0
- package/package.json +27 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,178 @@
|
|
|
1
1
|
# @aventara/nest
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e0e89c1: `POST {api}/_all` now streams when the client asks for it: the hosts forward `Accept`, so with
|
|
8
|
+
`application/x-ndjson` each entry's line is written as it settles (Nest on Express and Fastify writes it to the response,
|
|
9
|
+
flushed through `compression` when present; Next returns a `ReadableStream`), and plain JSON stays the answer otherwise.
|
|
10
|
+
When the client disconnects, the set stops: entries not yet started never start.
|
|
11
|
+
- 186bc29: `POST {api}/_all` is mounted with the other protocol routes and answered by the binding's `handleAll`.
|
|
12
|
+
- 33b5f83: The hosts hand the request URL's query to the protocol, so a cached read's `GET` (a read with a `cache({ for, maxAge })` stage)
|
|
13
|
+
is mounted and answered like every other route in `protocol.surface()`, on Express, Fastify and Next.js.
|
|
14
|
+
- bedab57: A cached read's `Vary` joins the `Vary` the platform already set instead of replacing it. With `enableCors()` on, a
|
|
15
|
+
`for: "anonymous"` or `"user"` answer carries `Vary: Origin, Authorization, Cookie` on Express and Fastify, where it
|
|
16
|
+
carried only `Vary: Authorization, Cookie` and dropped CORS's `Vary: Origin`, so a CDN could serve one origin's
|
|
17
|
+
`Access-Control-Allow-Origin` to another. Each name appears once, compared without case, the platform's first.
|
|
18
|
+
- Updated dependencies [6197c08]
|
|
19
|
+
- Updated dependencies [e0e89c1]
|
|
20
|
+
- Updated dependencies [186bc29]
|
|
21
|
+
- Updated dependencies [33b5f83]
|
|
22
|
+
- Updated dependencies [6d8b589]
|
|
23
|
+
- Updated dependencies [11ece49]
|
|
24
|
+
- Updated dependencies [92fd093]
|
|
25
|
+
- Updated dependencies [e5e98e6]
|
|
26
|
+
- Updated dependencies [a05d970]
|
|
27
|
+
- @aventara/core@1.0.0-rc.11
|
|
28
|
+
- @aventara/mcp@1.0.0-rc.11
|
|
29
|
+
|
|
30
|
+
## 1.0.0-rc.10
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 04bb39a: `engines.node` is now `^22.23.4 || >=24.22.1`, the Node security releases of 2026-10-20 (CVE-2026-105481,
|
|
35
|
+
CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106072, CVE-2026-106073, CVE-2026-106074,
|
|
36
|
+
CVE-2026-111497). The bins refuse an older Node in one sentence naming that range.
|
|
37
|
+
- 587dd01: `AventaraModule` mounts `POST {api}/_access/can` on Express and Fastify when the Framework is built with `access`,
|
|
38
|
+
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`
|
|
39
|
+
request's failure by its question (`A3000 on access question 2 (orders update) …`) or as `the access questions`.
|
|
40
|
+
- Updated dependencies [587dd01]
|
|
41
|
+
- Updated dependencies [d7e7369]
|
|
42
|
+
- Updated dependencies [7ee547b]
|
|
43
|
+
- Updated dependencies [f52c499]
|
|
44
|
+
- Updated dependencies [2907484]
|
|
45
|
+
- Updated dependencies [a6bbdf0]
|
|
46
|
+
- Updated dependencies [3e26549]
|
|
47
|
+
- Updated dependencies [a6bbdf0]
|
|
48
|
+
- Updated dependencies [6f3f6f6]
|
|
49
|
+
- Updated dependencies [04bb39a]
|
|
50
|
+
- Updated dependencies [d7e7369]
|
|
51
|
+
- Updated dependencies [3fb3333]
|
|
52
|
+
- Updated dependencies [827ccb6]
|
|
53
|
+
- Updated dependencies [7f5abc3]
|
|
54
|
+
- Updated dependencies [88263a1]
|
|
55
|
+
- Updated dependencies [10341d5]
|
|
56
|
+
- Updated dependencies [8129844]
|
|
57
|
+
- Updated dependencies [88263a1]
|
|
58
|
+
- Updated dependencies [3020b5c]
|
|
59
|
+
- Updated dependencies [88263a1]
|
|
60
|
+
- Updated dependencies [88263a1]
|
|
61
|
+
- @aventara/core@1.0.0-rc.10
|
|
62
|
+
- @aventara/mcp@1.0.0-rc.10
|
|
63
|
+
|
|
64
|
+
## 1.0.0-rc.9
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- 8c40b81: `@aventara/nest` declares a NestJS project's layout in its manifest (`"aventara": { "host": … }`).
|
|
69
|
+
`aventara new` and `aventara init`, which now also scaffold Next.js apps, and `aventara-build` read a NestJS project's
|
|
70
|
+
paths from it, so the three always agree. A NestJS project is scaffolded exactly as before.
|
|
71
|
+
- 205a379: `AventaraModule` hosts a Framework built with `access` (an access provider, from `@aventara/core`): each protocol
|
|
72
|
+
request's `ctx.$state` is seeded by the provider's `state`, called with `{ headers, request }`, the platform
|
|
73
|
+
request's headers as `Headers` and Express's `req` or Fastify's `request`. It works the same through `config`
|
|
74
|
+
(`forRoot({ config: { ...config, access } })`) and `framework`. The module's own `state` option beside `access` is
|
|
75
|
+
refused while Nest builds the application, and is a type error on a `config` that states `access`.
|
|
76
|
+
- Updated dependencies [205a379]
|
|
77
|
+
- Updated dependencies [a0bb05e]
|
|
78
|
+
- Updated dependencies [86ab319]
|
|
79
|
+
- Updated dependencies [fdf52e6]
|
|
80
|
+
- @aventara/core@1.0.0-rc.9
|
|
81
|
+
- @aventara/mcp@1.0.0-rc.9
|
|
82
|
+
|
|
83
|
+
## 1.0.0-rc.8
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
### Patch Changes
|
|
88
|
+
|
|
89
|
+
- Updated dependencies [1c8e4ee]
|
|
90
|
+
- @aventara/core@1.0.0-rc.8
|
|
91
|
+
- @aventara/mcp@1.0.0-rc.8
|
|
92
|
+
|
|
93
|
+
## 1.0.0-rc.7
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
### Patch Changes
|
|
98
|
+
|
|
99
|
+
- Updated dependencies [f917c1c]
|
|
100
|
+
- @aventara/core@1.0.0-rc.7
|
|
101
|
+
- @aventara/mcp@1.0.0-rc.7
|
|
102
|
+
|
|
103
|
+
## 1.0.0-rc.6
|
|
104
|
+
|
|
105
|
+
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.
|
|
106
|
+
|
|
107
|
+
### Patch Changes
|
|
108
|
+
|
|
109
|
+
- Updated dependencies [1cf72e0]
|
|
110
|
+
- @aventara/core@1.0.0-rc.6
|
|
111
|
+
- @aventara/mcp@1.0.0-rc.6
|
|
112
|
+
|
|
113
|
+
## 1.0.0-rc.5
|
|
114
|
+
|
|
115
|
+
### Patch Changes
|
|
116
|
+
|
|
117
|
+
- Updated dependencies [4795ff1]
|
|
118
|
+
- Updated dependencies [9e178ca]
|
|
119
|
+
- @aventara/core@1.0.0-rc.5
|
|
120
|
+
- @aventara/mcp@1.0.0-rc.5
|
|
121
|
+
|
|
122
|
+
## 1.0.0-rc.4
|
|
123
|
+
|
|
124
|
+
### Patch Changes
|
|
125
|
+
|
|
126
|
+
- Updated dependencies [e3d03a7]
|
|
127
|
+
- Updated dependencies [d10639a]
|
|
128
|
+
- Updated dependencies [f67e68c]
|
|
129
|
+
- Updated dependencies [708556c]
|
|
130
|
+
- Updated dependencies [3a0dfca]
|
|
131
|
+
- Updated dependencies [99169d2]
|
|
132
|
+
- Updated dependencies [0fd2608]
|
|
133
|
+
- Updated dependencies [154e440]
|
|
134
|
+
- Updated dependencies [2fcffdb]
|
|
135
|
+
- @aventara/core@1.0.0-rc.4
|
|
136
|
+
- @aventara/mcp@1.0.0-rc.4
|
|
137
|
+
|
|
138
|
+
## 1.0.0-rc.3
|
|
139
|
+
|
|
140
|
+
### Patch Changes
|
|
141
|
+
|
|
142
|
+
- Updated dependencies [b38034d]
|
|
143
|
+
- Updated dependencies [b38034d]
|
|
144
|
+
- Updated dependencies [b38034d]
|
|
145
|
+
- Updated dependencies [b38034d]
|
|
146
|
+
- Updated dependencies [b38034d]
|
|
147
|
+
- Updated dependencies [b38034d]
|
|
148
|
+
- @aventara/core@1.0.0-rc.3
|
|
149
|
+
- @aventara/mcp@1.0.0-rc.3
|
|
150
|
+
|
|
151
|
+
## 1.0.0-rc.2
|
|
152
|
+
|
|
153
|
+
### Patch Changes
|
|
154
|
+
|
|
155
|
+
- `AventaraModule.forRoot({ config })` reports a Pick layer that names what its parent hid with the sentence first, as
|
|
156
|
+
`createFramework` does: an inline `client: { exposed: false, exemptions: { resources: { Comment: true } } }` whose
|
|
157
|
+
root hid `Comment` fails at the key with
|
|
158
|
+
`Type 'true' is not assignable to type 'true & (readonly ["Error: client cannot expose 'Comment': hidden by root"] & …)'`.
|
|
159
|
+
- 9366e16: MCP endpoint polish:
|
|
160
|
+
|
|
161
|
+
- A request with no bearer credentials (no `Authorization` header, or another scheme such as `Basic`) is answered
|
|
162
|
+
`401` with `WWW-Authenticate: Bearer resource_metadata="…"` and no error code, as RFC 6750 §3.1 asks; its body no
|
|
163
|
+
longer says "Missing Authorization header". A bad bearer token keeps `error="invalid_token"`.
|
|
164
|
+
- A `mcp.authorization.verify` that throws is still answered `401`, and is now reported to the `diagnostics` sink as
|
|
165
|
+
an `AvMcpDiagnostic` holding the error.
|
|
166
|
+
- With `mcp` configured and `@aventara/mcp` missing, the startup error names the command that installs it at the
|
|
167
|
+
installed `@aventara/core`'s exact version: `npm install --save-exact @aventara/mcp@<version>`.
|
|
168
|
+
|
|
169
|
+
- Updated dependencies [9366e16]
|
|
170
|
+
- Updated dependencies [9366e16]
|
|
171
|
+
- Updated dependencies [9366e16]
|
|
172
|
+
- Updated dependencies [9366e16]
|
|
173
|
+
- @aventara/core@1.0.0-rc.2
|
|
174
|
+
- @aventara/mcp@1.0.0-rc.2
|
|
175
|
+
|
|
3
176
|
## 1.0.0-rc.1
|
|
4
177
|
|
|
5
178
|
### Patch Changes
|
|
@@ -65,7 +238,6 @@
|
|
|
65
238
|
nullable: false,
|
|
66
239
|
list: false,
|
|
67
240
|
lifecycle: ["VIRTUAL"],
|
|
68
|
-
format: "url",
|
|
69
241
|
},
|
|
70
242
|
pipelines: {
|
|
71
243
|
compute: {
|
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ export async function aventaraConfig(prisma: PrismaService) {
|
|
|
51
51
|
const { error } = diagnostic;
|
|
52
52
|
logger.error(
|
|
53
53
|
`${diagnostic.code} (request ${diagnostic.requestId}): raw error`,
|
|
54
|
-
error instanceof Error
|
|
54
|
+
(error instanceof Error && error.stack) || String(error),
|
|
55
55
|
);
|
|
56
56
|
},
|
|
57
57
|
adapter: await createPrismaAdapter({
|
|
@@ -140,7 +140,7 @@ rest of `<api>/_*`. Nest's `RouterExplorer` does not list platform routes, so th
|
|
|
140
140
|
own when it mounts them, through Nest's `Logger` (context `AventaraModule`; `logger: false` silences it like any other):
|
|
141
141
|
|
|
142
142
|
```text
|
|
143
|
-
[Nest] LOG [AventaraModule] Aventara mounted 10 operations at /api (+ GET /api/_contract, POST /api/_transactions)
|
|
143
|
+
[Nest] LOG [AventaraModule] Aventara mounted 10 operations at /api (+ GET /api/_contract, POST /api/_transactions, POST /api/_all)
|
|
144
144
|
```
|
|
145
145
|
|
|
146
146
|
An internal failure (`A3000`–`A3004`, answered as a 500) is logged as one error line through Nest's `Logger`
|
|
@@ -203,6 +203,11 @@ const signedIn: AvGuard = (ctx) => ctx.$state.user !== undefined;
|
|
|
203
203
|
runs before it (`app.use` middleware, an earlier `onRequest` hook) has run.
|
|
204
204
|
- Use `app.use(...)` for the middleware that finds the user (a Passport `authenticate` call, a session or JWT check).
|
|
205
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 } })`.
|
|
206
211
|
- Throw `new AvError("A4001", "…")` from `state` to refuse the request (`403`); any other throw is `500 A3000`, with the
|
|
207
212
|
error in your `diagnostics` sink.
|
|
208
213
|
- A plan sent to `/_transactions` gets one state for all its steps and transaction stages. A ctx call a stage makes
|
|
@@ -229,15 +234,20 @@ const signedIn: AvGuard = (ctx) => ctx.$state.user !== undefined;
|
|
|
229
234
|
## The MCP endpoint
|
|
230
235
|
|
|
231
236
|
With `mcp` in the configuration, the module also mounts the MCP server of `@aventara/mcp` at that path, on Express and
|
|
232
|
-
Fastify alike, and logs `Aventara mounted the MCP endpoint at /mcp (…)`. Install it beside this package, at
|
|
233
|
-
version:
|
|
237
|
+
Fastify alike, and logs `Aventara mounted the MCP endpoint at /mcp (…)`. Install it beside this package, at
|
|
238
|
+
`@aventara/core`'s exact version:
|
|
234
239
|
|
|
235
240
|
```bash
|
|
236
|
-
npm install @aventara/mcp@
|
|
241
|
+
npm install --save-exact @aventara/mcp@<your @aventara/core version>
|
|
237
242
|
```
|
|
238
243
|
|
|
239
244
|
It is an optional peer dependency: an application without `mcp` does not need it, and one with `mcp` and without the
|
|
240
|
-
package refuses to start, naming it
|
|
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.11), or remove `mcp` from the configuration.
|
|
249
|
+
```
|
|
250
|
+
As for the protocol's routes, the module only mounts: the endpoint's body is read
|
|
241
251
|
raw, the request's URL comes from the platform's `protocol` and `host` (set `trust proxy` on Express or `trustProxy` on
|
|
242
252
|
Fastify behind a proxy), and every answer is `@aventara/mcp`'s. The `state` option seeds each tool call's `ctx.$state`.
|
|
243
253
|
See `@aventara/mcp`'s README for the tools, the contract resource and authorization.
|
|
@@ -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
|
+
}
|
|
@@ -5,25 +5,35 @@ import { type PendingProtocolHost } from "./protocol-host.factory.js";
|
|
|
5
5
|
* How the application's Framework enters Nest: your own instance
|
|
6
6
|
* (`{ framework }`, from `createFramework`) or a configuration the module
|
|
7
7
|
* passes to `createFramework` itself (`{ config }`). Pass exactly one. Either
|
|
8
|
-
* arm may add `state`, which seeds each protocol request's `ctx.$state
|
|
8
|
+
* arm may add `state`, which seeds each protocol request's `ctx.$state`,
|
|
9
|
+
* unless the Framework is built with `access`, whose `state` seeds it.
|
|
9
10
|
*/
|
|
10
11
|
export type AventaraModuleOptions<M extends AvAdapterModel = AvAdapterModel, Cfg = AvConfigAny> = ({
|
|
11
12
|
readonly framework: AvFramework;
|
|
12
13
|
readonly config?: never;
|
|
13
14
|
} | {
|
|
14
|
-
readonly config: Cfg &
|
|
15
|
+
readonly config: Cfg & AvConfigExact<M, Cfg> & AvConfig<M, Cfg>;
|
|
15
16
|
readonly framework?: never;
|
|
16
|
-
}) & {
|
|
17
|
+
}) & StateBesideAccess<Cfg> & {
|
|
17
18
|
/**
|
|
18
19
|
* Seeds a protocol request's `ctx.$state`, once per request, with the
|
|
19
20
|
* platform's own request: Express's `req`, or Fastify's `request` (its Node
|
|
20
21
|
* request is `request.raw`). What middleware already set there, such as
|
|
21
22
|
* Passport's `req.user`, flows into every stage of the request. A throw is
|
|
22
23
|
* answered like any developer failure: a thrown `AvError` as its own
|
|
23
|
-
* outcome, anything else `500 A3000`. Omitted, the state is `{}
|
|
24
|
+
* outcome, anything else `500 A3000`. Omitted, the state is `{}`, or what
|
|
25
|
+
* the Framework's `access` provider seeds; not together with `access`.
|
|
24
26
|
*/
|
|
25
27
|
state?(request: unknown): AvState | Promise<AvState>;
|
|
26
28
|
};
|
|
29
|
+
/** `state` refused beside a configuration that states `access`, by the sentence that says why. */
|
|
30
|
+
type StateBesideAccess<Cfg> = [Cfg] extends [never] ? unknown : Cfg extends {
|
|
31
|
+
readonly access: object;
|
|
32
|
+
} ? {
|
|
33
|
+
readonly state?: readonly [
|
|
34
|
+
"Error: `access` seeds each request's state; remove `state`"
|
|
35
|
+
];
|
|
36
|
+
} : unknown;
|
|
27
37
|
/**
|
|
28
38
|
* `forRootAsync`'s options: the Nest idiom when what builds the Framework
|
|
29
39
|
* (a Prisma client, a configuration service) is itself a provider. The
|
|
@@ -62,3 +72,4 @@ export declare class AventaraModule implements NestModule, OnModuleInit {
|
|
|
62
72
|
static forRootAsync<M extends AvAdapterModel, const Cfg = AvConfigAny>(options: AventaraModuleAsyncOptions<M, Cfg>): DynamicModule;
|
|
63
73
|
private static mount;
|
|
64
74
|
}
|
|
75
|
+
export {};
|
package/dist/aventara.module.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createFramework, } from "@aventara/core";
|
|
2
2
|
import { Inject, Module, } from "@nestjs/common";
|
|
3
3
|
import { HttpAdapterHost } from "@nestjs/core";
|
|
4
|
+
import { accessStateSeed } from "./access-state.translator.js";
|
|
4
5
|
import { AVENTARA_FRAMEWORK } from "./framework.token.js";
|
|
5
6
|
import { checkedOptions, recordMount } from "./host-bootstrap.validator.js";
|
|
6
7
|
import { withInternalFailureLog } from "./internal-failure.logger.js";
|
|
@@ -66,7 +67,7 @@ export class AventaraModule {
|
|
|
66
67
|
{
|
|
67
68
|
provide: PROTOCOL_HOST,
|
|
68
69
|
inject: [HttpAdapterHost, AVENTARA_FRAMEWORK, OPTIONS, MCP_BINDING],
|
|
69
|
-
useFactory: (application, framework, options, mcp) => createPendingProtocolHost(application, framework, options.state?.bind(options), mcp ?? undefined),
|
|
70
|
+
useFactory: (application, framework, options, mcp) => createPendingProtocolHost(application, framework, options.state?.bind(options) ?? accessStateSeed(framework), mcp ?? undefined),
|
|
70
71
|
},
|
|
71
72
|
],
|
|
72
73
|
exports: [AVENTARA_FRAMEWORK],
|
|
@@ -25,7 +25,7 @@ type ExpressRequest = NodeIncoming & {
|
|
|
25
25
|
type ExpressResponse = NodeOutgoing;
|
|
26
26
|
/** Express passes `next`; Nest's handler type declares it as an optional `Function`. */
|
|
27
27
|
type ExpressHandler = (request: ExpressRequest, response: ExpressResponse, next?: unknown) => unknown;
|
|
28
|
-
|
|
28
|
+
type ExpressPlatform = {
|
|
29
29
|
get(path: string, handler: ExpressHandler): unknown;
|
|
30
30
|
post(path: string, handler: ExpressHandler): unknown;
|
|
31
31
|
delete(path: string, handler: ExpressHandler): unknown;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mcpRequest, NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
1
|
+
import { disconnectOf, mcpRequest, NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
2
2
|
import { MCP_ROUTES, PROTOCOL_ROUTES, } from "./protocol-route.table.js";
|
|
3
3
|
import { readRequestBody, unreadBodyAnswer, } from "./request-body.reader.js";
|
|
4
4
|
const REGISTER = { GET: "get", POST: "post", DELETE: "delete" };
|
|
@@ -27,7 +27,7 @@ export function createExpressProtocolHost(platform, binding, entrypoint, maxRequ
|
|
|
27
27
|
for (const method of route.methods) {
|
|
28
28
|
platform[REGISTER[method]](literalPath(route.path), exactly(route.path, async (request, response) => {
|
|
29
29
|
const body = bodyOf(request);
|
|
30
|
-
writeAnswer(response, await answer(mcpRequest(request.method, request, request.originalUrl, request, body.kind === "read" ? body.content : NO_BYTES, request.socket.remoteAddress), state === undefined ? undefined : () => state(request)));
|
|
30
|
+
await writeAnswer(response, await answer(mcpRequest(request.method, request, request.originalUrl, request, body.kind === "read" ? body.content : NO_BYTES, request.socket.remoteAddress), state === undefined ? undefined : () => state(request)));
|
|
31
31
|
}));
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -35,8 +35,9 @@ export function createExpressProtocolHost(platform, binding, entrypoint, maxRequ
|
|
|
35
35
|
const answer = PROTOCOL_ROUTES[route.kind](binding);
|
|
36
36
|
platform[REGISTER[route.method]](literalPath(`${entrypoint}${route.path}`), async (request, response) => {
|
|
37
37
|
const body = bodyOf(request);
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const disconnect = disconnectOf(response);
|
|
39
|
+
await writeAnswer(response, body.kind === "read"
|
|
40
|
+
? await answer(translated(request, entrypoint, body.content), state === undefined ? undefined : () => state(request), disconnect)
|
|
40
41
|
: unreadBodyAnswer(binding, body));
|
|
41
42
|
});
|
|
42
43
|
}
|
|
@@ -51,7 +52,7 @@ export function createExpressProtocolHost(platform, binding, entrypoint, maxRequ
|
|
|
51
52
|
handOn(next);
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
|
-
writeAnswer(response, absent);
|
|
55
|
+
void writeAnswer(response, absent);
|
|
55
56
|
});
|
|
56
57
|
},
|
|
57
58
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mcpRequest, NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
1
|
+
import { disconnectOf, mcpRequest, NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
2
2
|
import { MCP_ROUTES, PROTOCOL_ROUTES } from "./protocol-route.table.js";
|
|
3
3
|
import { readRequestBody, unreadBodyAnswer } from "./request-body.reader.js";
|
|
4
4
|
const UNMATCHABLE = /\*|%(?:3A|2B|23|24|26|2C|2F|3B|3D|3F|40)/i;
|
|
@@ -37,7 +37,7 @@ export function createFastifyProtocolHost(platform, binding, entrypoint, maxRequ
|
|
|
37
37
|
exposeHeadRoute: false,
|
|
38
38
|
onRequest: async (request, reply) => {
|
|
39
39
|
const body = await readRequestBody(request.raw, undefined, mcp.maxRequestBytes);
|
|
40
|
-
write(reply, await answer(mcpRequest(request.raw.method, request, request.raw.url, request.raw, body.kind === "read" ? body.content : NO_BYTES, request.raw.socket.remoteAddress), state === undefined ? undefined : () => state(request)));
|
|
40
|
+
await write(reply, await answer(mcpRequest(request.raw.method, request, request.raw.url, request.raw, body.kind === "read" ? body.content : NO_BYTES, request.raw.socket.remoteAddress), state === undefined ? undefined : () => state(request)));
|
|
41
41
|
return reply;
|
|
42
42
|
},
|
|
43
43
|
handler: () => {
|
|
@@ -53,8 +53,9 @@ export function createFastifyProtocolHost(platform, binding, entrypoint, maxRequ
|
|
|
53
53
|
exposeHeadRoute: false,
|
|
54
54
|
onRequest: async (request, reply) => {
|
|
55
55
|
const body = await readRequestBody(request.raw, request.raw.headersDistinct["content-encoding"], maxRequestBytes);
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const disconnect = disconnectOf(reply.raw);
|
|
57
|
+
await write(reply, body.kind === "read"
|
|
58
|
+
? await answer(translated(request, body.content), state === undefined ? undefined : () => state(request), disconnect)
|
|
58
59
|
: unreadBodyAnswer(binding, body));
|
|
59
60
|
return reply;
|
|
60
61
|
},
|
|
@@ -72,7 +73,7 @@ export function createFastifyProtocolHost(platform, binding, entrypoint, maxRequ
|
|
|
72
73
|
if (absent === undefined) {
|
|
73
74
|
return undefined;
|
|
74
75
|
}
|
|
75
|
-
write(reply, absent);
|
|
76
|
+
await write(reply, absent);
|
|
76
77
|
return reply;
|
|
77
78
|
});
|
|
78
79
|
},
|
|
@@ -83,5 +84,5 @@ function literalPath(path) {
|
|
|
83
84
|
}
|
|
84
85
|
function write(reply, answer) {
|
|
85
86
|
reply.hijack();
|
|
86
|
-
writeAnswer(reply.raw, answer, reply.getHeaders());
|
|
87
|
+
return writeAnswer(reply.raw, answer, reply.getHeaders());
|
|
87
88
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { accessProviderOf } from "@aventara/core/internal";
|
|
1
2
|
const NEITHER_ARM = "AventaraModule: the options carry neither `framework` nor `config`; pass exactly one.";
|
|
2
3
|
const BOTH_ARMS = "AventaraModule: the options carry both `framework` and `config`; pass exactly one.";
|
|
3
4
|
const NOT_A_FRAMEWORK = "AventaraModule: `framework` is not an Aventara Framework (it has no string `api` and no `contracts.client`); pass what `createFramework` resolved to.";
|
|
4
5
|
const STATE_NOT_A_FUNCTION = "AventaraModule: `state` is not a function; pass `(request) => ({ … })`, called once per protocol request.";
|
|
6
|
+
const STATE_BESIDE_ACCESS = "AventaraModule: the Framework is built with `access`, whose `state` seeds each request's state; remove the module's `state` option.";
|
|
5
7
|
const MOUNTED_TWICE = "AventaraModule is mounted twice in one application; import AventaraModule.forRoot or forRootAsync once.";
|
|
6
8
|
export function checkedOptions(options) {
|
|
7
9
|
const record = typeof options === "object" && options !== null
|
|
@@ -22,6 +24,13 @@ export function checkedOptions(options) {
|
|
|
22
24
|
if (state !== undefined && typeof state !== "function") {
|
|
23
25
|
throw new Error(STATE_NOT_A_FUNCTION);
|
|
24
26
|
}
|
|
27
|
+
if (state !== undefined &&
|
|
28
|
+
(hasFramework
|
|
29
|
+
? accessProviderOf(record.framework) !== undefined
|
|
30
|
+
: record.config?.access !==
|
|
31
|
+
undefined)) {
|
|
32
|
+
throw new Error(STATE_BESIDE_ACCESS);
|
|
33
|
+
}
|
|
25
34
|
return record;
|
|
26
35
|
}
|
|
27
36
|
function isFramework(value) {
|
|
@@ -17,7 +17,11 @@ export function internalFailureLine(diagnostic) {
|
|
|
17
17
|
? `${diagnostic.resource}.${diagnostic.family}.${diagnostic.variant}`
|
|
18
18
|
: diagnostic.kind === "transaction"
|
|
19
19
|
? "a transaction"
|
|
20
|
-
:
|
|
20
|
+
: diagnostic.kind === "can"
|
|
21
|
+
? diagnostic.question === undefined
|
|
22
|
+
? "the access questions"
|
|
23
|
+
: `access question ${diagnostic.question} (${diagnostic.resource} ${diagnostic.action})`
|
|
24
|
+
: "the MCP endpoint";
|
|
21
25
|
const step = diagnostic.operation === undefined
|
|
22
26
|
? ""
|
|
23
27
|
: `, plan operation ${diagnostic.operation}`;
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
const MCP_PACKAGE = "@aventara/mcp";
|
|
2
|
-
const missing = (specifier) => `AventaraModule: the configuration has \`mcp\`, and the package that serves it, ${specifier}, is not installed. Install it
|
|
2
|
+
const missing = (specifier, coreVersion) => `AventaraModule: the configuration has \`mcp\`, and the package that serves it, ${specifier}, is not installed. Install it at @aventara/core's exact version (npm install --save-exact ${specifier}@${coreVersion ?? "<your @aventara/core version>"}), or remove \`mcp\` from the configuration.`;
|
|
3
|
+
async function installedCoreVersion() {
|
|
4
|
+
try {
|
|
5
|
+
const manifest = new URL("../package.json", import.meta.resolve("@aventara/core"));
|
|
6
|
+
const { default: read } = (await import(manifest.href, {
|
|
7
|
+
with: { type: "json" },
|
|
8
|
+
}));
|
|
9
|
+
return read.name === "@aventara/core" && typeof read.version === "string"
|
|
10
|
+
? read.version
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
3
17
|
function isMissing(error, specifier) {
|
|
4
18
|
const { code, message } = (error ?? {});
|
|
5
19
|
return ((code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") &&
|
|
@@ -16,7 +30,7 @@ export async function loadMcpBinding(framework, specifier = MCP_PACKAGE) {
|
|
|
16
30
|
}
|
|
17
31
|
catch (error) {
|
|
18
32
|
if (isMissing(error, specifier)) {
|
|
19
|
-
throw new Error(missing(specifier));
|
|
33
|
+
throw new Error(missing(specifier, await installedCoreVersion()));
|
|
20
34
|
}
|
|
21
35
|
throw error;
|
|
22
36
|
}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
import type { AvProtocolRequest
|
|
1
|
+
import type { AvProtocolRequest } from "@aventara/core/protocol";
|
|
2
2
|
import type { McpRequest } from "./mcp-binding.loader.js";
|
|
3
|
+
import type { RouteResponse } from "./protocol-route.table.js";
|
|
3
4
|
export type NodeIncoming = AsyncIterable<Uint8Array> & {
|
|
4
5
|
readonly headersDistinct: Readonly<Record<string, readonly string[]>>;
|
|
5
6
|
};
|
|
6
7
|
export type NodeOutgoing = {
|
|
7
8
|
writeHead(status: number, headers: Readonly<Record<string, string | number | readonly string[]>>): unknown;
|
|
8
|
-
end(body
|
|
9
|
+
end(body?: string): unknown;
|
|
10
|
+
/** A header already set on the response (Express's middleware sets its own here). */
|
|
11
|
+
getHeader(name: string): string | number | readonly string[] | undefined;
|
|
12
|
+
/** `false` when the socket's buffer is full: wait for `drain`. */
|
|
13
|
+
write(chunk: string): boolean;
|
|
14
|
+
once(event: "close" | "drain", listener: () => void): unknown;
|
|
15
|
+
removeListener(event: "close" | "drain", listener: () => void): unknown;
|
|
16
|
+
destroy(): unknown;
|
|
17
|
+
/** Sends the status line and headers now, before any body is written. */
|
|
18
|
+
flushHeaders(): void;
|
|
19
|
+
/** Whether the whole answer was handed to the socket. */
|
|
20
|
+
readonly writableFinished: boolean;
|
|
21
|
+
/** Whether the response was ended or the socket went away. */
|
|
22
|
+
readonly writableEnded: boolean;
|
|
23
|
+
readonly destroyed: boolean;
|
|
24
|
+
/** The `compression` middleware's: sends what it has compressed so far. */
|
|
25
|
+
flush?(): void;
|
|
9
26
|
};
|
|
10
27
|
/** Headers a platform's own hooks set on the reply before the protocol answered. */
|
|
11
28
|
export type PlatformHeaders = Readonly<Record<string, string | number | readonly string[] | undefined>>;
|
|
@@ -13,16 +30,23 @@ export type PlatformHeaders = Readonly<Record<string, string | number | readonly
|
|
|
13
30
|
export declare const NO_BYTES: Uint8Array<ArrayBuffer>;
|
|
14
31
|
/** A request URL's raw pathname: the query is dropped and nothing is URL-decoded. */
|
|
15
32
|
export declare function pathnameOf(url: string): string;
|
|
16
|
-
/** The request as the protocol reads it: path relative to the entrypoint, distinct headers, raw body. */
|
|
33
|
+
/** The request as the protocol reads it: path relative to the entrypoint, raw query, distinct headers, raw body. */
|
|
17
34
|
export declare function protocolRequest(method: string, url: string, incoming: NodeIncoming, entrypoint: string, content: Uint8Array): AvProtocolRequest;
|
|
18
35
|
/** The request as `@aventara/mcp` reads it: absolute URL, distinct headers, raw body, peer address. */
|
|
19
36
|
export declare function mcpRequest(method: string, origin: {
|
|
20
37
|
readonly protocol: string;
|
|
21
38
|
readonly host: string;
|
|
22
39
|
}, url: string, incoming: NodeIncoming, content: Uint8Array, remoteAddress: string | undefined): McpRequest;
|
|
40
|
+
/** Aborted when the client goes away before the answer was written. */
|
|
41
|
+
export declare function disconnectOf(response: NodeOutgoing): AbortSignal;
|
|
23
42
|
/**
|
|
24
43
|
* Writes the protocol's answer as it is. Headers the platform's hooks already
|
|
25
44
|
* set (CORS, say) go beneath it; a protocol header of the same name, in any
|
|
26
|
-
* case, wins
|
|
45
|
+
* case, wins, except `Vary`: the protocol's names join the platform's
|
|
46
|
+
* (`mergedVary`), which Express holds on the response and Fastify hands in
|
|
47
|
+
* `beneath`. A streamed body (`/_all` under NDJSON) is piped: each line
|
|
48
|
+
* written as core yields it, and flushed through `compression` when that
|
|
49
|
+
* middleware wraps the response. Writing stops when the client goes away,
|
|
50
|
+
* and the lines are no longer pulled (`return()`).
|
|
27
51
|
*/
|
|
28
|
-
export declare function writeAnswer(response: NodeOutgoing, answer:
|
|
52
|
+
export declare function writeAnswer(response: NodeOutgoing, answer: RouteResponse, beneath?: PlatformHeaders): Promise<void>;
|
|
@@ -3,10 +3,15 @@ export function pathnameOf(url) {
|
|
|
3
3
|
const query = url.indexOf("?");
|
|
4
4
|
return query === -1 ? url : url.slice(0, query);
|
|
5
5
|
}
|
|
6
|
+
function queryOf(url) {
|
|
7
|
+
const query = url.indexOf("?");
|
|
8
|
+
return query === -1 ? "" : url.slice(query + 1);
|
|
9
|
+
}
|
|
6
10
|
export function protocolRequest(method, url, incoming, entrypoint, content) {
|
|
7
11
|
return {
|
|
8
12
|
method,
|
|
9
13
|
path: pathnameOf(url).slice(entrypoint.length),
|
|
14
|
+
query: queryOf(url),
|
|
10
15
|
headers: incoming.headersDistinct,
|
|
11
16
|
body: { kind: "raw", content },
|
|
12
17
|
};
|
|
@@ -20,7 +25,35 @@ export function mcpRequest(method, origin, url, incoming, content, remoteAddress
|
|
|
20
25
|
remoteAddress,
|
|
21
26
|
};
|
|
22
27
|
}
|
|
23
|
-
export function
|
|
28
|
+
export function disconnectOf(response) {
|
|
29
|
+
const disconnect = new AbortController();
|
|
30
|
+
response.once("close", () => {
|
|
31
|
+
if (!response.writableFinished) {
|
|
32
|
+
disconnect.abort();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
return disconnect.signal;
|
|
36
|
+
}
|
|
37
|
+
function textOf(value) {
|
|
38
|
+
return value === undefined
|
|
39
|
+
? ""
|
|
40
|
+
: typeof value === "object"
|
|
41
|
+
? value.join(", ")
|
|
42
|
+
: String(value);
|
|
43
|
+
}
|
|
44
|
+
function mergedVary(lists) {
|
|
45
|
+
const names = new Map();
|
|
46
|
+
for (const list of lists) {
|
|
47
|
+
for (const name of list.split(",")) {
|
|
48
|
+
const trimmed = name.trim();
|
|
49
|
+
if (trimmed !== "" && !names.has(trimmed.toLowerCase())) {
|
|
50
|
+
names.set(trimmed.toLowerCase(), trimmed);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return names.has("*") ? "*" : [...names.values()].join(", ");
|
|
55
|
+
}
|
|
56
|
+
export async function writeAnswer(response, answer, beneath = {}) {
|
|
24
57
|
const own = new Set(Object.keys(answer.headers).map((name) => name.toLowerCase()));
|
|
25
58
|
const headers = {};
|
|
26
59
|
for (const [name, value] of Object.entries(beneath)) {
|
|
@@ -28,6 +61,52 @@ export function writeAnswer(response, answer, beneath = {}) {
|
|
|
28
61
|
headers[name] = value;
|
|
29
62
|
}
|
|
30
63
|
}
|
|
31
|
-
|
|
32
|
-
|
|
64
|
+
const written = {
|
|
65
|
+
...headers,
|
|
66
|
+
...answer.headers,
|
|
67
|
+
};
|
|
68
|
+
const vary = Object.keys(answer.headers).find((name) => name.toLowerCase() === "vary");
|
|
69
|
+
if (vary !== undefined) {
|
|
70
|
+
const platform = Object.entries(beneath).flatMap(([name, value]) => name.toLowerCase() === "vary" ? [textOf(value)] : []);
|
|
71
|
+
written[vary] = mergedVary([
|
|
72
|
+
textOf(response.getHeader("vary")),
|
|
73
|
+
...platform,
|
|
74
|
+
textOf(answer.headers[vary]),
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
response.writeHead(answer.status, written);
|
|
78
|
+
if (typeof answer.body === "string") {
|
|
79
|
+
response.end(answer.body);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
response.flushHeaders();
|
|
83
|
+
try {
|
|
84
|
+
for await (const line of answer.body) {
|
|
85
|
+
if (response.destroyed || response.writableEnded) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (!response.write(line)) {
|
|
89
|
+
await drained(response);
|
|
90
|
+
}
|
|
91
|
+
response.flush?.();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
response.destroy();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!response.destroyed) {
|
|
99
|
+
response.end();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function drained(response) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const done = () => {
|
|
105
|
+
response.removeListener("drain", done);
|
|
106
|
+
response.removeListener("close", done);
|
|
107
|
+
resolve();
|
|
108
|
+
};
|
|
109
|
+
response.once("drain", done);
|
|
110
|
+
response.once("close", done);
|
|
111
|
+
});
|
|
33
112
|
}
|
|
@@ -1,41 +1,19 @@
|
|
|
1
1
|
import type { AvFramework, AvState } from "@aventara/core";
|
|
2
2
|
import type { AbstractHttpAdapter } from "@nestjs/core";
|
|
3
|
-
import type { McpBinding
|
|
4
|
-
import type { SurfaceRoute } from "./protocol-route.table.js";
|
|
3
|
+
import type { McpBinding } from "./mcp-binding.loader.js";
|
|
5
4
|
/** The developer's `state` option: seeds a request's `ctx.$state` from its platform request. */
|
|
6
5
|
export type HostState = (request: unknown) => AvState | Promise<AvState>;
|
|
7
6
|
/** The protocol, bound once at startup, and the one call that mounts it. */
|
|
8
7
|
export type ProtocolHost = {
|
|
9
8
|
readonly mount: () => void;
|
|
10
9
|
};
|
|
11
|
-
/**
|
|
12
|
-
* Binds `framework`'s protocol for the application's platform, refusing an
|
|
13
|
-
* unsupported platform while providers are instantiated, so `NestFactory.create`
|
|
14
|
-
* rejects before the application listens. The routes are mounted later, from
|
|
15
|
-
* the module's `configure()`.
|
|
16
|
-
*/
|
|
17
|
-
export declare function createProtocolHost(adapter: AbstractHttpAdapter | undefined, framework: AvFramework, state?: HostState, mcp?: McpBinding): ProtocolHost;
|
|
18
|
-
/**
|
|
19
|
-
* What the MCP mount registered: the endpoint, and its OAuth metadata route
|
|
20
|
-
* or, without one, the rule an open endpoint answers by — e.g. `Aventara
|
|
21
|
-
* mounted the MCP endpoint at /mcp (+ GET
|
|
22
|
-
* /.well-known/oauth-protected-resource/mcp)`.
|
|
23
|
-
*/
|
|
24
|
-
export declare function mcpMountedLine(routes: readonly McpRoute[]): string;
|
|
25
|
-
/**
|
|
26
|
-
* What one mount registered, read off `protocol.surface()`: the operation
|
|
27
|
-
* count under the entrypoint, then each framework route by method and path —
|
|
28
|
-
* e.g. `Aventara mounted 5 operations at /api (+ GET /api/_contract, POST
|
|
29
|
-
* /api/_transactions)`.
|
|
30
|
-
*/
|
|
31
|
-
export declare function mountedLine(routes: readonly SurfaceRoute[], entrypoint: string): string;
|
|
32
10
|
/** What `AventaraModule` holds: a host, perhaps waiting for its platform, and the check that it has one. */
|
|
33
11
|
export type PendingProtocolHost = ProtocolHost & {
|
|
34
12
|
/** Throws when no platform arrived, or the one that did is refused. */
|
|
35
13
|
readonly assertReady: () => void;
|
|
36
14
|
};
|
|
37
15
|
/** The parts of Nest's `HttpAdapterHost` the module reads. */
|
|
38
|
-
|
|
16
|
+
type AdapterHostView = {
|
|
39
17
|
readonly httpAdapter: AbstractHttpAdapter | undefined;
|
|
40
18
|
readonly init$: {
|
|
41
19
|
subscribe(next: () => void): unknown;
|
|
@@ -46,3 +24,4 @@ export type AdapterHostView = {
|
|
|
46
24
|
* are instantiated (Nest's testing module).
|
|
47
25
|
*/
|
|
48
26
|
export declare function createPendingProtocolHost(application: AdapterHostView, framework: AvFramework, state?: HostState, mcp?: McpBinding): PendingProtocolHost;
|
|
27
|
+
export {};
|
|
@@ -12,7 +12,7 @@ const PLATFORM_MOUNTERS = {
|
|
|
12
12
|
}, binding, entrypoint, maxRequestBytes, state, mcp),
|
|
13
13
|
fastify: (adapter, binding, entrypoint, maxRequestBytes, state, mcp) => createFastifyProtocolHost(adapter.getInstance(), binding, entrypoint, maxRequestBytes, state, mcp),
|
|
14
14
|
};
|
|
15
|
-
|
|
15
|
+
function createProtocolHost(adapter, framework, state, mcp) {
|
|
16
16
|
const platform = checkedPlatform(adapter?.getType());
|
|
17
17
|
const binding = AvProtocol.bind(framework);
|
|
18
18
|
const host = PLATFORM_MOUNTERS[platform](adapter, binding, framework.api, framework.contracts.client.limits.maxRequestBytes, state, mcp);
|
|
@@ -26,7 +26,7 @@ export function createProtocolHost(adapter, framework, state, mcp) {
|
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
function mcpMountedLine(routes) {
|
|
30
30
|
const endpoint = routes.find((route) => route.kind === "endpoint");
|
|
31
31
|
const metadata = routes.find((route) => route.kind === "metadata");
|
|
32
32
|
return `Aventara mounted the MCP endpoint at ${endpoint?.path ?? "/"} (${metadata === undefined
|
|
@@ -34,7 +34,7 @@ export function mcpMountedLine(routes) {
|
|
|
34
34
|
: `+ GET ${metadata.path}`})`;
|
|
35
35
|
}
|
|
36
36
|
const MOUNT_LOGGER = new Logger("AventaraModule");
|
|
37
|
-
|
|
37
|
+
function mountedLine(routes, entrypoint) {
|
|
38
38
|
const operations = routes.filter((route) => route.kind === "operation");
|
|
39
39
|
const framework = routes
|
|
40
40
|
.filter((route) => route.kind !== "operation")
|
|
@@ -4,17 +4,22 @@ import type { McpBinding, McpRequest, McpRoute } from "./mcp-binding.loader.js";
|
|
|
4
4
|
/** One route `protocol.surface()` lists. */
|
|
5
5
|
export type SurfaceRoute = ReturnType<AvProtocolBinding["surface"]>[number];
|
|
6
6
|
/** The developer's `state` option over one platform request: seeds its `ctx.$state`. */
|
|
7
|
-
|
|
7
|
+
type StateSeed = () => AvState | Promise<AvState>;
|
|
8
|
+
/** An answer a route writes: the protocol's, its body text or, for a streamed `/_all`, lines as they settle. */
|
|
9
|
+
export type RouteResponse = Awaited<ReturnType<AvProtocolBinding["handleAll"]>>;
|
|
8
10
|
/** What a mounted route answers its request with: a bound protocol member. */
|
|
9
|
-
export type RouteAnswer = (request: AvProtocolRequest, state: StateSeed | undefined
|
|
11
|
+
export type RouteAnswer = (request: AvProtocolRequest, state: StateSeed | undefined,
|
|
12
|
+
/** Aborted when the client goes away; only `/_all` reads it. */
|
|
13
|
+
disconnect: AbortSignal) => RouteResponse | Promise<RouteResponse>;
|
|
10
14
|
/** The bound member each kind of surface route is answered by. */
|
|
11
15
|
export type ProtocolRouteTable = {
|
|
12
16
|
readonly [K in SurfaceRoute["kind"]]: (binding: AvProtocolBinding) => RouteAnswer;
|
|
13
17
|
};
|
|
14
18
|
export declare const PROTOCOL_ROUTES: ProtocolRouteTable;
|
|
15
19
|
/** What a mounted MCP route answers its request with: a bound `@aventara/mcp` member. */
|
|
16
|
-
|
|
20
|
+
type McpRouteAnswer = (request: McpRequest, state: StateSeed | undefined) => AvProtocolResponse | Promise<AvProtocolResponse>;
|
|
17
21
|
/** The bound `@aventara/mcp` member each kind of MCP route is answered by. */
|
|
18
22
|
export declare const MCP_ROUTES: {
|
|
19
23
|
readonly [K in McpRoute["kind"]]: (binding: McpBinding) => McpRouteAnswer;
|
|
20
24
|
};
|
|
25
|
+
export {};
|
|
@@ -2,6 +2,10 @@ export const PROTOCOL_ROUTES = {
|
|
|
2
2
|
operation: (binding) => (request, state) => binding.handleOperation(request, state === undefined ? {} : { state }),
|
|
3
3
|
contract: (binding) => (request) => binding.encodeContract(request),
|
|
4
4
|
transactions: (binding) => (request, state) => binding.handleTransaction(request, state === undefined ? {} : { state }),
|
|
5
|
+
can: (binding) => (request, state) => binding.handleCan(request, state === undefined ? {} : { state }),
|
|
6
|
+
all: (binding) => (request, state, disconnect) => binding.handleAll(request, state === undefined
|
|
7
|
+
? { signal: disconnect }
|
|
8
|
+
: { state, signal: disconnect }),
|
|
5
9
|
};
|
|
6
10
|
export const MCP_ROUTES = {
|
|
7
11
|
endpoint: (binding) => (request, state) => binding.handleEndpoint(request, state === undefined ? {} : { state }),
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/nest",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.11",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "NestJS host integration for Aventara.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": "^22.
|
|
8
|
+
"node": "^22.23.4 || >=24.22.1"
|
|
9
9
|
},
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"CHANGELOG.md"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@aventara/core": "1.0.0-rc.
|
|
25
|
+
"@aventara/core": "1.0.0-rc.11"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@aventara/mcp": "1.0.0-rc.
|
|
28
|
+
"@aventara/mcp": "1.0.0-rc.11",
|
|
29
29
|
"@nestjs/common": "^12.0.0",
|
|
30
30
|
"@nestjs/core": "^12.0.0"
|
|
31
31
|
},
|
|
@@ -34,21 +34,33 @@
|
|
|
34
34
|
"optional": true
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
+
"aventara": {
|
|
38
|
+
"host": {
|
|
39
|
+
"alias": "nest",
|
|
40
|
+
"label": "NestJS",
|
|
41
|
+
"framework": "@nestjs/core",
|
|
42
|
+
"layout": {
|
|
43
|
+
"config": "src/aventara.config",
|
|
44
|
+
"adapter": "src/aventara.adapter",
|
|
45
|
+
"generated": "src/generated/aventara"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
37
49
|
"publishConfig": {
|
|
38
50
|
"access": "public"
|
|
39
51
|
},
|
|
40
52
|
"devDependencies": {
|
|
41
|
-
"@aventara/mcp": "1.0.0-rc.
|
|
42
|
-
"@aventara/prisma7-adapter": "1.0.0-rc.
|
|
43
|
-
"@aventara/testing": "1.0.0-rc.
|
|
44
|
-
"@modelcontextprotocol/sdk": "1.
|
|
45
|
-
"@nestjs/common": "^12.
|
|
46
|
-
"@nestjs/core": "^12.
|
|
47
|
-
"@nestjs/platform-express": "^12.
|
|
48
|
-
"@nestjs/platform-fastify": "^12.
|
|
49
|
-
"@nestjs/testing": "^12.
|
|
50
|
-
"@prisma/adapter-better-sqlite3": "7.10.
|
|
51
|
-
"@prisma/adapter-pg": "7.10.
|
|
53
|
+
"@aventara/mcp": "1.0.0-rc.11",
|
|
54
|
+
"@aventara/prisma7-adapter": "1.0.0-rc.11",
|
|
55
|
+
"@aventara/testing": "1.0.0-rc.11",
|
|
56
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
57
|
+
"@nestjs/common": "^12.2.0",
|
|
58
|
+
"@nestjs/core": "^12.2.0",
|
|
59
|
+
"@nestjs/platform-express": "^12.2.0",
|
|
60
|
+
"@nestjs/platform-fastify": "^12.2.0",
|
|
61
|
+
"@nestjs/testing": "^12.2.0",
|
|
62
|
+
"@prisma/adapter-better-sqlite3": "7.10.1",
|
|
63
|
+
"@prisma/adapter-pg": "7.10.1",
|
|
52
64
|
"reflect-metadata": "^0.2.2",
|
|
53
65
|
"rxjs": "^7.8.2"
|
|
54
66
|
},
|